From b82d4c5e4e5751f12b61a71b16592fb20b8f822e Mon Sep 17 00:00:00 2001
From: Michael Hanselmann <hansmi@google.com>
Date: Fri, 28 Jan 2011 15:21:04 +0100
Subject: [PATCH] Add RAPI resource for instance console

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>
---
 doc/rapi.rst                        | 31 +++++++++++++++++++++++++++++
 lib/rapi/client.py                  | 11 ++++++++++
 lib/rapi/connector.py               |  2 ++
 lib/rapi/rlib2.py                   | 24 ++++++++++++++++++++++
 qa/ganeti-qa.py                     |  4 ++++
 qa/qa_rapi.py                       | 20 +++++++++++++++++++
 test/ganeti.rapi.client_unittest.py |  9 +++++++++
 test/ganeti.rapi.rlib2_unittest.py  |  9 +++++++++
 8 files changed, 110 insertions(+)

diff --git a/doc/rapi.rst b/doc/rapi.rst
index 9b3bfad20..1a45c8cf4 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 9e076e80f..cb437849d 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 fceda6dee..b6c66f66f 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 1f95bcb22..a3bf4cd3f 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 d4c363474..78181a3a5 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 b3edeb4e3..d8679f9d2 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 3c672c2f6..33ba9a232 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 c2f73796f..509df5adc 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)
-- 
GitLab