Commit de40437a authored by Michael Hanselmann's avatar Michael Hanselmann
Browse files

Change RAPI for new node evacuation opcode



The change is not backwards compatible, see the updated NEWS file.
Signed-off-by: default avatarMichael Hanselmann <hansmi@google.com>
Reviewed-by: default avatarIustin Pop <iustin@google.com>
Reviewed-by: default avatarRené Nussbaumer <rn@google.com>
parent aafee533
......@@ -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
......
......@@ -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``
......
......@@ -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):
......
......@@ -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):
......
......@@ -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([]))
......
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