Commit 25ce3ec4 authored by Michael Hanselmann's avatar Michael Hanselmann

Introduce verbose opcode result for console

With this patch OpConnectConsole will no longer just return a command
with arguments, but rather a detailed description about how to connect
to an instance's console. Unittests for some parts are included. Another
patch will follow to adjust the hypervisor abstractions for the new
model.
Signed-off-by: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
Reviewed-by: default avatarRené Nussbaumer <rn@google.com>
parent 2a917701
...@@ -437,6 +437,7 @@ python_tests = \ ...@@ -437,6 +437,7 @@ python_tests = \
test/ganeti.backend_unittest.py \ test/ganeti.backend_unittest.py \
test/ganeti.bdev_unittest.py \ test/ganeti.bdev_unittest.py \
test/ganeti.cli_unittest.py \ test/ganeti.cli_unittest.py \
test/ganeti.client.gnt_instance_unittest.py \
test/ganeti.daemon_unittest.py \ test/ganeti.daemon_unittest.py \
test/ganeti.cmdlib_unittest.py \ test/ganeti.cmdlib_unittest.py \
test/ganeti.compat_unittest.py \ test/ganeti.compat_unittest.py \
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
import itertools import itertools
import simplejson import simplejson
import logging
from cStringIO import StringIO from cStringIO import StringIO
from ganeti.cli import * from ganeti.cli import *
...@@ -36,6 +37,8 @@ from ganeti import compat ...@@ -36,6 +37,8 @@ from ganeti import compat
from ganeti import utils from ganeti import utils
from ganeti import errors from ganeti import errors
from ganeti import netutils from ganeti import netutils
from ganeti import ssh
from ganeti import objects
_SHUTDOWN_CLUSTER = "cluster" _SHUTDOWN_CLUSTER = "cluster"
...@@ -886,15 +889,66 @@ def ConnectToInstanceConsole(opts, args): ...@@ -886,15 +889,66 @@ def ConnectToInstanceConsole(opts, args):
instance_name = args[0] instance_name = args[0]
op = opcodes.OpConnectConsole(instance_name=instance_name) op = opcodes.OpConnectConsole(instance_name=instance_name)
cmd = SubmitOpCode(op, opts=opts)
if opts.show_command: cl = GetClient()
ToStdout("%s", utils.ShellQuoteArgs(cmd)) try:
cluster_name = cl.QueryConfigValues(["cluster_name"])[0]
console_data = SubmitOpCode(op, opts=opts, cl=cl)
finally:
# Ensure client connection is closed while external commands are run
cl.Close()
del cl
return _DoConsole(objects.InstanceConsole.FromDict(console_data),
opts.show_command, cluster_name)
def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
_runcmd_fn=utils.RunCmd):
"""Acts based on the result of L{opcodes.OpConnectConsole}.
@type console: L{objects.InstanceConsole}
@param console: Console object
@type show_command: bool
@param show_command: Whether to just display commands
@type cluster_name: string
@param cluster_name: Cluster name as retrieved from master daemon
"""
assert console.Validate()
if console.kind == constants.CONS_MESSAGE:
feedback_fn(console.message)
elif console.kind == constants.CONS_VNC:
feedback_fn("Instance %s has VNC listening on %s:%s (display %s),"
" URL <vnc://%s:%s/>",
console.instance, console.host, console.port,
console.display, console.host, console.port)
elif console.kind == constants.CONS_SSH:
# Convert to string if not already one
if isinstance(console.command, basestring):
cmd = console.command
else:
cmd = utils.ShellQuoteArgs(console.command)
srun = ssh.SshRunner(cluster_name=cluster_name)
ssh_cmd = srun.BuildCmd(console.host, console.user, cmd,
batch=True, quiet=False, tty=True)
if show_command:
feedback_fn(utils.ShellQuoteArgs(ssh_cmd))
else:
result = _runcmd_fn(ssh_cmd, interactive=True)
if result.failed:
logging.error("Console command \"%s\" failed with reason '%s' and"
" output %r", result.cmd, result.fail_reason,
result.output)
raise errors.OpExecError("Connection to console of instance %s failed,"
" please check cluster configuration" %
console.instance)
else: else:
result = utils.RunCmd(cmd, interactive=True) raise errors.GenericError("Unknown console type '%s'" % console.kind)
if result.failed:
raise errors.OpExecError("Console command \"%s\" failed: %s" %
(utils.ShellQuoteArgs(cmd), result.fail_reason))
return constants.EXIT_SUCCESS return constants.EXIT_SUCCESS
......
...@@ -7693,8 +7693,15 @@ class LUConnectConsole(NoHooksLU): ...@@ -7693,8 +7693,15 @@ class LUConnectConsole(NoHooksLU):
beparams = cluster.FillBE(instance) beparams = cluster.FillBE(instance)
console_cmd = hyper.GetShellCommandForConsole(instance, hvparams, beparams) console_cmd = hyper.GetShellCommandForConsole(instance, hvparams, beparams)
# build ssh cmdline console = objects.InstanceConsole(instance=instance.name,
return self.ssh.BuildCmd(node, "root", console_cmd, batch=True, tty=True) kind=constants.CONS_SSH,
host=node,
user="root",
command=console_cmd)
assert console.Validate()
return console.ToDict()
class LUReplaceDisks(LogicalUnit): class LUReplaceDisks(LogicalUnit):
......
...@@ -223,6 +223,18 @@ SOCAT_USE_ESCAPE = _autoconf.SOCAT_USE_ESCAPE ...@@ -223,6 +223,18 @@ SOCAT_USE_ESCAPE = _autoconf.SOCAT_USE_ESCAPE
SOCAT_USE_COMPRESS = _autoconf.SOCAT_USE_COMPRESS SOCAT_USE_COMPRESS = _autoconf.SOCAT_USE_COMPRESS
SOCAT_ESCAPE_CODE = "0x1d" SOCAT_ESCAPE_CODE = "0x1d"
#: Console as SSH command
CONS_SSH = "ssh"
#: Console as VNC server
CONS_VNC = "vnc"
#: Display a message for console access
CONS_MESSAGE = "msg"
#: All console types
CONS_ALL = frozenset([CONS_SSH, CONS_VNC, CONS_MESSAGE])
# For RSA keys more bits are better, but they also make operations more # For RSA keys more bits are better, but they also make operations more
# expensive. NIST SP 800-131 recommends a minimum of 2048 bits from the year # expensive. NIST SP 800-131 recommends a minimum of 2048 bits from the year
# 2010 on. # 2010 on.
......
...@@ -1466,6 +1466,40 @@ class QueryFieldsResponse(_QueryResponseBase): ...@@ -1466,6 +1466,40 @@ class QueryFieldsResponse(_QueryResponseBase):
] ]
class InstanceConsole(ConfigObject):
"""Object describing how to access the console of an instance.
"""
__slots__ = [
"instance",
"kind",
"message",
"host",
"port",
"user",
"command",
"display",
]
def Validate(self):
"""Validates contents of this object.
"""
assert self.kind in constants.CONS_ALL, "Unknown console type"
assert self.instance, "Missing instance name"
assert self.message or self.kind in [constants.CONS_SSH, constants.CONS_VNC]
assert self.host or self.kind == constants.CONS_MESSAGE
assert self.port or self.kind in [constants.CONS_MESSAGE,
constants.CONS_SSH]
assert self.user or self.kind in [constants.CONS_MESSAGE,
constants.CONS_VNC]
assert self.command or self.kind in [constants.CONS_MESSAGE,
constants.CONS_VNC]
assert self.display or self.kind in [constants.CONS_MESSAGE,
constants.CONS_SSH]
return True
class SerializableConfigParser(ConfigParser.SafeConfigParser): class SerializableConfigParser(ConfigParser.SafeConfigParser):
"""Simple wrapper over ConfigParse that allows serialization. """Simple wrapper over ConfigParse that allows serialization.
......
#!/usr/bin/python
#
# Copyright (C) 2011 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
"""Script for testing ganeti.client.gnt_instance"""
import unittest
from ganeti import constants
from ganeti import utils
from ganeti import errors
from ganeti import objects
from ganeti.client import gnt_instance
import testutils
class TestConsole(unittest.TestCase):
def setUp(self):
self._output = []
self._cmds = []
self._next_cmd_exitcode = 0
def _Test(self, *args):
return gnt_instance._DoConsole(*args,
feedback_fn=self._Feedback,
_runcmd_fn=self._FakeRunCmd)
def _Feedback(self, msg, *args):
if args:
msg = msg % args
self._output.append(msg)
def _FakeRunCmd(self, cmd, interactive=None):
self.assertTrue(interactive)
self.assertTrue(isinstance(cmd, list))
self._cmds.append(cmd)
return utils.RunResult(self._next_cmd_exitcode, None, "", "", "cmd",
utils._TIMEOUT_NONE, 5)
def testMessage(self):
cons = objects.InstanceConsole(instance="inst98.example.com",
kind=constants.CONS_MESSAGE,
message="Hello World")
self.assertEqual(self._Test(cons, False, "cluster.example.com"),
constants.EXIT_SUCCESS)
self.assertEqual(len(self._cmds), 0)
self.assertEqual(self._output, ["Hello World"])
def testVnc(self):
cons = objects.InstanceConsole(instance="inst1.example.com",
kind=constants.CONS_VNC,
host="node1.example.com",
port=5901,
display=1)
self.assertEqual(self._Test(cons, False, "cluster.example.com"),
constants.EXIT_SUCCESS)
self.assertEqual(len(self._cmds), 0)
self.assertEqual(len(self._output), 1)
self.assertTrue(" inst1.example.com " in self._output[0])
self.assertTrue(" node1.example.com:5901 " in self._output[0])
self.assertTrue("vnc://node1.example.com:5901/" in self._output[0])
def testSshShow(self):
cons = objects.InstanceConsole(instance="inst31.example.com",
kind=constants.CONS_SSH,
host="node93.example.com",
user="user_abc",
command="xm console x.y.z")
self.assertEqual(self._Test(cons, True, "cluster.example.com"),
constants.EXIT_SUCCESS)
self.assertEqual(len(self._cmds), 0)
self.assertEqual(len(self._output), 1)
self.assertTrue(" user_abc@node93.example.com " in self._output[0])
self.assertTrue("'xm console x.y.z'" in self._output[0])
def testSshRun(self):
cons = objects.InstanceConsole(instance="inst31.example.com",
kind=constants.CONS_SSH,
host="node93.example.com",
user="user_abc",
command=["xm", "console", "x.y.z"])
self.assertEqual(self._Test(cons, False, "cluster.example.com"),
constants.EXIT_SUCCESS)
self.assertEqual(len(self._cmds), 1)
self.assertEqual(len(self._output), 0)
# This is very important to prevent escapes from the console
self.assertTrue("-oEscapeChar=none" in self._cmds[0])
def testSshRunFail(self):
cons = objects.InstanceConsole(instance="inst31.example.com",
kind=constants.CONS_SSH,
host="node93.example.com",
user="user_abc",
command=["xm", "console", "x.y.z"])
self._next_cmd_exitcode = 100
self.assertRaises(errors.OpExecError, self._Test,
cons, False, "cluster.example.com")
self.assertEqual(len(self._cmds), 1)
self.assertEqual(len(self._output), 0)
if __name__ == "__main__":
testutils.GanetiTestProgram()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment