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