On Fri, Jan 7, 2011 at 1:42 PM, Michael Hanselmann <[email protected]> wrote: > 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 <[email protected]> > --- > Makefile.am | 1 + > lib/client/gnt_instance.py | 64 +++++++++++++-- > lib/cmdlib.py | 11 ++- > lib/constants.py | 12 +++ > lib/objects.py | 34 ++++++++ > test/ganeti.client.gnt_instance_unittest.py | 120 > +++++++++++++++++++++++++++ > 6 files changed, 233 insertions(+), 9 deletions(-) > create mode 100755 test/ganeti.client.gnt_instance_unittest.py > > diff --git a/Makefile.am b/Makefile.am > index 68bf10f..6c25dca 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 f0188a9..512d8b6 100644 > --- a/lib/client/gnt_instance.py > +++ b/lib/client/gnt_instance.py > @@ -36,6 +36,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 +888,63 @@ 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: > + raise errors.OpExecError("Running \"%s\" as %...@%s failed: %s" % > + (cmd, console.user, console.host, > + result.fail_reason)) > 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 d80c0da..996489e 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 2219369..16fb71d 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 cb1790e..fdda582 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 0000000..861c1c1 > --- /dev/null > +++ b/test/ganeti.client.gnt_instance_unittest.py > @@ -0,0 +1,120 @@ > +#!/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(" [email protected] " 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) > + > + 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() > -- > 1.7.3.1
LGTM > >
