Commit ebeb600f authored by Michael Hanselmann's avatar Michael Hanselmann

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: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
parent 9bf56d77
......@@ -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``
+++++++++++++++++++++++++++++++++++++
......
......@@ -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.
......
......@@ -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):
......
......@@ -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
......
......@@ -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):
......
......@@ -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" } ]')
......
......@@ -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()
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment