diff --git a/doc/rapi.rst b/doc/rapi.rst index df4a9ee792a397b81640f6c7ab6d3b475ee49434..0e82c3779f8f7a57c28d2d6e38caae3e9e280499 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 262e3893e9b77732c62d271f1bcbcfe090163b89..e1513aa9ac54d871a848dad3ded01d2e0b68b3e2 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 aa60a0a89a0b22195db54f2f0bc7206912bc442a..1cef5927ea422848bb5cb072ab22d62b25929c86 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 6626ae8434fdce8140b8ee8ef8f588a16e4a6e89..5425ed9628ba6493f86627cbd5ee0dac6ab6570f 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 c8214865a67a9ed03fed972de8ea323646366fa8..eb35adbf2577b5b2ba77cef1e38decc5ac7a3e6b 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 54fd5ff23eb5839027d35c5cbc7366ecedc4151c..10c23d00c5118ace57392e451a26f191d869e2f1 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 70f163e3b435ebcfe1f20b29880ca6d1c8e9a64c..d2cab632c353c6acf9c39fff1402f6e51b1ede7e 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()