From ebeb600f516bbde41f3f47cdbb5e203c03ab858b Mon Sep 17 00:00:00 2001
From: Michael Hanselmann <hansmi@google.com>
Date: Tue, 4 May 2010 17:38:21 +0200
Subject: [PATCH] RAPI changes for instance moves

Two new resources are added:
- /2/instances/$name/prepare-export
- /2/instances/$name/export

The documentation for the existing resource for creating instances is updated
for remote imports. The RAPI client is extended for the new resources.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>
---
 doc/rapi.rst                        | 47 ++++++++++++++++++++
 lib/rapi/client.py                  | 50 +++++++++++++++++++++
 lib/rapi/connector.py               |  4 ++
 lib/rapi/rlib2.py                   | 69 +++++++++++++++++++++++++++++
 qa/qa_rapi.py                       | 16 ++++++-
 test/ganeti.rapi.client_unittest.py | 20 +++++++++
 test/ganeti.rapi.rlib2_unittest.py  | 44 ++++++++++++++++++
 7 files changed, 249 insertions(+), 1 deletion(-)

diff --git a/doc/rapi.rst b/doc/rapi.rst
index df4a9ee79..0e82c3779 100644
--- a/doc/rapi.rst
+++ b/doc/rapi.rst
@@ -414,6 +414,12 @@ Body parameters:
   File storage driver.
 ``iallocator`` (string)
   Instance allocator name.
+``source_handshake``
+  Signed handshake from source (remote import only).
+``source_x509_ca`` (string)
+  Source X509 CA in PEM format (remote import only).
+``source_instance_name`` (string)
+  Source instance name (remote import only).
 ``hypervisor`` (string)
   Hypervisor name.
 ``hvparams`` (dict)
@@ -579,6 +585,47 @@ It supports the following commands: ``PUT``.
 Takes no parameters.
 
 
+``/2/instances/[instance_name]/prepare-export``
++++++++++++++++++++++++++++++++++++++++++++++++++
+
+Prepares an export of an instance.
+
+It supports the following commands: ``PUT``.
+
+``PUT``
+~~~~~~~
+
+Takes one parameter, ``mode``, for the export mode. Returns a job ID.
+
+
+``/2/instances/[instance_name]/export``
++++++++++++++++++++++++++++++++++++++++++++++++++
+
+Exports an instance.
+
+It supports the following commands: ``PUT``.
+
+``PUT``
+~~~~~~~
+
+Returns a job ID.
+
+Body parameters:
+
+``mode`` (string)
+  Export mode.
+``destination`` (required)
+  Destination information, depends on export mode.
+``shutdown`` (bool, required)
+  Whether to shutdown instance before export.
+``remove_instance`` (bool)
+  Whether to remove instance after export.
+``x509_key_name``
+  Name of X509 key (remote export only).
+``destination_x509_ca``
+  Destination X509 CA (remote export only).
+
+
 ``/2/instances/[instance_name]/tags``
 +++++++++++++++++++++++++++++++++++++
 
diff --git a/lib/rapi/client.py b/lib/rapi/client.py
index 262e3893e..e1513aa9a 100644
--- a/lib/rapi/client.py
+++ b/lib/rapi/client.py
@@ -840,6 +840,56 @@ class GanetiRapiClient(object):
                              ("/%s/instances/%s/replace-disks" %
                               (GANETI_RAPI_VERSION, instance)), query, None)
 
+  def PrepareExport(self, instance, mode):
+    """Prepares an instance for an export.
+
+    @type instance: string
+    @param instance: Instance name
+    @type mode: string
+    @param mode: Export mode
+    @rtype: string
+    @return: Job ID
+
+    """
+    query = [("mode", mode)]
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/prepare-export" %
+                              (GANETI_RAPI_VERSION, instance)), query, None)
+
+  def ExportInstance(self, instance, mode, destination, shutdown=None,
+                     remove_instance=None,
+                     x509_key_name=None, destination_x509_ca=None):
+    """Exports an instance.
+
+    @type instance: string
+    @param instance: Instance name
+    @type mode: string
+    @param mode: Export mode
+    @rtype: string
+    @return: Job ID
+
+    """
+    body = {
+      "destination": destination,
+      "mode": mode,
+      }
+
+    if shutdown is not None:
+      body["shutdown"] = shutdown
+
+    if remove_instance is not None:
+      body["remove_instance"] = remove_instance
+
+    if x509_key_name is not None:
+      body["x509_key_name"] = x509_key_name
+
+    if destination_x509_ca is not None:
+      body["destination_x509_ca"] = destination_x509_ca
+
+    return self._SendRequest(HTTP_PUT,
+                             ("/%s/instances/%s/export" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
   def GetJobs(self):
     """Gets all jobs for the cluster.
 
diff --git a/lib/rapi/connector.py b/lib/rapi/connector.py
index aa60a0a89..1cef5927e 100644
--- a/lib/rapi/connector.py
+++ b/lib/rapi/connector.py
@@ -205,6 +205,10 @@ def GetHandlers(node_name_pattern, instance_name_pattern, job_id_pattern):
       rlib2.R_2_instances_name_activate_disks,
     re.compile(r'^/2/instances/(%s)/deactivate-disks$' % instance_name_pattern):
       rlib2.R_2_instances_name_deactivate_disks,
+    re.compile(r'^/2/instances/(%s)/prepare-export$' % instance_name_pattern):
+      rlib2.R_2_instances_name_prepare_export,
+    re.compile(r'^/2/instances/(%s)/export$' % instance_name_pattern):
+      rlib2.R_2_instances_name_export,
 
     "/2/jobs": rlib2.R_2_jobs,
     re.compile(r'/2/jobs/(%s)$' % job_id_pattern):
diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py
index 6626ae843..5425ed962 100644
--- a/lib/rapi/rlib2.py
+++ b/lib/rapi/rlib2.py
@@ -578,6 +578,12 @@ def _ParseInstanceCreateRequestVersion1(data, dry_run):
                                              default=None),
     file_driver=baserlib.CheckParameter(data, "file_driver",
                                         default=constants.FD_LOOP),
+    source_handshake=baserlib.CheckParameter(data, "source_handshake",
+                                             default=None),
+    source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
+                                           default=None),
+    source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
+                                                 default=None),
     iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
     hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
     hvparams=hvparams,
@@ -891,6 +897,69 @@ class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
     return baserlib.SubmitJob([op])
 
 
+class R_2_instances_name_prepare_export(baserlib.R_Generic):
+  """/2/instances/[instance_name]/prepare-export resource.
+
+  """
+  def PUT(self):
+    """Prepares an export for an instance.
+
+    @return: a job id
+
+    """
+    instance_name = self.items[0]
+    mode = self._checkStringVariable("mode")
+
+    op = opcodes.OpPrepareExport(instance_name=instance_name,
+                                 mode=mode)
+
+    return baserlib.SubmitJob([op])
+
+
+def _ParseExportInstanceRequest(name, data):
+  """Parses a request for an instance export.
+
+  @rtype: L{opcodes.OpExportInstance}
+  @return: Instance export opcode
+
+  """
+  mode = baserlib.CheckParameter(data, "mode",
+                                 default=constants.EXPORT_MODE_LOCAL)
+  target_node = baserlib.CheckParameter(data, "destination")
+  shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
+  remove_instance = baserlib.CheckParameter(data, "remove_instance",
+                                            exptype=bool, default=False)
+  x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
+  destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
+                                                default=None)
+
+  return opcodes.OpExportInstance(instance_name=name,
+                                  mode=mode,
+                                  target_node=target_node,
+                                  shutdown=shutdown,
+                                  remove_instance=remove_instance,
+                                  x509_key_name=x509_key_name,
+                                  destination_x509_ca=destination_x509_ca)
+
+
+class R_2_instances_name_export(baserlib.R_Generic):
+  """/2/instances/[instance_name]/export resource.
+
+  """
+  def PUT(self):
+    """Exports an instance.
+
+    @return: a job id
+
+    """
+    if not isinstance(self.request_body, dict):
+      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
+
+    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
+
+    return baserlib.SubmitJob([op])
+
+
 class _R_Tags(baserlib.R_Generic):
   """ Quasiclass for tagging resources
 
diff --git a/qa/qa_rapi.py b/qa/qa_rapi.py
index c8214865a..eb35adbf2 100644
--- a/qa/qa_rapi.py
+++ b/qa/qa_rapi.py
@@ -198,6 +198,19 @@ def TestInstance(instance):
      _VerifyReturnsJob, 'PUT', None),
     ])
 
+  # Test OpPrepareExport
+  (job_id, ) = _DoTests([
+    ("/2/instances/%s/prepare-export?mode=%s" %
+     (instance["name"], constants.EXPORT_MODE_REMOTE),
+     _VerifyReturnsJob, "PUT", None),
+    ])
+
+  result = _WaitForRapiJob(job_id)[0]
+  AssertEqual(len(result["handshake"]), 3)
+  AssertEqual(result["handshake"][0], constants.RIE_VERSION)
+  AssertEqual(len(result["x509_key_name"]), 3)
+  AssertIn("-----BEGIN CERTIFICATE-----", result["x509_ca"])
+
 
 def TestNode(node):
   """Testing getting node(s) info via remote API.
@@ -259,7 +272,8 @@ def _WaitForRapiJob(job_id):
     ("/2/jobs/%s" % job_id, _VerifyJob, "GET", None),
     ])
 
-  rapi.client_utils.PollJob(_rapi_client, job_id, cli.StdioJobPollReportCb())
+  return rapi.client_utils.PollJob(_rapi_client, job_id,
+                                   cli.StdioJobPollReportCb())
 
 
 def TestRapiInstanceAdd(node, use_client):
diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py
index 54fd5ff23..10c23d00c 100755
--- a/test/ganeti.rapi.client_unittest.py
+++ b/test/ganeti.rapi.client_unittest.py
@@ -396,6 +396,26 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertItems(["instance-moo"])
     self.assertQuery("disks", None)
 
+  def testPrepareExport(self):
+    self.rapi.AddResponse("8326")
+    self.assertEqual(8326, self.client.PrepareExport("inst1", "local"))
+    self.assertHandler(rlib2.R_2_instances_name_prepare_export)
+    self.assertItems(["inst1"])
+    self.assertQuery("mode", ["local"])
+
+  def testExportInstance(self):
+    self.rapi.AddResponse("19695")
+    job_id = self.client.ExportInstance("inst2", "local", "nodeX",
+                                        shutdown=True)
+    self.assertEqual(job_id, 19695)
+    self.assertHandler(rlib2.R_2_instances_name_export)
+    self.assertItems(["inst2"])
+
+    data = serializer.LoadJson(self.http.last_request.data)
+    self.assertEqual(data["mode"], "local")
+    self.assertEqual(data["destination"], "nodeX")
+    self.assertEqual(data["shutdown"], True)
+
   def testGetJobs(self):
     self.rapi.AddResponse('[ { "id": "123", "uri": "\\/2\\/jobs\\/123" },'
                           '  { "id": "124", "uri": "\\/2\\/jobs\\/124" } ]')
diff --git a/test/ganeti.rapi.rlib2_unittest.py b/test/ganeti.rapi.rlib2_unittest.py
index 70f163e3b..d2cab632c 100755
--- a/test/ganeti.rapi.rlib2_unittest.py
+++ b/test/ganeti.rapi.rlib2_unittest.py
@@ -180,5 +180,49 @@ class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
         self.assertRaises(http.HttpBadRequest, self.Parse, data, False)
 
 
+class TestParseExportInstanceRequest(testutils.GanetiTestCase):
+  def setUp(self):
+    testutils.GanetiTestCase.setUp(self)
+
+    self.Parse = rlib2._ParseExportInstanceRequest
+
+  def test(self):
+    name = "instmoo"
+    data = {
+      "mode": constants.EXPORT_MODE_REMOTE,
+      "destination": [(1, 2, 3), (99, 99, 99)],
+      "shutdown": True,
+      "remove_instance": True,
+      "x509_key_name": ("name", "hash"),
+      "destination_x509_ca": ("x", "y", "z"),
+      }
+    op = self.Parse(name, data)
+    self.assert_(isinstance(op, opcodes.OpExportInstance))
+    self.assertEqual(op.instance_name, name)
+    self.assertEqual(op.mode, constants.EXPORT_MODE_REMOTE)
+    self.assertEqual(op.shutdown, True)
+    self.assertEqual(op.remove_instance, True)
+    self.assertEqualValues(op.x509_key_name, ("name", "hash"))
+    self.assertEqualValues(op.destination_x509_ca, ("x", "y", "z"))
+
+  def testDefaults(self):
+    name = "inst1"
+    data = {
+      "destination": "node2",
+      "shutdown": False,
+      }
+    op = self.Parse(name, data)
+    self.assert_(isinstance(op, opcodes.OpExportInstance))
+    self.assertEqual(op.instance_name, name)
+    self.assertEqual(op.mode, constants.EXPORT_MODE_LOCAL)
+    self.assertEqual(op.remove_instance, False)
+
+  def testErrors(self):
+    self.assertRaises(http.HttpBadRequest, self.Parse, "err1",
+                      { "remove_instance": "True", })
+    self.assertRaises(http.HttpBadRequest, self.Parse, "err1",
+                      { "remove_instance": "False", })
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
-- 
GitLab