diff --git a/NEWS b/NEWS index 172277a512a2c029dc1a07ae0665084469e1411a..153ff35d2cb633b0d1babd5ae2c2abfd3282ccfa 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,11 @@ Version 2.5.0 beta1 - The default of the ``/2/instances/[instance_name]/rename`` RAPI resource's ``ip_check`` parameter changed from ``True`` to ``False`` to match the underlying LUXI interface +- The ``/2/nodes/[node_name]/evacuate`` RAPI resource was changed to use + body parameters, see :doc:`RAPI documentation <rapi>`. The server does + not maintain backwards-compatibility as the underlying operation + changed in an incompatible way. The RAPI client can talk to old + servers, but it needs to be told so as the return value changed. - When creating file-based instances via RAPI, the ``file_driver`` parameter no longer defaults to ``loop`` and must be specified - The deprecated "bridge" nic parameter is no longer supported. Use diff --git a/doc/rapi.rst b/doc/rapi.rst index fbc9970c3c9bda2d77efe1b79f15e7efad725652..e1ee709c91ec8fe4decdd1f45255520003986bb7 100644 --- a/doc/rapi.rst +++ b/doc/rapi.rst @@ -1161,32 +1161,24 @@ It supports the following commands: ``GET``. ``/2/nodes/[node_name]/evacuate`` +++++++++++++++++++++++++++++++++ -Evacuates all secondary instances off a node. +Evacuates instances off a node. It supports the following commands: ``POST``. ``POST`` ~~~~~~~~ -To evacuate a node, either one of the ``iallocator`` or ``remote_node`` -parameters must be passed:: +Returns a job ID. The result of the job will contain the IDs of the +individual jobs submitted to evacuate the node. - evacuate?iallocator=[iallocator] - evacuate?remote_node=[nodeX.example.com] - -The result value will be a list, each element being a triple of the job -id (for this specific evacuation), the instance which is being evacuated -by this job, and the node to which it is being relocated. In case the -node is already empty, the result will be an empty list (without any -jobs being submitted). +Body parameters: -And additional parameter ``early_release`` signifies whether to try to -parallelize the evacuations, at the risk of increasing I/O contention -and increasing the chances of data loss, if the primary node of any of -the instances being evacuated is not fully healthy. +.. opcode_params:: OP_NODE_EVACUATE + :exclude: nodes -If the dry-run parameter was specified, then the evacuation jobs were -not actually submitted, and the job IDs will be null. +Up to and including Ganeti 2.4 query arguments were used. Those are no +longer supported. The new request can be detected by the presence of the +:pyeval:`rlib2._NODE_EVAC_RES1` feature string. ``/2/nodes/[node_name]/migrate`` diff --git a/lib/rapi/client.py b/lib/rapi/client.py index d2aa4acc533588d3eb48b0a9df485983ea3ff5d5..d21952476f2433ee71af73b881a5b925fa9dbe70 100644 --- a/lib/rapi/client.py +++ b/lib/rapi/client.py @@ -93,6 +93,7 @@ _REQ_DATA_VERSION_FIELD = "__version__" _INST_CREATE_REQV1 = "instance-create-reqv1" _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" _NODE_MIGRATE_REQV1 = "node-migrate-reqv1" +_NODE_EVAC_RES1 = "node-evac-res1" _INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"]) _INST_CREATE_V0_DISK_PARAMS = frozenset(["size"]) _INST_CREATE_V0_PARAMS = frozenset([ @@ -1250,7 +1251,8 @@ class GanetiRapiClient(object): # pylint: disable-msg=R0904 None, None) def EvacuateNode(self, node, iallocator=None, remote_node=None, - dry_run=False, early_release=False): + dry_run=False, early_release=None, + primary=None, secondary=None, accept_old=False): """Evacuates instances from a Ganeti node. @type node: str @@ -1263,11 +1265,19 @@ class GanetiRapiClient(object): # pylint: disable-msg=R0904 @param dry_run: whether to perform a dry run @type early_release: bool @param early_release: whether to enable parallelization - - @rtype: list - @return: list of (job ID, instance name, new secondary node); if - dry_run was specified, then the actual move jobs were not - submitted and the job IDs will be C{None} + @type primary: bool + @param primary: Whether to evacuate primary instances + @type secondary: bool + @param secondary: Whether to evacuate secondary instances + @type accept_old: bool + @param accept_old: Whether caller is ready to accept old-style (pre-2.5) + results + + @rtype: string, or a list for pre-2.5 results + @return: Job ID or, if C{accept_old} is set and server is pre-2.5, + list of (job ID, instance name, new secondary node); if dry_run was + specified, then the actual move jobs were not submitted and the job IDs + will be C{None} @raises GanetiApiError: if an iallocator and remote_node are both specified @@ -1277,18 +1287,44 @@ class GanetiRapiClient(object): # pylint: disable-msg=R0904 raise GanetiApiError("Only one of iallocator or remote_node can be used") query = [] - if iallocator: - query.append(("iallocator", iallocator)) - if remote_node: - query.append(("remote_node", remote_node)) if dry_run: query.append(("dry-run", 1)) - if early_release: - query.append(("early_release", 1)) + + if _NODE_EVAC_RES1 in self.GetFeatures(): + body = {} + + if iallocator is not None: + body["iallocator"] = iallocator + if remote_node is not None: + body["remote_node"] = remote_node + if early_release is not None: + body["early_release"] = early_release + if primary is not None: + body["primary"] = primary + if secondary is not None: + body["secondary"] = secondary + else: + # Pre-2.5 request format + body = None + + if not accept_old: + raise GanetiApiError("Server is version 2.4 or earlier and caller does" + " not accept old-style results (parameter" + " accept_old)") + + if primary or primary is None or not (secondary is None or secondary): + raise GanetiApiError("Server can only evacuate secondary instances") + + if iallocator: + query.append(("iallocator", iallocator)) + if remote_node: + query.append(("remote_node", remote_node)) + if early_release: + query.append(("early_release", 1)) return self._SendRequest(HTTP_POST, ("/%s/nodes/%s/evacuate" % - (GANETI_RAPI_VERSION, node)), query, None) + (GANETI_RAPI_VERSION, node)), query, body) def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None, target_node=None): diff --git a/lib/rapi/rlib2.py b/lib/rapi/rlib2.py index d6bd9ce7641ae2b99a541e5d3b9468626feb7e0e..649417013ca45661e6a97d55c17e4e4ef1879bae 100644 --- a/lib/rapi/rlib2.py +++ b/lib/rapi/rlib2.py @@ -107,6 +107,9 @@ _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" # Feature string for node migration version 1 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1" +# Feature string for node evacuation with LU-generated jobs +_NODE_EVAC_RES1 = "node-evac-res1" + # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change. _WFJC_TIMEOUT = 10 @@ -148,7 +151,8 @@ class R_2_features(baserlib.R_Generic): """Returns list of optional RAPI features implemented. """ - return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1, _NODE_MIGRATE_REQV1] + return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1, _NODE_MIGRATE_REQV1, + _NODE_EVAC_RES1] class R_2_os(baserlib.R_Generic): @@ -414,38 +418,15 @@ class R_2_nodes_name_evacuate(baserlib.R_Generic): """ def POST(self): - """Evacuate all secondary instances off a node. + """Evacuate all instances off a node. """ - node_name = self.items[0] - remote_node = self._checkStringVariable("remote_node", default=None) - iallocator = self._checkStringVariable("iallocator", default=None) - early_r = bool(self._checkIntVariable("early_release", default=0)) - dry_run = bool(self.dryRun()) - - cl = baserlib.GetClient() - - op = opcodes.OpNodeEvacStrategy(nodes=[node_name], - iallocator=iallocator, - remote_node=remote_node) - - job_id = baserlib.SubmitJob([op], cl) - # we use custom feedback function, instead of print we log the status - result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn) + op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, { + "node_name": self.items[0], + "dry_run": self.dryRun(), + }) - jobs = [] - for iname, node in result[0]: - if dry_run: - jid = None - else: - op = opcodes.OpInstanceReplaceDisks(instance_name=iname, - remote_node=node, disks=[], - mode=constants.REPLACE_DISK_CHG, - early_release=early_r) - jid = baserlib.SubmitJob([op]) - jobs.append((jid, iname, node)) - - return jobs + return baserlib.SubmitJob([op]) class R_2_nodes_name_migrate(baserlib.R_Generic): diff --git a/test/ganeti.rapi.client_unittest.py b/test/ganeti.rapi.client_unittest.py index 23dacf8e0c8fbaff3237662cc6677dc40f9f2356..478747d46af2ba5dda2e89e40aac91054b37bdb9 100755 --- a/test/ganeti.rapi.client_unittest.py +++ b/test/ganeti.rapi.client_unittest.py @@ -152,6 +152,7 @@ class TestConstants(unittest.TestCase): self.assertEqual(client._INST_CREATE_REQV1, rlib2._INST_CREATE_REQV1) self.assertEqual(client._INST_REINSTALL_REQV1, rlib2._INST_REINSTALL_REQV1) self.assertEqual(client._NODE_MIGRATE_REQV1, rlib2._NODE_MIGRATE_REQV1) + self.assertEqual(client._NODE_EVAC_RES1, rlib2._NODE_EVAC_RES1) self.assertEqual(client._INST_NIC_PARAMS, constants.INIC_PARAMS) self.assertEqual(client.JOB_STATUS_QUEUED, constants.JOB_STATUS_QUEUED) self.assertEqual(client.JOB_STATUS_WAITLOCK, constants.JOB_STATUS_WAITLOCK) @@ -817,23 +818,63 @@ class GanetiRapiClientTests(testutils.GanetiTestCase): self.assertItems(["node-foo"]) def testEvacuateNode(self): + self.rapi.AddResponse(serializer.DumpJson([rlib2._NODE_EVAC_RES1])) self.rapi.AddResponse("9876") job_id = self.client.EvacuateNode("node-1", remote_node="node-2") self.assertEqual(9876, job_id) self.assertHandler(rlib2.R_2_nodes_name_evacuate) self.assertItems(["node-1"]) - self.assertQuery("remote_node", ["node-2"]) + self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()), + { "remote_node": "node-2", }) + self.assertEqual(self.rapi.CountPending(), 0) + self.rapi.AddResponse(serializer.DumpJson([rlib2._NODE_EVAC_RES1])) self.rapi.AddResponse("8888") job_id = self.client.EvacuateNode("node-3", iallocator="hail", dry_run=True) self.assertEqual(8888, job_id) self.assertItems(["node-3"]) - self.assertQuery("iallocator", ["hail"]) + self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()), + { "iallocator": "hail", }) self.assertDryRun() self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, "node-4", iallocator="hail", remote_node="node-5") + self.assertEqual(self.rapi.CountPending(), 0) + + def testEvacuateNodeOldResponse(self): + self.rapi.AddResponse(serializer.DumpJson([])) + self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, + "node-4", accept_old=False) + self.assertEqual(self.rapi.CountPending(), 0) + + self.rapi.AddResponse(serializer.DumpJson([])) + self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, + "node-4", accept_old=True) + self.assertEqual(self.rapi.CountPending(), 0) + + self.rapi.AddResponse(serializer.DumpJson([])) + self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, + "node-4", accept_old=True, primary=True) + self.assertEqual(self.rapi.CountPending(), 0) + + self.rapi.AddResponse(serializer.DumpJson([])) + self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, + "node-4", accept_old=True, secondary=False) + self.assertEqual(self.rapi.CountPending(), 0) + + for sec in [True, None]: + self.rapi.AddResponse(serializer.DumpJson([])) + self.rapi.AddResponse(serializer.DumpJson([["res", "foo"]])) + result = self.client.EvacuateNode("node-3", iallocator="hail", + dry_run=True, accept_old=True, + primary=False, secondary=sec) + self.assertEqual(result, [["res", "foo"]]) + self.assertItems(["node-3"]) + self.assertQuery("iallocator", ["hail"]) + self.assertFalse(self.rapi.GetLastRequestData()) + self.assertDryRun() + self.assertEqual(self.rapi.CountPending(), 0) def testMigrateNode(self): self.rapi.AddResponse(serializer.DumpJson([]))