1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 11:16:47 +00:00
webvirtcloud/instances/test_live_clone.py
catborise 57cf49262a fix(instances): clones roll back their copies; own error messages stay
Clone:
- only shut-off VMs are cloned, as the UI already required: a running
  guest keeps writing while its disks are copied; the definition comes
  from the persistent XML;
- MAC addresses and disk destinations are checked before anything is
  allocated;
- every volume the clone creates (NVRAM and disks) is tracked; if a
  later copy or the definition fails, only those are deleted, and any
  that cannot be deleted are named in the error. Before, a failed clone
  left its earlier copies behind.

libvirtError(msg) reports the thread's last libvirt error instead of
msg when there is one, so WebVirtCloud's own errors could show an
earlier, unrelated failure. util.OperationError, a libvirtError
subclass that keeps its message, replaces those raises in vrtManager,
instances and nwfilters; 'except libvirtError' still catches it.
2026-10-04 01:08:11 +03:00

92 lines
3.2 KiB
Python

"""Live checks for cloning (R-08). Runs only with TEST_LIBVIRT_HOST set."""
import os
import unittest
import libvirt
from django.test import SimpleTestCase
from vrtManager.connection import connection_manager
from vrtManager.instance import wvmInstance
from . import livetest
P = livetest.PREFIX
class LiveCloneTestCase(SimpleTestCase):
@classmethod
def setUpClass(cls):
if not livetest.enabled():
raise unittest.SkipTest("Set TEST_LIBVIRT_HOST to run the live libvirt tests")
cls.args = (
os.environ["TEST_LIBVIRT_HOST"],
os.environ.get("TEST_LIBVIRT_LOGIN", ""),
os.environ.get("TEST_LIBVIRT_PASSWORD", ""),
int(os.environ.get("TEST_LIBVIRT_TYPE", 4)),
)
cls.conn = connection_manager.get_connection(*cls.args)
livetest.cleanup(cls.conn)
livetest.ensure_pool(cls.conn)
cls.inventory = livetest.inventory(cls.conn)
super().setUpClass()
@classmethod
def tearDownClass(cls):
try:
livetest.remove_pool(cls.conn)
finally:
super().tearDownClass()
if livetest.inventory(cls.conn) != cls.inventory:
raise AssertionError("Live tests changed objects outside the test namespace")
def tearDown(self):
livetest.cleanup(self.conn)
def pool_volumes(self):
pool = self.conn.storagePoolLookupByName(livetest.POOL)
pool.refresh(0)
return set(pool.listVolumes())
def source(self, name):
disks = [livetest.create_volume(self.conn, P + f"{name}-{d}") for d in ("a", "b")]
dom = livetest.define_vm(self.conn, P + name, disks)
return dom, wvmInstance(*self.args, None, uuid=dom.UUIDString())
def clone_data(self, name):
return {
"name": P + name,
"disk-vda": P + name + "-a.qcow2",
"disk-vdb": P + name + "-b.qcow2",
"disk_owner_uid": 0,
"disk_owner_gid": 0,
}
def test_failed_clone_removes_only_its_own_copies(self):
_, vm = self.source("clone-src")
taken = livetest.create_volume(self.conn, P + "clone-dst-b") # destination of vdb exists
before = self.pool_volumes()
with self.assertRaises(Exception):
vm.clone_instance(self.clone_data("clone-dst"))
self.assertEqual(self.pool_volumes(), before, "copies left behind or a volume deleted")
self.assertTrue(livetest.volume_exists(self.conn, taken))
with self.assertRaises(libvirt.libvirtError):
self.conn.lookupByName(P + "clone-dst")
def test_running_vm_is_not_cloned(self):
dom, vm = self.source("clone-run")
dom.create()
before = self.pool_volumes()
with self.assertRaises(Exception):
vm.clone_instance(self.clone_data("clone-run-copy"))
self.assertEqual(self.pool_volumes(), before)
def test_clone_of_a_stopped_vm(self):
_, vm = self.source("clone-ok")
uuid = vm.clone_instance(self.clone_data("clone-ok-copy"))
clone = self.conn.lookupByUUIDString(uuid)
self.assertEqual(clone.name(), P + "clone-ok-copy")
self.assertTrue({P + "clone-ok-copy-a.qcow2", P + "clone-ok-copy-b.qcow2"} <= self.pool_volumes())