hv_xen: Fix issues with migration, add tests
authorMichael Hanselmann <hansmi@google.com>
Fri, 25 Jan 2013 13:35:43 +0000 (14:35 +0100)
committerMichael Hanselmann <hansmi@google.com>
Fri, 25 Jan 2013 14:07:05 +0000 (15:07 +0100)
Commit 3d942d8 broke instance migration (“self._cmd” was set to None).
This patch fixes that issue, refactors “MigrateInstance” for testing and
adds those tests.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Guido Trotter <ultrotter@google.com>

lib/hypervisor/hv_xen.py
test/py/ganeti.hypervisor.hv_xen_unittest.py

index 3dc9355..11f9544 100644 (file)
@@ -336,6 +336,9 @@ class XenHypervisor(hv_base.BaseHypervisor):
     self._cmd = _cmd
 
   def _GetCommand(self):
+    """Returns Xen command to use.
+
+    """
     if self._cmd is None:
       # TODO: Make command a hypervisor parameter
       cmd = constants.XEN_CMD
@@ -663,37 +666,53 @@ class XenHypervisor(hv_base.BaseHypervisor):
     @param live: perform a live migration
 
     """
-    if self.GetInstanceInfo(instance.name) is None:
+    port = instance.hvparams[constants.HV_MIGRATION_PORT]
+
+    # TODO: Pass cluster name via RPC
+    cluster_name = ssconf.SimpleStore().GetClusterName()
+
+    return self._MigrateInstance(cluster_name, instance.name, target, port,
+                                 live)
+
+  def _MigrateInstance(self, cluster_name, instance_name, target, port, live,
+                       _ping_fn=netutils.TcpPing):
+    """Migrate an instance to a target node.
+
+    @see: L{MigrateInstance} for details
+
+    """
+    if self.GetInstanceInfo(instance_name) is None:
       raise errors.HypervisorError("Instance not running, cannot migrate")
 
-    port = instance.hvparams[constants.HV_MIGRATION_PORT]
+    cmd = self._GetCommand()
 
-    if (self._cmd == constants.XEN_CMD_XM and
-        not netutils.TcpPing(target, port, live_port_needed=True)):
+    if (cmd == constants.XEN_CMD_XM and
+        not _ping_fn(target, port, live_port_needed=True)):
       raise errors.HypervisorError("Remote host %s not listening on port"
                                    " %s, cannot migrate" % (target, port))
 
     args = ["migrate"]
 
-    if self._cmd == constants.XEN_CMD_XM:
+    if cmd == constants.XEN_CMD_XM:
       args.extend(["-p", "%d" % port])
       if live:
         args.append("-l")
 
-    elif self._cmd == constants.XEN_CMD_XL:
-      cluster_name = ssconf.SimpleStore().GetClusterName()
-      args.extend(["-s", constants.XL_SSH_CMD % cluster_name])
-      args.extend(["-C", self._ConfigFileName(instance.name)])
+    elif cmd == constants.XEN_CMD_XL:
+      args.extend([
+        "-s", constants.XL_SSH_CMD % cluster_name,
+        "-C", self._ConfigFileName(instance_name),
+        ])
 
     else:
-      raise errors.HypervisorError("Unsupported xen command: %s" % self._cmd)
+      raise errors.HypervisorError("Unsupported Xen command: %s" % self._cmd)
 
-    args.extend([instance.name, target])
+    args.extend([instance_name, target])
 
     result = self._RunXen(args)
     if result.failed:
       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
-                                   (instance.name, result.output))
+                                   (instance_name, result.output))
 
   def FinalizeMigrationSource(self, instance, success, live):
     """Finalize the instance migration on the source node.
index cc45a94..057ac76 100755 (executable)
@@ -366,6 +366,10 @@ class _TestXenHypervisor(object):
                            "", "This command failed", None,
                            NotImplemented, NotImplemented)
 
+  def _FakeTcpPing(self, expected, result, target, port, **kwargs):
+    self.assertEqual((target, port), expected)
+    return result
+
   def testReadingNonExistentConfigFile(self):
     hv = self._GetHv()
 
@@ -588,6 +592,130 @@ class _TestXenHypervisor(object):
           hv._StopInstance(name, force)
           self.assertFalse(os.path.exists(cfgfile))
 
+  def _MigrateNonRunningInstCmd(self, cmd):
+    if cmd == [self.CMD, "list"]:
+      output = testutils.ReadTestData("xen-xm-list-4.0.1-dom0-only.txt")
+    else:
+      self.fail("Unhandled command: %s" % (cmd, ))
+
+    return self._SuccessCommand(output, cmd)
+
+  def testMigrateInstanceNotRunning(self):
+    name = "nonexistinginstance.example.com"
+    target = constants.IP4_ADDRESS_LOCALHOST
+    port = 14618
+
+    hv = self._GetHv(run_cmd=self._MigrateNonRunningInstCmd)
+
+    for live in [False, True]:
+      try:
+        hv._MigrateInstance(NotImplemented, name, target, port, live,
+                            _ping_fn=NotImplemented)
+      except errors.HypervisorError, err:
+        self.assertEqual(str(err), "Instance not running, cannot migrate")
+      else:
+        self.fail("Exception was not raised")
+
+  def _MigrateInstTargetUnreachCmd(self, cmd):
+    if cmd == [self.CMD, "list"]:
+      output = testutils.ReadTestData("xen-xm-list-4.0.1-four-instances.txt")
+    else:
+      self.fail("Unhandled command: %s" % (cmd, ))
+
+    return self._SuccessCommand(output, cmd)
+
+  def testMigrateTargetUnreachable(self):
+    name = "server01.example.com"
+    target = constants.IP4_ADDRESS_LOCALHOST
+    port = 28349
+
+    hv = self._GetHv(run_cmd=self._MigrateInstTargetUnreachCmd)
+
+    for live in [False, True]:
+      if self.CMD == constants.XEN_CMD_XL:
+        # TODO: Detect unreachable targets
+        pass
+      else:
+        try:
+          hv._MigrateInstance(NotImplemented, name, target, port, live,
+                              _ping_fn=compat.partial(self._FakeTcpPing,
+                                                      (target, port), False))
+        except errors.HypervisorError, err:
+          wanted = "Remote host %s not" % target
+          self.assertTrue(str(err).startswith(wanted))
+        else:
+          self.fail("Exception was not raised")
+
+  def _MigrateInstanceCmd(self, cluster_name, instance_name, target, port,
+                          live, fail, cmd):
+    if cmd == [self.CMD, "list"]:
+      output = testutils.ReadTestData("xen-xm-list-4.0.1-four-instances.txt")
+    elif cmd[:2] == [self.CMD, "migrate"]:
+      if self.CMD == constants.XEN_CMD_XM:
+        args = ["-p", str(port)]
+
+        if live:
+          args.append("-l")
+
+      elif self.CMD == constants.XEN_CMD_XL:
+        args = [
+          "-s", constants.XL_SSH_CMD % cluster_name,
+          "-C", utils.PathJoin(self.tmpdir, instance_name),
+          ]
+
+      else:
+        self.fail("Unknown Xen command '%s'" % self.CMD)
+
+      args.extend([instance_name, target])
+      self.assertEqual(cmd[2:], args)
+
+      if fail:
+        return self._FailingCommand(cmd)
+
+      output = ""
+    else:
+      self.fail("Unhandled command: %s" % (cmd, ))
+
+    return self._SuccessCommand(output, cmd)
+
+  def testMigrateInstance(self):
+    clustername = "cluster.example.com"
+    instname = "server01.example.com"
+    target = constants.IP4_ADDRESS_LOCALHOST
+    port = 22364
+
+    for live in [False, True]:
+      for fail in [False, True]:
+        ping_fn = \
+          testutils.CallCounter(compat.partial(self._FakeTcpPing,
+                                               (target, port), True))
+
+        run_cmd = \
+          compat.partial(self._MigrateInstanceCmd,
+                         clustername, instname, target, port, live,
+                         fail)
+
+        hv = self._GetHv(run_cmd=run_cmd)
+
+        if fail:
+          try:
+            hv._MigrateInstance(clustername, instname, target, port, live,
+                                _ping_fn=ping_fn)
+          except errors.HypervisorError, err:
+            self.assertTrue(str(err).startswith("Failed to migrate instance"))
+          else:
+            self.fail("Exception was not raised")
+        else:
+          hv._MigrateInstance(clustername, instname, target, port, live,
+                              _ping_fn=ping_fn)
+
+        if self.CMD == constants.XEN_CMD_XM:
+          expected_pings = 1
+        else:
+          expected_pings = 0
+
+        self.assertEqual(ping_fn.Count(), expected_pings)
+
 
 def _MakeTestClass(cls, cmd):
   """Makes a class for testing.