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.
---
doc/rapi.rst | 47 ++++++++++++++++++++++++
lib/rapi/client.py | 50 +++++++++++++++++++++++++
lib/rapi/connector.py | 4 ++
lib/rapi/rlib2.py | 69 +++++++++++++++++++++++++++++++++++
qa/qa_rapi.py | 15 +++++++-
test/ganeti.rapi.client_unittest.py | 20 ++++++++++
test/ganeti.rapi.rlib2_unittest.py | 44 ++++++++++++++++++++++
7 files changed, 248 insertions(+), 1 deletions(-)
diff --git a/doc/rapi.rst b/doc/rapi.rst
index df4a9ee..0e82c37 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 262e389..e1513aa 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 aa60a0a..1cef592 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 6626ae8..5425ed9 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 c821486..b1a28a2 100644
--- a/qa/qa_rapi.py
+++ b/qa/qa_rapi.py
@@ -198,6 +198,18 @@ 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"]), 2)
+ 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 +271,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 54fd5ff..10c23d0 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 70f163e..d2cab63 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()
--
1.7.0.4