diff --git a/doc/rapi.rst b/doc/rapi.rst index 9b3bfad20bc6ef62d4c11f533e34c2a54641ba5a..1a45c8cf448a1896b452538890bc0a21235ee669 100644 --- a/doc/rapi.rst +++ b/doc/rapi.rst @@ -984,6 +984,37 @@ Body parameters: Whether to force an unknown variant. +``/2/instances/[instance_name]/console`` +++++++++++++++++++++++++++++++++++++++++ + +Request information for connecting to instance's console. + +Supports the following commands: ``GET``. + +``GET`` +~~~~~~~ + +Returns a dictionary containing information about the instance's +console. Contained keys: + +``instance`` + Instance name. +``kind`` + Console type, one of ``ssh``, ``vnc`` or ``msg``. +``message`` + Message to display (``msg`` type only). +``host`` + Host to connect to (``ssh`` and ``vnc`` only). +``port`` + TCP port to connect to (``vnc`` only). +``user`` + Username to use (``ssh`` only). +``command`` + Command to execute on machine (``ssh`` only) +``display`` + VNC display number (``vnc`` only). + + ``/2/instances/[instance_name]/tags`` +++++++++++++++++++++++++++++++++++++ diff --git a/lib/rapi/client.py b/lib/rapi/client.py index 9e076e80f2bb8f745f2cd2860958ccddfeba8ebc..cb437849db353e66c525dc3120e9b2556248db9e 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -1124,6 +1124,17 @@ class GanetiRapiClient(object): # pylint: disable-msg=R0904 ("/%s/instances/%s/rename" % (GANETI_RAPI_VERSION, instance)), None, body) + def GetInstanceConsole(self, instance): + """Request information for connecting to instance's console. + + @type instance: string + @param instance: Instance name + + """ + return self._SendRequest(HTTP_GET, + ("/%s/instances/%s/console" % + (GANETI_RAPI_VERSION, instance)), None, None) + def GetJobs(self): """Gets all jobs for the cluster. diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py index fceda6dee72cc28fb3abad44fc1608421164a75b..b6c66f66f4d3c820f8fb6ed8b3610ee4893be84d 100644 --- a/lib/rapi/connector.py +++ b/lib/rapi/connector.py @@ -217,6 +217,8 @@ def GetHandlers(node_name_pattern, instance_name_pattern, re.compile(r"^/2/instances/(%s)/disk/(%s)/grow$" % (instance_name_pattern, disk_pattern)): rlib2.R_2_instances_name_disk_grow, + re.compile(r'^/2/instances/(%s)/console$' % instance_name_pattern): + rlib2.R_2_instances_name_console, "/2/groups": rlib2.R_2_groups, re.compile(r'^/2/groups/(%s)$' % group_name_pattern): diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index 1f95bcb224e47155c0c56aa124f9a6716d254f4a..a3bf4cd3fb16a72853877340b4a8633d28de286c 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -1336,6 +1336,30 @@ class R_2_instances_name_disk_grow(baserlib.R_Generic): return baserlib.SubmitJob([op]) +class R_2_instances_name_console(baserlib.R_Generic): + """/2/instances/[instance_name]/console resource. + + """ + GET_ACCESS = [rapi.RAPI_ACCESS_WRITE] + + def GET(self): + """Request information for connecting to instance's console. + + @return: Serialized instance console description, see + L{objects.InstanceConsole} + + """ + client = baserlib.GetClient() + + ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False) + + if console is None: + raise http.HttpServiceUnavailable("Instance console unavailable") + + assert isinstance(console, dict) + return console + + class _R_Tags(baserlib.R_Generic): """ Quasiclass for tagging resources diff --git a/qa/ganeti-qa.py b/qa/ganeti-qa.py index d4c36347483f7141dff86a16609336946450d4dc..78181a3a5baa227faecd4736aafb6abea8ad0f90 100755 --- a/qa/ganeti-qa.py +++ b/qa/ganeti-qa.py @@ -196,6 +196,8 @@ def RunCommonInstanceTests(instance): """ RunTestIf("instance-shutdown", qa_instance.TestInstanceShutdown, instance) + RunTestIf(["instance-shutdown", "instance-console", "rapi"], + qa_rapi.TestRapiStoppedInstanceConsole, instance) RunTestIf("instance-shutdown", qa_instance.TestInstanceStartup, instance) RunTestIf("instance-list", qa_instance.TestInstanceList) @@ -207,6 +209,8 @@ def RunCommonInstanceTests(instance): qa_rapi.TestRapiInstanceModify, instance) RunTestIf("instance-console", qa_instance.TestInstanceConsole, instance) + RunTestIf(["instance-console", "rapi"], + qa_rapi.TestRapiInstanceConsole, instance) RunTestIf("instance-reinstall", qa_instance.TestInstanceShutdown, instance) RunTestIf("instance-reinstall", qa_instance.TestInstanceReinstall, instance) diff --git a/qa/qa_rapi.py b/qa/qa_rapi.py index b3edeb4e3fbb1dccf8e9af611190e15d9c557cd5..d8679f9d223e56c76d61577f5db8580858079b5c 100644 --- a/qa/qa_rapi.py +++ b/qa/qa_rapi.py @@ -29,6 +29,7 @@ from ganeti import constants from ganeti import errors from ganeti import cli from ganeti import rapi +from ganeti import objects import ganeti.rapi.client # pylint: disable-msg=W0611 import ganeti.rapi.client_utils @@ -492,6 +493,25 @@ def TestRapiInstanceModify(instance): }) +def TestRapiInstanceConsole(instance): + """Test getting instance console information via RAPI""" + result = _rapi_client.GetInstanceConsole(instance["name"]) + console = objects.InstanceConsole.FromDict(result) + AssertEqual(console.Validate(), True) + AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"])) + + +def TestRapiStoppedInstanceConsole(instance): + """Test getting stopped instance's console information via RAPI""" + try: + _rapi_client.GetInstanceConsole(instance["name"]) + except rapi.client.GanetiApiError, err: + AssertEqual(err.code, 503) + else: + raise qa_error.Error("Getting console for stopped instance didn't" + " return HTTP 503") + + def TestInterClusterInstanceMove(src_instance, dest_instance, pnode, snode, tnode): """Test tools/move-instance""" diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py index 3c672c2f6eb06e9530a227dd93fa2c171af255bf..33ba9a2327ab633009963700b5eec2db22459119 100755 --- a/test/ganeti.rapi.client_unittest.py +++ b/test/ganeti.rapi.client_unittest.py @@ -1123,6 +1123,15 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): self.assertHandler(rlib2.R_2_instances_name_deactivate_disks) self.assertFalse(self.rapi.GetLastHandler().queryargs) + def testGetInstanceConsole(self): + self.rapi.AddResponse("26876") + job_id = self.client.GetInstanceConsole("inst21491") + self.assertEqual(job_id, 26876) + self.assertItems(["inst21491"]) + self.assertHandler(rlib2.R_2_instances_name_console) + self.assertFalse(self.rapi.GetLastHandler().queryargs) + self.assertFalse(self.rapi.GetLastRequestData()) + def testGrowInstanceDisk(self): for idx, wait_for_sync in enumerate([None, False, True]): amount = 128 + (512 * idx) diff --git a/test/ganeti.rapi.rlib2_unittest.py b/test/ganeti.rapi.rlib2_unittest.py index c2f73796fd56c8f494f7e678ea9276cb5b837099..509df5adc09bac670ed68f6af50487751c2d07b3 100755 --- a/test/ganeti.rapi.rlib2_unittest.py +++ b/test/ganeti.rapi.rlib2_unittest.py @@ -31,12 +31,21 @@ from ganeti import constants from ganeti import opcodes from ganeti import compat from ganeti import http +from ganeti import query from ganeti.rapi import rlib2 import testutils +class TestConstants(unittest.TestCase): + def testConsole(self): + # Exporting the console field without authentication might expose + # information + assert "console" in query.INSTANCE_FIELDS + self.assertTrue("console" not in rlib2.I_FIELDS) + + class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase): def setUp(self): testutils.GanetiTestCase.setUp(self)