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([]))