From 25ce3ec49d316f96eeecc7389b3a2df006f75f4f Mon Sep 17 00:00:00 2001 From: Michael Hanselmann <hansmi@google.com> Date: Fri, 7 Jan 2011 12:31:02 +0100 Subject: [PATCH] Introduce verbose opcode result for console MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: Michael Hanselmann <hansmi@google.com> Reviewed-by: Iustin Pop <iustin@google.com> Reviewed-by: RenΓ© Nussbaumer <rn@google.com> --- Makefile.am | 1 + lib/client/gnt_instance.py | 68 +++++++++-- lib/cmdlib.py | 11 +- lib/constants.py | 12 ++ lib/objects.py | 34 ++++++ test/ganeti.client.gnt_instance_unittest.py | 123 ++++++++++++++++++++ 6 files changed, 240 insertions(+), 9 deletions(-) create mode 100755 test/ganeti.client.gnt_instance_unittest.py diff --git a/Makefile.am b/Makefile.am index 68bf10f3b..6c25dcaaa 100644 --- a/Makefile.am +++ b/Makefile.am @@ -437,6 +437,7 @@ python_tests = \ test/ganeti.backend_unittest.py \ test/ganeti.bdev_unittest.py \ test/ganeti.cli_unittest.py \ + test/ganeti.client.gnt_instance_unittest.py \ test/ganeti.daemon_unittest.py \ test/ganeti.cmdlib_unittest.py \ test/ganeti.compat_unittest.py \ diff --git a/lib/client/gnt_instance.py b/lib/client/gnt_instance.py index f0188a9ae..d26efe776 100644 --- a/lib/client/gnt_instance.py +++ b/lib/client/gnt_instance.py @@ -27,6 +27,7 @@ import itertools import simplejson +import logging from cStringIO import StringIO from ganeti.cli import * @@ -36,6 +37,8 @@ from ganeti import compat from ganeti import utils from ganeti import errors from ganeti import netutils +from ganeti import ssh +from ganeti import objects _SHUTDOWN_CLUSTER = "cluster" @@ -886,15 +889,66 @@ def ConnectToInstanceConsole(opts, args): instance_name = args[0] op = opcodes.OpConnectConsole(instance_name=instance_name) - cmd = SubmitOpCode(op, opts=opts) - if opts.show_command: - ToStdout("%s", utils.ShellQuoteArgs(cmd)) + cl = GetClient() + 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: - result = utils.RunCmd(cmd, interactive=True) - if result.failed: - raise errors.OpExecError("Console command \"%s\" failed: %s" % - (utils.ShellQuoteArgs(cmd), result.fail_reason)) + raise errors.GenericError("Unknown console type '%s'" % console.kind) return constants.EXIT_SUCCESS diff --git a/lib/cmdlib.py b/lib/cmdlib.py index d80c0da8b..996489e5c 100644 --- a/lib/cmdlib.py +++ b/lib/cmdlib.py @@ -7693,8 +7693,15 @@ class LUConnectConsole(NoHooksLU): beparams = cluster.FillBE(instance) console_cmd = hyper.GetShellCommandForConsole(instance, hvparams, beparams) - # build ssh cmdline - return self.ssh.BuildCmd(node, "root", console_cmd, batch=True, tty=True) + console = objects.InstanceConsole(instance=instance.name, + kind=constants.CONS_SSH, + host=node, + user="root", + command=console_cmd) + + assert console.Validate() + + return console.ToDict() class LUReplaceDisks(LogicalUnit): diff --git a/lib/constants.py b/lib/constants.py index 22193691e..16fb71db6 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -223,6 +223,18 @@ SOCAT_USE_ESCAPE = _autoconf.SOCAT_USE_ESCAPE SOCAT_USE_COMPRESS = _autoconf.SOCAT_USE_COMPRESS 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 # expensive. NIST SP 800-131 recommends a minimum of 2048 bits from the year # 2010 on. diff --git a/lib/objects.py b/lib/objects.py index cb1790e3d..fdda582fa 100644 --- a/lib/objects.py +++ b/lib/objects.py @@ -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): """Simple wrapper over ConfigParse that allows serialization. diff --git a/test/ganeti.client.gnt_instance_unittest.py b/test/ganeti.client.gnt_instance_unittest.py new file mode 100755 index 000000000..51410958a --- /dev/null +++ b/test/ganeti.client.gnt_instance_unittest.py @@ -0,0 +1,123 @@ +#!/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() -- GitLab