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 = \
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 \
......
......@@ -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
......
......@@ -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):
......
......@@ -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.
......
......@@ -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.
......
#!/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