Merge branch 'stable-2.5' into devel-2.5
[ganeti-local] / test / ganeti.rapi.client_unittest.py
index 9b432b9..1067656 100755 (executable)
@@ -30,6 +30,9 @@ import pycurl
 from ganeti import constants
 from ganeti import http
 from ganeti import serializer
+from ganeti import utils
+from ganeti import query
+from ganeti import objects
 
 from ganeti.rapi import connector
 from ganeti.rapi import rlib2
@@ -40,6 +43,15 @@ import testutils
 
 _URI_RE = re.compile(r"https://(?P<host>.*):(?P<port>\d+)(?P<path>/.*)")
 
+# List of resource handlers which aren't used by the RAPI client
+_KNOWN_UNUSED = set([
+  connector.R_root,
+  connector.R_2,
+  ])
+
+# Global variable for collecting used handlers
+_used_handlers = None
+
 
 def _GetPathFromUri(uri):
   """Gets the path and query from a URI.
@@ -91,6 +103,9 @@ class RapiMock(object):
     self._last_handler = None
     self._last_req_data = None
 
+  def ResetResponses(self):
+    del self._responses[:]
+
   def AddResponse(self, response, code=200):
     self._responses.insert(0, (code, response))
 
@@ -107,8 +122,12 @@ class RapiMock(object):
     self._last_req_data = request_body
 
     try:
-      HandlerClass, items, args = self._mapper.getController(path)
-      self._last_handler = HandlerClass(items, args, None)
+      (handler_cls, items, args) = self._mapper.getController(path)
+
+      # Record handler as used
+      _used_handlers.add(handler_cls)
+
+      self._last_handler = handler_cls(items, args, None)
       if not hasattr(self._last_handler, method.upper()):
         raise http.HttpNotImplemented(message="Method not implemented")
 
@@ -132,7 +151,27 @@ class TestConstants(unittest.TestCase):
     self.assertEqual(client._REQ_DATA_VERSION_FIELD, rlib2._REQ_DATA_VERSION)
     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_WAITING, constants.JOB_STATUS_WAITING)
+    self.assertEqual(client.JOB_STATUS_CANCELING,
+                     constants.JOB_STATUS_CANCELING)
+    self.assertEqual(client.JOB_STATUS_RUNNING, constants.JOB_STATUS_RUNNING)
+    self.assertEqual(client.JOB_STATUS_CANCELED, constants.JOB_STATUS_CANCELED)
+    self.assertEqual(client.JOB_STATUS_SUCCESS, constants.JOB_STATUS_SUCCESS)
+    self.assertEqual(client.JOB_STATUS_ERROR, constants.JOB_STATUS_ERROR)
+    self.assertEqual(client.JOB_STATUS_FINALIZED, constants.JOBS_FINALIZED)
+    self.assertEqual(client.JOB_STATUS_ALL, constants.JOB_STATUS_ALL)
+
+    # Node evacuation
+    self.assertEqual(client.NODE_EVAC_PRI, constants.IALLOCATOR_NEVAC_PRI)
+    self.assertEqual(client.NODE_EVAC_SEC, constants.IALLOCATOR_NEVAC_SEC)
+    self.assertEqual(client.NODE_EVAC_ALL, constants.IALLOCATOR_NEVAC_ALL)
+
+    # Legacy name
+    self.assertEqual(client.JOB_STATUS_WAITLOCK, constants.JOB_STATUS_WAITING)
 
 
 class RapiMockTest(unittest.TestCase):
@@ -351,6 +390,9 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
   def assertDryRun(self):
     self.assertTrue(self.rapi.GetLastHandler().dryRun())
 
+  def assertUseForce(self):
+    self.assertTrue(self.rapi.GetLastHandler().useForce())
+
   def testEncodeQuery(self):
     query = [
       ("a", None),
@@ -478,92 +520,12 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertQuery("static", ["1"])
 
   def testCreateInstanceOldVersion(self):
-    # No NICs
+    # The old request format, version 0, is no longer supported
     self.rapi.AddResponse(None, code=404)
     self.assertRaises(client.GanetiApiError, self.client.CreateInstance,
                       "create", "inst1.example.com", "plain", [], [])
     self.assertEqual(self.rapi.CountPending(), 0)
 
-    # More than one NIC
-    self.rapi.AddResponse(None, code=404)
-    self.assertRaises(client.GanetiApiError, self.client.CreateInstance,
-                      "create", "inst1.example.com", "plain", [],
-                      [{}, {}, {}])
-    self.assertEqual(self.rapi.CountPending(), 0)
-
-    # Unsupported NIC fields
-    self.rapi.AddResponse(None, code=404)
-    self.assertRaises(client.GanetiApiError, self.client.CreateInstance,
-                      "create", "inst1.example.com", "plain", [],
-                      [{"x": True, "y": False}])
-    self.assertEqual(self.rapi.CountPending(), 0)
-
-    # Unsupported disk fields
-    self.rapi.AddResponse(None, code=404)
-    self.assertRaises(client.GanetiApiError, self.client.CreateInstance,
-                      "create", "inst1.example.com", "plain",
-                      [{}, {"moo": "foo",}], [{}])
-    self.assertEqual(self.rapi.CountPending(), 0)
-
-    # Unsupported fields
-    self.rapi.AddResponse(None, code=404)
-    self.assertRaises(client.GanetiApiError, self.client.CreateInstance,
-                      "create", "inst1.example.com", "plain", [], [{}],
-                      hello_world=123)
-    self.assertEqual(self.rapi.CountPending(), 0)
-
-    self.rapi.AddResponse(None, code=404)
-    self.assertRaises(client.GanetiApiError, self.client.CreateInstance,
-                      "create", "inst1.example.com", "plain", [], [{}],
-                      memory=128)
-    self.assertEqual(self.rapi.CountPending(), 0)
-
-    # Normal creation
-    testnics = [
-      [{}],
-      [{ "mac": constants.VALUE_AUTO, }],
-      [{ "ip": "192.0.2.99", "mode": constants.NIC_MODE_ROUTED, }],
-      ]
-
-    testdisks = [
-      [],
-      [{ "size": 128, }],
-      [{ "size": 321, }, { "size": 4096, }],
-      ]
-
-    for idx, nics in enumerate(testnics):
-      for disks in testdisks:
-        beparams = {
-          constants.BE_MEMORY: 512,
-          constants.BE_AUTO_BALANCE: False,
-          }
-        hvparams = {
-          constants.HV_MIGRATION_PORT: 9876,
-          constants.HV_VNC_TLS: True,
-          }
-
-        self.rapi.AddResponse(None, code=404)
-        self.rapi.AddResponse(serializer.DumpJson(3122617 + idx))
-        job_id = self.client.CreateInstance("create", "inst1.example.com",
-                                            "plain", disks, nics,
-                                            pnode="node99", dry_run=True,
-                                            hvparams=hvparams,
-                                            beparams=beparams)
-        self.assertEqual(job_id, 3122617 + idx)
-        self.assertHandler(rlib2.R_2_instances)
-        self.assertDryRun()
-        self.assertEqual(self.rapi.CountPending(), 0)
-
-        data = serializer.LoadJson(self.rapi.GetLastRequestData())
-        self.assertEqual(data["name"], "inst1.example.com")
-        self.assertEqual(data["disk_template"], "plain")
-        self.assertEqual(data["pnode"], "node99")
-        self.assertEqual(data[constants.BE_MEMORY], 512)
-        self.assertEqual(data[constants.BE_AUTO_BALANCE], False)
-        self.assertEqual(data[constants.HV_MIGRATION_PORT], 9876)
-        self.assertEqual(data[constants.HV_VNC_TLS], True)
-        self.assertEqual(data["disks"], [disk["size"] for disk in disks])
-
   def testCreateInstance(self):
     self.rapi.AddResponse(serializer.DumpJson([rlib2._INST_CREATE_REQV1]))
     self.rapi.AddResponse("23030")
@@ -712,24 +674,21 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
   def testReplaceInstanceDisks(self):
     self.rapi.AddResponse("999")
     job_id = self.client.ReplaceInstanceDisks("instance-name",
-        disks=[0, 1], dry_run=True, iallocator="hail")
+        disks=[0, 1], iallocator="hail")
     self.assertEqual(999, job_id)
     self.assertHandler(rlib2.R_2_instances_name_replace_disks)
     self.assertItems(["instance-name"])
     self.assertQuery("disks", ["0,1"])
     self.assertQuery("mode", ["replace_auto"])
     self.assertQuery("iallocator", ["hail"])
-    self.assertDryRun()
 
     self.rapi.AddResponse("1000")
     job_id = self.client.ReplaceInstanceDisks("instance-bar",
-        disks=[1], mode="replace_on_secondary", remote_node="foo-node",
-        dry_run=True)
+        disks=[1], mode="replace_on_secondary", remote_node="foo-node")
     self.assertEqual(1000, job_id)
     self.assertItems(["instance-bar"])
     self.assertQuery("disks", ["1"])
     self.assertQuery("remote_node", ["foo-node"])
-    self.assertDryRun()
 
     self.rapi.AddResponse("5175")
     self.assertEqual(5175, self.client.ReplaceInstanceDisks("instance-moo"))
@@ -781,6 +740,36 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
         self.assertEqual(data["mode"], mode)
         self.assertEqual(data["cleanup"], cleanup)
 
+  def testFailoverInstanceDefaults(self):
+    self.rapi.AddResponse("7639")
+    job_id = self.client.FailoverInstance("inst13579")
+    self.assertEqual(job_id, 7639)
+    self.assertHandler(rlib2.R_2_instances_name_failover)
+    self.assertItems(["inst13579"])
+
+    data = serializer.LoadJson(self.rapi.GetLastRequestData())
+    self.assertFalse(data)
+
+  def testFailoverInstance(self):
+    for iallocator in ["dumb", "hail"]:
+      for ignore_consistency in [False, True]:
+        for target_node in ["node-a", "node2"]:
+          self.rapi.AddResponse("19161")
+          job_id = \
+            self.client.FailoverInstance("inst251", iallocator=iallocator,
+                                         ignore_consistency=ignore_consistency,
+                                         target_node=target_node)
+          self.assertEqual(job_id, 19161)
+          self.assertHandler(rlib2.R_2_instances_name_failover)
+          self.assertItems(["inst251"])
+
+          data = serializer.LoadJson(self.rapi.GetLastRequestData())
+          self.assertEqual(len(data), 3)
+          self.assertEqual(data["iallocator"], iallocator)
+          self.assertEqual(data["ignore_consistency"], ignore_consistency)
+          self.assertEqual(data["target_node"], target_node)
+          self.assertEqual(self.rapi.CountPending(), 0)
+
   def testRenameInstanceDefaults(self):
     new_name = "newnametha7euqu"
     self.rapi.AddResponse("8791")
@@ -864,32 +853,72 @@ 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)
+    job_id = self.client.EvacuateNode("node-3", iallocator="hail", dry_run=True,
+                                      mode=constants.IALLOCATOR_NEVAC_ALL,
+                                      early_release=True)
     self.assertEqual(8888, job_id)
     self.assertItems(["node-3"])
-    self.assertQuery("iallocator", ["hail"])
+    self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()), {
+      "iallocator": "hail",
+      "mode": "all",
+      "early_release": True,
+      })
     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)
+
+    for mode in [client.NODE_EVAC_PRI, client.NODE_EVAC_ALL]:
+      self.rapi.AddResponse(serializer.DumpJson([]))
+      self.assertRaises(client.GanetiApiError, self.client.EvacuateNode,
+                        "node-4", accept_old=True, mode=mode)
+      self.assertEqual(self.rapi.CountPending(), 0)
+
+    self.rapi.AddResponse(serializer.DumpJson([]))
+    self.rapi.AddResponse(serializer.DumpJson("21533"))
+    result = self.client.EvacuateNode("node-3", iallocator="hail",
+                                      dry_run=True, accept_old=True,
+                                      mode=client.NODE_EVAC_SEC,
+                                      early_release=True)
+    self.assertEqual(result, "21533")
+    self.assertItems(["node-3"])
+    self.assertQuery("iallocator", ["hail"])
+    self.assertQuery("early_release", ["1"])
+    self.assertFalse(self.rapi.GetLastRequestData())
+    self.assertDryRun()
+    self.assertEqual(self.rapi.CountPending(), 0)
 
   def testMigrateNode(self):
+    self.rapi.AddResponse(serializer.DumpJson([]))
     self.rapi.AddResponse("1111")
     self.assertEqual(1111, self.client.MigrateNode("node-a", dry_run=True))
     self.assertHandler(rlib2.R_2_nodes_name_migrate)
     self.assertItems(["node-a"])
     self.assert_("mode" not in self.rapi.GetLastHandler().queryargs)
     self.assertDryRun()
+    self.assertFalse(self.rapi.GetLastRequestData())
 
+    self.rapi.AddResponse(serializer.DumpJson([]))
     self.rapi.AddResponse("1112")
     self.assertEqual(1112, self.client.MigrateNode("node-a", dry_run=True,
                                                    mode="live"))
@@ -897,6 +926,36 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertItems(["node-a"])
     self.assertQuery("mode", ["live"])
     self.assertDryRun()
+    self.assertFalse(self.rapi.GetLastRequestData())
+
+    self.rapi.AddResponse(serializer.DumpJson([]))
+    self.assertRaises(client.GanetiApiError, self.client.MigrateNode,
+                      "node-c", target_node="foonode")
+    self.assertEqual(self.rapi.CountPending(), 0)
+
+  def testMigrateNodeBodyData(self):
+    self.rapi.AddResponse(serializer.DumpJson([rlib2._NODE_MIGRATE_REQV1]))
+    self.rapi.AddResponse("27539")
+    self.assertEqual(27539, self.client.MigrateNode("node-a", dry_run=False,
+                                                    mode="live"))
+    self.assertHandler(rlib2.R_2_nodes_name_migrate)
+    self.assertItems(["node-a"])
+    self.assertFalse(self.rapi.GetLastHandler().queryargs)
+    self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()),
+                     { "mode": "live", })
+
+    self.rapi.AddResponse(serializer.DumpJson([rlib2._NODE_MIGRATE_REQV1]))
+    self.rapi.AddResponse("14219")
+    self.assertEqual(14219, self.client.MigrateNode("node-x", dry_run=True,
+                                                    target_node="node9",
+                                                    iallocator="ial"))
+    self.assertHandler(rlib2.R_2_nodes_name_migrate)
+    self.assertItems(["node-x"])
+    self.assertDryRun()
+    self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()),
+                     { "target_node": "node9", "iallocator": "ial", })
+
+    self.assertEqual(self.rapi.CountPending(), 0)
 
   def testGetNodeRole(self):
     self.rapi.AddResponse("\"master\"")
@@ -913,6 +972,14 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertQuery("force", ["1"])
     self.assertEqual("\"master-candidate\"", self.rapi.GetLastRequestData())
 
+  def testModifyNode(self):
+    self.rapi.AddResponse("3783")
+    job_id = self.client.ModifyNode("node16979.example.com", drained=True)
+    self.assertEqual(job_id, 3783)
+    self.assertHandler(rlib2.R_2_nodes_name_modify)
+    self.assertItems(["node16979.example.com"])
+    self.assertEqual(self.rapi.CountPending(), 0)
+
   def testGetNodeStorageUnits(self):
     self.rapi.AddResponse("42")
     self.assertEqual(42,
@@ -1045,6 +1112,24 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertEqual(job_id, 12348)
     self.assertHandler(rlib2.R_2_groups_name_modify)
 
+  def testAssignGroupNodes(self):
+    self.rapi.AddResponse("12349")
+    job_id = self.client.AssignGroupNodes("mygroup", ["node1", "node2"],
+                                          force=True, dry_run=True)
+    self.assertEqual(job_id, 12349)
+    self.assertHandler(rlib2.R_2_groups_name_assign_nodes)
+    self.assertDryRun()
+    self.assertUseForce()
+
+  def testModifyInstance(self):
+    self.rapi.AddResponse("23681")
+    job_id = self.client.ModifyInstance("inst7210", os_name="linux")
+    self.assertEqual(job_id, 23681)
+    self.assertItems(["inst7210"])
+    self.assertHandler(rlib2.R_2_instances_name_modify)
+    self.assertEqual(serializer.LoadJson(self.rapi.GetLastRequestData()),
+                     { "os_name": "linux", })
+
   def testModifyCluster(self):
     for mnh in [None, False, True]:
       self.rapi.AddResponse("14470")
@@ -1057,6 +1142,46 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
       self.assertEqual(data["maintain_node_health"], mnh)
       self.assertEqual(self.rapi.CountPending(), 0)
 
+  def testRedistributeConfig(self):
+    self.rapi.AddResponse("3364")
+    job_id = self.client.RedistributeConfig()
+    self.assertEqual(job_id, 3364)
+    self.assertItems([])
+    self.assertHandler(rlib2.R_2_redist_config)
+
+  def testActivateInstanceDisks(self):
+    self.rapi.AddResponse("23547")
+    job_id = self.client.ActivateInstanceDisks("inst28204")
+    self.assertEqual(job_id, 23547)
+    self.assertItems(["inst28204"])
+    self.assertHandler(rlib2.R_2_instances_name_activate_disks)
+    self.assertFalse(self.rapi.GetLastHandler().queryargs)
+
+  def testActivateInstanceDisksIgnoreSize(self):
+    self.rapi.AddResponse("11044")
+    job_id = self.client.ActivateInstanceDisks("inst28204", ignore_size=True)
+    self.assertEqual(job_id, 11044)
+    self.assertItems(["inst28204"])
+    self.assertHandler(rlib2.R_2_instances_name_activate_disks)
+    self.assertQuery("ignore_size", ["1"])
+
+  def testDeactivateInstanceDisks(self):
+    self.rapi.AddResponse("14591")
+    job_id = self.client.DeactivateInstanceDisks("inst28234")
+    self.assertEqual(job_id, 14591)
+    self.assertItems(["inst28234"])
+    self.assertHandler(rlib2.R_2_instances_name_deactivate_disks)
+    self.assertFalse(self.rapi.GetLastHandler().queryargs)
+
+  def testGetInstanceConsole(self):
+    self.rapi.AddResponse("26876")
+    job_id = self.client.GetInstanceConsole("inst21491")
+    self.assertEqual(job_id, 26876)
+    self.assertItems(["inst21491"])
+    self.assertHandler(rlib2.R_2_instances_name_console)
+    self.assertFalse(self.rapi.GetLastHandler().queryargs)
+    self.assertFalse(self.rapi.GetLastRequestData())
+
   def testGrowInstanceDisk(self):
     for idx, wait_for_sync in enumerate([None, False, True]):
       amount = 128 + (512 * idx)
@@ -1077,6 +1202,174 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
       self.assertEqual(data["amount"], amount)
       self.assertEqual(self.rapi.CountPending(), 0)
 
+  def testGetGroupTags(self):
+    self.rapi.AddResponse("[]")
+    self.assertEqual([], self.client.GetGroupTags("fooGroup"))
+    self.assertHandler(rlib2.R_2_groups_name_tags)
+    self.assertItems(["fooGroup"])
+
+  def testAddGroupTags(self):
+    self.rapi.AddResponse("1234")
+    self.assertEqual(1234,
+        self.client.AddGroupTags("fooGroup", ["awesome"], dry_run=True))
+    self.assertHandler(rlib2.R_2_groups_name_tags)
+    self.assertItems(["fooGroup"])
+    self.assertDryRun()
+    self.assertQuery("tag", ["awesome"])
+
+  def testDeleteGroupTags(self):
+    self.rapi.AddResponse("25826")
+    self.assertEqual(25826, self.client.DeleteGroupTags("foo", ["awesome"],
+                                                        dry_run=True))
+    self.assertHandler(rlib2.R_2_groups_name_tags)
+    self.assertItems(["foo"])
+    self.assertDryRun()
+    self.assertQuery("tag", ["awesome"])
+
+  def testQuery(self):
+    for idx, what in enumerate(constants.QR_VIA_RAPI):
+      for idx2, filter_ in enumerate([None, ["?", "name"]]):
+        job_id = 11010 + (idx << 4) + (idx2 << 16)
+        fields = sorted(query.ALL_FIELDS[what].keys())[:10]
+
+        self.rapi.AddResponse(str(job_id))
+        self.assertEqual(self.client.Query(what, fields, filter_=filter_),
+                         job_id)
+        self.assertItems([what])
+        self.assertHandler(rlib2.R_2_query)
+        self.assertFalse(self.rapi.GetLastHandler().queryargs)
+        data = serializer.LoadJson(self.rapi.GetLastRequestData())
+        self.assertEqual(data["fields"], fields)
+        if filter_ is None:
+          self.assertTrue("filter" not in data)
+        else:
+          self.assertEqual(data["filter"], filter_)
+        self.assertEqual(self.rapi.CountPending(), 0)
+
+  def testQueryFields(self):
+    exp_result = objects.QueryFieldsResponse(fields=[
+      objects.QueryFieldDefinition(name="pnode", title="PNode",
+                                   kind=constants.QFT_NUMBER),
+      objects.QueryFieldDefinition(name="other", title="Other",
+                                   kind=constants.QFT_BOOL),
+      ])
+
+    for what in constants.QR_VIA_RAPI:
+      for fields in [None, ["name", "_unknown_"], ["&", "?|"]]:
+        self.rapi.AddResponse(serializer.DumpJson(exp_result.ToDict()))
+        result = self.client.QueryFields(what, fields=fields)
+        self.assertItems([what])
+        self.assertHandler(rlib2.R_2_query_fields)
+        self.assertFalse(self.rapi.GetLastRequestData())
+
+        queryargs = self.rapi.GetLastHandler().queryargs
+        if fields is None:
+          self.assertFalse(queryargs)
+        else:
+          self.assertEqual(queryargs, {
+            "fields": [",".join(fields)],
+            })
+
+        self.assertEqual(objects.QueryFieldsResponse.FromDict(result).ToDict(),
+                         exp_result.ToDict())
+
+        self.assertEqual(self.rapi.CountPending(), 0)
+
+  def testWaitForJobCompletionNoChange(self):
+    resp = serializer.DumpJson({
+      "status": constants.JOB_STATUS_WAITING,
+      })
+
+    for retries in [1, 5, 25]:
+      for _ in range(retries):
+        self.rapi.AddResponse(resp)
+
+      self.assertFalse(self.client.WaitForJobCompletion(22789, period=None,
+                                                        retries=retries))
+      self.assertHandler(rlib2.R_2_jobs_id)
+      self.assertItems(["22789"])
+
+      self.assertEqual(self.rapi.CountPending(), 0)
+
+  def testWaitForJobCompletionAlreadyFinished(self):
+    self.rapi.AddResponse(serializer.DumpJson({
+      "status": constants.JOB_STATUS_SUCCESS,
+      }))
+
+    self.assertTrue(self.client.WaitForJobCompletion(22793, period=None,
+                                                     retries=1))
+    self.assertHandler(rlib2.R_2_jobs_id)
+    self.assertItems(["22793"])
+
+    self.assertEqual(self.rapi.CountPending(), 0)
+
+  def testWaitForJobCompletionEmptyResponse(self):
+    self.rapi.AddResponse("{}")
+    self.assertFalse(self.client.WaitForJobCompletion(22793, period=None,
+                                                     retries=10))
+    self.assertHandler(rlib2.R_2_jobs_id)
+    self.assertItems(["22793"])
+
+    self.assertEqual(self.rapi.CountPending(), 0)
+
+  def testWaitForJobCompletionOutOfRetries(self):
+    for retries in [3, 10, 21]:
+      for _ in range(retries):
+        self.rapi.AddResponse(serializer.DumpJson({
+          "status": constants.JOB_STATUS_RUNNING,
+          }))
+
+      self.assertFalse(self.client.WaitForJobCompletion(30948, period=None,
+                                                        retries=retries - 1))
+      self.assertHandler(rlib2.R_2_jobs_id)
+      self.assertItems(["30948"])
+
+      self.assertEqual(self.rapi.CountPending(), 1)
+      self.rapi.ResetResponses()
+
+  def testWaitForJobCompletionSuccessAndFailure(self):
+    for retries in [1, 4, 13]:
+      for (success, end_status) in [(False, constants.JOB_STATUS_ERROR),
+                                    (True, constants.JOB_STATUS_SUCCESS)]:
+        for _ in range(retries):
+          self.rapi.AddResponse(serializer.DumpJson({
+            "status": constants.JOB_STATUS_RUNNING,
+            }))
+
+        self.rapi.AddResponse(serializer.DumpJson({
+          "status": end_status,
+          }))
+
+        result = self.client.WaitForJobCompletion(3187, period=None,
+                                                  retries=retries + 1)
+        self.assertEqual(result, success)
+        self.assertHandler(rlib2.R_2_jobs_id)
+        self.assertItems(["3187"])
+
+        self.assertEqual(self.rapi.CountPending(), 0)
+
+
+class RapiTestRunner(unittest.TextTestRunner):
+  def run(self, *args):
+    global _used_handlers
+    assert _used_handlers is None
+
+    _used_handlers = set()
+    try:
+      # Run actual tests
+      result = unittest.TextTestRunner.run(self, *args)
+
+      diff = (set(connector.CONNECTOR.values()) - _used_handlers -
+             _KNOWN_UNUSED)
+      if diff:
+        raise AssertionError("The following RAPI resources were not used by the"
+                             " RAPI client: %r" % utils.CommaJoin(diff))
+    finally:
+      # Reset global variable
+      _used_handlers = None
+
+    return result
+
 
 if __name__ == '__main__':
-  client.UsesRapiClient(testutils.GanetiTestProgram)()
+  client.UsesRapiClient(testutils.GanetiTestProgram)(testRunner=RapiTestRunner)