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

>
>

Reply via email to