Revision de40437a
b/NEWS | ||
---|---|---|
9 | 9 |
- The default of the ``/2/instances/[instance_name]/rename`` RAPI |
10 | 10 |
resource's ``ip_check`` parameter changed from ``True`` to ``False`` |
11 | 11 |
to match the underlying LUXI interface |
12 |
- The ``/2/nodes/[node_name]/evacuate`` RAPI resource was changed to use |
|
13 |
body parameters, see :doc:`RAPI documentation <rapi>`. The server does |
|
14 |
not maintain backwards-compatibility as the underlying operation |
|
15 |
changed in an incompatible way. The RAPI client can talk to old |
|
16 |
servers, but it needs to be told so as the return value changed. |
|
12 | 17 |
- When creating file-based instances via RAPI, the ``file_driver`` |
13 | 18 |
parameter no longer defaults to ``loop`` and must be specified |
14 | 19 |
- The deprecated "bridge" nic parameter is no longer supported. Use |
b/doc/rapi.rst | ||
---|---|---|
1161 | 1161 |
``/2/nodes/[node_name]/evacuate`` |
1162 | 1162 |
+++++++++++++++++++++++++++++++++ |
1163 | 1163 |
|
1164 |
Evacuates all secondary instances off a node.
|
|
1164 |
Evacuates instances off a node. |
|
1165 | 1165 |
|
1166 | 1166 |
It supports the following commands: ``POST``. |
1167 | 1167 |
|
1168 | 1168 |
``POST`` |
1169 | 1169 |
~~~~~~~~ |
1170 | 1170 |
|
1171 |
To evacuate a node, either one of the ``iallocator`` or ``remote_node``
|
|
1172 |
parameters must be passed::
|
|
1171 |
Returns a job ID. The result of the job will contain the IDs of the
|
|
1172 |
individual jobs submitted to evacuate the node.
|
|
1173 | 1173 |
|
1174 |
evacuate?iallocator=[iallocator] |
|
1175 |
evacuate?remote_node=[nodeX.example.com] |
|
1176 |
|
|
1177 |
The result value will be a list, each element being a triple of the job |
|
1178 |
id (for this specific evacuation), the instance which is being evacuated |
|
1179 |
by this job, and the node to which it is being relocated. In case the |
|
1180 |
node is already empty, the result will be an empty list (without any |
|
1181 |
jobs being submitted). |
|
1174 |
Body parameters: |
|
1182 | 1175 |
|
1183 |
And additional parameter ``early_release`` signifies whether to try to |
|
1184 |
parallelize the evacuations, at the risk of increasing I/O contention |
|
1185 |
and increasing the chances of data loss, if the primary node of any of |
|
1186 |
the instances being evacuated is not fully healthy. |
|
1176 |
.. opcode_params:: OP_NODE_EVACUATE |
|
1177 |
:exclude: nodes |
|
1187 | 1178 |
|
1188 |
If the dry-run parameter was specified, then the evacuation jobs were |
|
1189 |
not actually submitted, and the job IDs will be null. |
|
1179 |
Up to and including Ganeti 2.4 query arguments were used. Those are no |
|
1180 |
longer supported. The new request can be detected by the presence of the |
|
1181 |
:pyeval:`rlib2._NODE_EVAC_RES1` feature string. |
|
1190 | 1182 |
|
1191 | 1183 |
|
1192 | 1184 |
``/2/nodes/[node_name]/migrate`` |
b/lib/rapi/client.py | ||
---|---|---|
93 | 93 |
_INST_CREATE_REQV1 = "instance-create-reqv1" |
94 | 94 |
_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" |
95 | 95 |
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1" |
96 |
_NODE_EVAC_RES1 = "node-evac-res1" |
|
96 | 97 |
_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"]) |
97 | 98 |
_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"]) |
98 | 99 |
_INST_CREATE_V0_PARAMS = frozenset([ |
... | ... | |
1250 | 1251 |
None, None) |
1251 | 1252 |
|
1252 | 1253 |
def EvacuateNode(self, node, iallocator=None, remote_node=None, |
1253 |
dry_run=False, early_release=False): |
|
1254 |
dry_run=False, early_release=None, |
|
1255 |
primary=None, secondary=None, accept_old=False): |
|
1254 | 1256 |
"""Evacuates instances from a Ganeti node. |
1255 | 1257 |
|
1256 | 1258 |
@type node: str |
... | ... | |
1263 | 1265 |
@param dry_run: whether to perform a dry run |
1264 | 1266 |
@type early_release: bool |
1265 | 1267 |
@param early_release: whether to enable parallelization |
1266 |
|
|
1267 |
@rtype: list |
|
1268 |
@return: list of (job ID, instance name, new secondary node); if |
|
1269 |
dry_run was specified, then the actual move jobs were not |
|
1270 |
submitted and the job IDs will be C{None} |
|
1268 |
@type primary: bool |
|
1269 |
@param primary: Whether to evacuate primary instances |
|
1270 |
@type secondary: bool |
|
1271 |
@param secondary: Whether to evacuate secondary instances |
|
1272 |
@type accept_old: bool |
|
1273 |
@param accept_old: Whether caller is ready to accept old-style (pre-2.5) |
|
1274 |
results |
|
1275 |
|
|
1276 |
@rtype: string, or a list for pre-2.5 results |
|
1277 |
@return: Job ID or, if C{accept_old} is set and server is pre-2.5, |
|
1278 |
list of (job ID, instance name, new secondary node); if dry_run was |
|
1279 |
specified, then the actual move jobs were not submitted and the job IDs |
|
1280 |
will be C{None} |
|
1271 | 1281 |
|
1272 | 1282 |
@raises GanetiApiError: if an iallocator and remote_node are both |
1273 | 1283 |
specified |
... | ... | |
1277 | 1287 |
raise GanetiApiError("Only one of iallocator or remote_node can be used") |
1278 | 1288 |
|
1279 | 1289 |
query = [] |
1280 |
if iallocator: |
|
1281 |
query.append(("iallocator", iallocator)) |
|
1282 |
if remote_node: |
|
1283 |
query.append(("remote_node", remote_node)) |
|
1284 | 1290 |
if dry_run: |
1285 | 1291 |
query.append(("dry-run", 1)) |
1286 |
if early_release: |
|
1287 |
query.append(("early_release", 1)) |
|
1292 |
|
|
1293 |
if _NODE_EVAC_RES1 in self.GetFeatures(): |
|
1294 |
body = {} |
|
1295 |
|
|
1296 |
if iallocator is not None: |
|
1297 |
body["iallocator"] = iallocator |
|
1298 |
if remote_node is not None: |
|
1299 |
body["remote_node"] = remote_node |
|
1300 |
if early_release is not None: |
|
1301 |
body["early_release"] = early_release |
|
1302 |
if primary is not None: |
|
1303 |
body["primary"] = primary |
|
1304 |
if secondary is not None: |
|
1305 |
body["secondary"] = secondary |
|
1306 |
else: |
|
1307 |
# Pre-2.5 request format |
|
1308 |
body = None |
|
1309 |
|
|
1310 |
if not accept_old: |
|
1311 |
raise GanetiApiError("Server is version 2.4 or earlier and caller does" |
|
1312 |
" not accept old-style results (parameter" |
|
1313 |
" accept_old)") |
|
1314 |
|
|
1315 |
if primary or primary is None or not (secondary is None or secondary): |
|
1316 |
raise GanetiApiError("Server can only evacuate secondary instances") |
|
1317 |
|
|
1318 |
if iallocator: |
|
1319 |
query.append(("iallocator", iallocator)) |
|
1320 |
if remote_node: |
|
1321 |
query.append(("remote_node", remote_node)) |
|
1322 |
if early_release: |
|
1323 |
query.append(("early_release", 1)) |
|
1288 | 1324 |
|
1289 | 1325 |
return self._SendRequest(HTTP_POST, |
1290 | 1326 |
("/%s/nodes/%s/evacuate" % |
1291 |
(GANETI_RAPI_VERSION, node)), query, None)
|
|
1327 |
(GANETI_RAPI_VERSION, node)), query, body)
|
|
1292 | 1328 |
|
1293 | 1329 |
def MigrateNode(self, node, mode=None, dry_run=False, iallocator=None, |
1294 | 1330 |
target_node=None): |
b/lib/rapi/rlib2.py | ||
---|---|---|
107 | 107 |
# Feature string for node migration version 1 |
108 | 108 |
_NODE_MIGRATE_REQV1 = "node-migrate-reqv1" |
109 | 109 |
|
110 |
# Feature string for node evacuation with LU-generated jobs |
|
111 |
_NODE_EVAC_RES1 = "node-evac-res1" |
|
112 |
|
|
110 | 113 |
# Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change. |
111 | 114 |
_WFJC_TIMEOUT = 10 |
112 | 115 |
|
... | ... | |
148 | 151 |
"""Returns list of optional RAPI features implemented. |
149 | 152 |
|
150 | 153 |
""" |
151 |
return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1, _NODE_MIGRATE_REQV1] |
|
154 |
return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1, _NODE_MIGRATE_REQV1, |
|
155 |
_NODE_EVAC_RES1] |
|
152 | 156 |
|
153 | 157 |
|
154 | 158 |
class R_2_os(baserlib.R_Generic): |
... | ... | |
414 | 418 |
|
415 | 419 |
""" |
416 | 420 |
def POST(self): |
417 |
"""Evacuate all secondary instances off a node.
|
|
421 |
"""Evacuate all instances off a node. |
|
418 | 422 |
|
419 | 423 |
""" |
420 |
node_name = self.items[0] |
|
421 |
remote_node = self._checkStringVariable("remote_node", default=None) |
|
422 |
iallocator = self._checkStringVariable("iallocator", default=None) |
|
423 |
early_r = bool(self._checkIntVariable("early_release", default=0)) |
|
424 |
dry_run = bool(self.dryRun()) |
|
425 |
|
|
426 |
cl = baserlib.GetClient() |
|
427 |
|
|
428 |
op = opcodes.OpNodeEvacStrategy(nodes=[node_name], |
|
429 |
iallocator=iallocator, |
|
430 |
remote_node=remote_node) |
|
431 |
|
|
432 |
job_id = baserlib.SubmitJob([op], cl) |
|
433 |
# we use custom feedback function, instead of print we log the status |
|
434 |
result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn) |
|
424 |
op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, { |
|
425 |
"node_name": self.items[0], |
|
426 |
"dry_run": self.dryRun(), |
|
427 |
}) |
|
435 | 428 |
|
436 |
jobs = [] |
|
437 |
for iname, node in result[0]: |
|
438 |
if dry_run: |
|
439 |
jid = None |
|
440 |
else: |
|
441 |
op = opcodes.OpInstanceReplaceDisks(instance_name=iname, |
|
442 |
remote_node=node, disks=[], |
|
443 |
mode=constants.REPLACE_DISK_CHG, |
|
444 |
early_release=early_r) |
|
445 |
jid = baserlib.SubmitJob([op]) |
|
446 |
jobs.append((jid, iname, node)) |
|
447 |
|
|
448 |
return jobs |
|
429 |
return baserlib.SubmitJob([op]) |
|
449 | 430 |
|
450 | 431 |
|
451 | 432 |
class R_2_nodes_name_migrate(baserlib.R_Generic): |
b/test/ganeti.rapi.client_unittest.py | ||
---|---|---|
152 | 152 |
self.assertEqual(client._INST_CREATE_REQV1, rlib2._INST_CREATE_REQV1) |
153 | 153 |
self.assertEqual(client._INST_REINSTALL_REQV1, rlib2._INST_REINSTALL_REQV1) |
154 | 154 |
self.assertEqual(client._NODE_MIGRATE_REQV1, rlib2._NODE_MIGRATE_REQV1) |
155 |
self.assertEqual(client._NODE_EVAC_RES1, rlib2._NODE_EVAC_RES1) |
|
155 | 156 |
self.assertEqual(client._INST_NIC_PARAMS, constants.INIC_PARAMS) |
156 | 157 |
self.assertEqual(client.JOB_STATUS_QUEUED, constants.JOB_STATUS_QUEUED) |
157 | 158 |
self.assertEqual(client.JOB_STATUS_WAITLOCK, constants.JOB_STATUS_WAITLOCK) |
... | ... | |
817 | 818 |
self.assertItems(["node-foo"]) |
818 | 819 |
|
819 | 820 |
def testEvacuateNode(self): |
821 |
self.rapi.AddResponse(serializer.DumpJson([rlib2._NODE_EVAC_RES1])) |
|
820 | 822 |
self.rapi.AddResponse("9876") |
821 | 823 |
job_id = self.client.EvacuateNode("node-1", remote_node="node-2") |
822 | 824 |
self.assertEqual(9876, job_id) |
823 | 825 |
self.assertHandler(rlib2.R_2_nodes_name_evacuate) |
824 | 826 |
self.assertItems(["node-1"]) |
825 |
self.assertQuery("remote_node", ["node-2"]) |
|
827 |
self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()), |
|
828 |
{ "remote_node": "node-2", }) |
|
829 |
self.assertEqual(self.rapi.CountPending(), 0) |
|
826 | 830 |
|
831 |
self.rapi.AddResponse(serializer.DumpJson([rlib2._NODE_EVAC_RES1])) |
|
827 | 832 |
self.rapi.AddResponse("8888") |
828 | 833 |
job_id = self.client.EvacuateNode("node-3", iallocator="hail", dry_run=True) |
829 | 834 |
self.assertEqual(8888, job_id) |
830 | 835 |
self.assertItems(["node-3"]) |
831 |
self.assertQuery("iallocator", ["hail"]) |
|
836 |
self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()), |
|
837 |
{ "iallocator": "hail", }) |
|
832 | 838 |
self.assertDryRun() |
833 | 839 |
|
834 | 840 |
self.assertRaises(client.GanetiApiError, |
835 | 841 |
self.client.EvacuateNode, |
836 | 842 |
"node-4", iallocator="hail", remote_node="node-5") |
843 |
self.assertEqual(self.rapi.CountPending(), 0) |
|
844 |
|
|
845 |
def testEvacuateNodeOldResponse(self): |
|
846 |
self.rapi.AddResponse(serializer.DumpJson([])) |
|
847 |
self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, |
|
848 |
"node-4", accept_old=False) |
|
849 |
self.assertEqual(self.rapi.CountPending(), 0) |
|
850 |
|
|
851 |
self.rapi.AddResponse(serializer.DumpJson([])) |
|
852 |
self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, |
|
853 |
"node-4", accept_old=True) |
|
854 |
self.assertEqual(self.rapi.CountPending(), 0) |
|
855 |
|
|
856 |
self.rapi.AddResponse(serializer.DumpJson([])) |
|
857 |
self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, |
|
858 |
"node-4", accept_old=True, primary=True) |
|
859 |
self.assertEqual(self.rapi.CountPending(), 0) |
|
860 |
|
|
861 |
self.rapi.AddResponse(serializer.DumpJson([])) |
|
862 |
self.assertRaises(client.GanetiApiError, self.client.EvacuateNode, |
|
863 |
"node-4", accept_old=True, secondary=False) |
|
864 |
self.assertEqual(self.rapi.CountPending(), 0) |
|
865 |
|
|
866 |
for sec in [True, None]: |
|
867 |
self.rapi.AddResponse(serializer.DumpJson([])) |
|
868 |
self.rapi.AddResponse(serializer.DumpJson([["res", "foo"]])) |
|
869 |
result = self.client.EvacuateNode("node-3", iallocator="hail", |
|
870 |
dry_run=True, accept_old=True, |
|
871 |
primary=False, secondary=sec) |
|
872 |
self.assertEqual(result, [["res", "foo"]]) |
|
873 |
self.assertItems(["node-3"]) |
|
874 |
self.assertQuery("iallocator", ["hail"]) |
|
875 |
self.assertFalse(self.rapi.GetLastRequestData()) |
|
876 |
self.assertDryRun() |
|
877 |
self.assertEqual(self.rapi.CountPending(), 0) |
|
837 | 878 |
|
838 | 879 |
def testMigrateNode(self): |
839 | 880 |
self.rapi.AddResponse(serializer.DumpJson([])) |
Also available in: Unified diff