"""Migration: the source definition never stays behind; a shut-off VM's
definition is moved by the app, not by libvirt's offline migration."""
import unittest
import libvirt
from django.conf import settings
if not settings.configured:
settings.configure(MAC_OUI="52:54:10")
from vrtManager import util
from vrtManager.instance import wvmInstances
def no_domain():
error = libvirt.libvirtError("Domain not found")
error.err = (libvirt.VIR_ERR_NO_DOMAIN,)
return error
class FakeDomain:
def __init__(self, xml="", snapshots=0, saved=False, undefine_fails=False, undefine_lost=False,
started=False):
self.calls = []
self.xml = xml
self.snapshots = snapshots
self.saved = saved
self.undefine_fails = undefine_fails
self.undefine_lost = undefine_lost # undefined, but the reply is lost
self.started = started # started by another client during the move
self.undefined = None
self.redefined = []
self.restored_autostart = None
def migrate(self, dconn, flags, dname, uri, bandwidth):
self.calls.append((flags, uri))
def name(self):
return "vm"
def UUIDString(self):
return "u-1"
def XMLDesc(self, flags):
return self.xml
def snapshotNum(self, flags=0):
return self.snapshots
def hasManagedSaveImage(self, flags=0):
return self.saved
def undefineFlags(self, flags):
if self.undefine_fails:
raise libvirt.libvirtError("undefine failed")
self.undefined = flags
if self.undefine_lost:
raise libvirt.libvirtError("Cannot recv data")
def isPersistent(self):
return self.undefined is None or bool(self.redefined)
def isActive(self):
if not self.started and self.undefined is not None:
raise no_domain() # libvirt: an undefined shut-off VM is gone
return self.started
def autostart(self):
return 1
def setAutostart(self, value):
self.restored_autostart = value
def connect(self):
return self
def defineXML(self, xml):
self.redefined.append(xml)
return self
class FakeHost:
"""The destination's libvirt connection."""
def __init__(self, existing=()):
self.existing = set(existing)
self.defined = []
self.new = []
def lookup(self, key):
if key in self.existing:
return FakeDomain()
raise no_domain()
lookupByName = lookupByUUIDString = lookup
def defineXML(self, xml):
self.defined.append(xml)
self.new.append(FakeDomain())
return self.new[-1]
class FakeSource:
def __init__(self, state, **domain):
self.state = state
self.instance = FakeDomain(**domain)
reads = 0
def get_status(self):
self.reads += 1
return self.state
def get_arch(self):
return "x86_64"
def get_dom_emulator(self):
return "/usr/bin/qemu-system-x86_64"
def destination(host=None):
dest = wvmInstances.__new__(wvmInstances)
dest.wvm = host or FakeHost()
dest.get_emulator = lambda arch: "/usr/bin/qemu-system-x86_64"
return dest
class MigrationFlagsTestCase(unittest.TestCase):
def test_the_source_definition_is_always_removed(self):
for state, live, offline in ((1, True, False), (3, False, False)):
with self.subTest(state=state):
source = FakeSource(state)
destination().moveto(source, "vm", live=live, unsafe=False, offline=offline)
flags = source.instance.calls[0][0]
self.assertTrue(flags & libvirt.VIR_MIGRATE_UNDEFINE_SOURCE)
self.assertTrue(flags & libvirt.VIR_MIGRATE_PERSIST_DEST)
def test_the_migration_uri_is_passed_on(self):
source = FakeSource(1)
destination().moveto(source, "vm", live=True, unsafe=False, offline=False, uri="tcp://192.0.2.12")
self.assertEqual(source.instance.calls[0][1], "tcp://192.0.2.12")
source = FakeSource(1)
destination().moveto(source, "vm", live=True, unsafe=False, offline=False)
self.assertIsNone(source.instance.calls[0][1])
class ShutOffMigrationTestCase(unittest.TestCase):
"""libvirt's offline migration fails on some builds (RHEL-156800); a
shut-off VM is only its definition, so the app moves that itself."""
def test_the_definition_moves_to_the_destination(self):
host = FakeHost()
source = FakeSource(5, xml="vm")
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertEqual(host.defined, ["vm"])
self.assertEqual(source.instance.calls, [])
self.assertEqual(source.instance.undefined, libvirt.VIR_DOMAIN_UNDEFINE_KEEP_NVRAM)
def test_tpm_state_stays_on_the_source(self):
source = FakeSource(5, xml="")
destination().moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertEqual(
source.instance.undefined,
libvirt.VIR_DOMAIN_UNDEFINE_KEEP_NVRAM | libvirt.VIR_DOMAIN_UNDEFINE_KEEP_TPM,
)
def test_refused_before_anything_changes(self):
cases = {
"snapshots": (FakeHost(), {"snapshots": 2}),
"saved state": (FakeHost(), {"saved": True}),
"same uuid on the destination": (FakeHost(existing={"u-1"}), {}),
"same name on the destination": (FakeHost(existing={"vm"}), {}),
}
for case, (host, domain) in cases.items():
with self.subTest(case=case):
source = FakeSource(5, **domain)
with self.assertRaises(util.OperationError):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertEqual(host.defined, [])
self.assertIsNone(source.instance.undefined)
def test_a_failed_source_undefine_removes_the_new_definition(self):
host = FakeHost()
source = FakeSource(5, undefine_fails=True)
with self.assertRaises(libvirt.libvirtError):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertIsNotNone(host.new[0].undefined)
def test_the_destination_definition_stays_when_the_source_has_none(self):
host = FakeHost()
source = FakeSource(5, undefine_lost=True)
with self.assertRaisesRegex(util.OperationError, "only on the destination"):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertIsNone(host.new[0].undefined)
def test_a_failed_rollback_is_reported(self):
host = FakeHost()
source = FakeSource(5, undefine_fails=True)
host.defineXML = lambda xml: host.new.append(FakeDomain(undefine_fails=True)) or host.new[-1]
with self.assertRaisesRegex(util.OperationError, "undefine failed; the VM is now defined on both hosts"):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
def test_a_vm_started_meanwhile_stays_on_the_source(self):
host = FakeHost()
source = FakeSource(5, xml="vm", started=True)
with self.assertRaisesRegex(util.OperationError, "started on the source"):
destination(host).moveto(source, "vm", live=False, unsafe=False, offline=True)
self.assertEqual(source.instance.redefined, ["vm"])
self.assertEqual(source.instance.restored_autostart, 1)
self.assertIsNotNone(host.new[0].undefined)
class MigrationModeTestCase(unittest.TestCase):
"""The state is read once; a request that does not fit it is refused
before anything changes, and moveto names the mode it used."""
def migrate(self, state, **request):
source = FakeSource(state)
options = {"live": False, "unsafe": False, "offline": False, **request}
mode = destination().moveto(source, "vm", **options)
return mode, source.instance.calls[0][0] if source.instance.calls else None
def test_refused_requests(self):
for state, request in ((5, {"live": True}), (1, {"offline": True}), (3, {"offline": True}),
(1, {"live": True, "offline": True}), (6, {}), (4, {}), (7, {}), (0, {})):
with self.subTest(state=state, request=request):
source = FakeSource(state)
with self.assertRaises(util.OperationError):
destination().moveto(source, "vm", unsafe=False, **{"live": False, "offline": False, **request})
self.assertEqual(source.instance.calls, [])
def test_modes(self):
live, unsafe = libvirt.VIR_MIGRATE_LIVE, libvirt.VIR_MIGRATE_UNSAFE
converge, compressed = libvirt.VIR_MIGRATE_AUTO_CONVERGE, libvirt.VIR_MIGRATE_COMPRESSED
mode, flags = self.migrate(1, live=True, autoconverge=True, compress=True, unsafe=True)
self.assertEqual(mode, "live, unsafe, auto converge, compressed")
self.assertEqual(flags & (live | unsafe | converge | compressed), live | unsafe | converge | compressed)
source = FakeSource(1)
destination().moveto(source, "vm", live=True, unsafe=False, offline=False)
self.assertEqual(source.reads, 1)
mode, flags = self.migrate(1, compress=True, autoconverge=True)
self.assertEqual(mode, "non-live, compressed")
self.assertEqual(flags & (live | converge | compressed), compressed)
# a paused VM stays paused; the tuning options are for running VMs
mode, flags = self.migrate(3, live=True, autoconverge=True, compress=True, unsafe=True)
self.assertEqual(mode, "live")
self.assertEqual(flags & (live | unsafe | converge | compressed), live)
self.assertEqual(self.migrate(5), ("offline", None))
self.assertEqual(self.migrate(5, offline=True), ("offline", None))