"""A volume a VM still needs is never deleted from a storage pool: not a
disk, not a backing file, not a file of a snapshot. libvirt deletes any
volume it is asked to; the live cases are in instances/tests/test_live_dataloss.py."""
import unittest
from unittest.mock import MagicMock
import libvirt
from libvirt import libvirtError
from vrtManager import util
from vrtManager.storage import wvmStorage
def domain(uuid, disks=(), snapshots=()):
dom = MagicMock()
dom.UUIDString.return_value = uuid
devices = "".join(f"" for d in disks)
dom.XMLDesc.return_value = f"{devices}"
snaps = []
for xml in snapshots:
snap = MagicMock()
snap.getXMLDesc.return_value = xml
snaps.append(snap)
dom.listAllSnapshots.return_value = snaps
return dom
def volume(path, backing=None):
vol = MagicMock()
vol.path.return_value = path
parent = f"{backing}" if backing else ""
vol.XMLDesc.return_value = f"{parent}"
return vol
def storage(domains=(), volumes=()):
conn = wvmStorage.__new__(wvmStorage)
conn.wvm = MagicMock()
conn.wvm.listAllDomains.return_value = list(domains)
pool = MagicMock(**{"isActive.return_value": True})
pool.listAllVolumes.return_value = list(volumes)
by_name = {v.path().rsplit("/", 1)[1]: v for v in volumes}
pool.storageVolLookupByName.side_effect = lambda name: by_name[name]
conn.wvm.listAllStoragePools.return_value = [pool]
conn.pool = pool
return conn, by_name
SNAPSHOT = (
"s"
""
""
""
)
class PathsInUseTestCase(unittest.TestCase):
def test_disks_backing_files_and_snapshot_files_are_in_use(self):
conn, _ = storage(
domains=[domain("u1", ["/pool/vm.now"], [SNAPSHOT])],
volumes=[volume("/pool/clone.qcow2", backing="/pool/template.qcow2")],
)
self.assertEqual(
conn.paths_in_use(),
{"/pool/vm.now", "/pool/vm.s", "/pool/vm.s.mem", "/pool/vm.qcow2", "/pool/template.qcow2"},
)
def test_one_domain_and_the_backing_files_of_own_volumes_can_be_left_out(self):
conn, _ = storage(
domains=[domain("u1", ["/pool/a.qcow2"]), domain("u2", ["/pool/b.qcow2"])],
volumes=[volume("/pool/a.qcow2", backing="/pool/base.qcow2")],
)
self.assertEqual(conn.paths_in_use(skip_uuid="u1", own=["/pool/a.qcow2"]), {"/pool/b.qcow2"})
def test_a_network_disk_is_in_use_by_its_name(self):
dom = domain("u1")
dom.XMLDesc.return_value = (
""
""
)
conn, _ = storage(domains=[dom])
self.assertIn("rbdpool/disk", conn.paths_in_use())
def test_a_pool_volume_disk_is_in_use_by_its_path(self):
for xml in (
"{}",
"", # in a snapshot's saved definition
):
with self.subTest(xml):
disk = ""
dom = domain("u1")
if "{}" in xml:
dom.XMLDesc.return_value = xml.format(disk)
else:
snap = MagicMock()
snap.getXMLDesc.return_value = f"{disk}"
dom.listAllSnapshots.return_value = [snap]
conn, _ = storage(domains=[dom], volumes=[volume("/pool/disk.qcow2")])
conn.wvm.storagePoolLookupByName.return_value = conn.pool
self.assertIn("/pool/disk.qcow2", conn.paths_in_use())
def test_a_volume_that_cannot_be_read_is_not_taken_as_unused(self):
removed, unreadable = volume("/pool/gone.qcow2"), volume("/pool/top.qcow2")
error = libvirtError("removed")
error.err = (libvirt.VIR_ERR_NO_STORAGE_VOL,)
removed.XMLDesc.side_effect = error
conn, _ = storage(volumes=[removed])
self.assertEqual(conn.paths_in_use(), set())
unreadable.XMLDesc.side_effect = libvirtError("permission denied")
conn, _ = storage(volumes=[unreadable])
with self.assertRaises(libvirtError):
conn.paths_in_use()
class DeleteVolumeTestCase(unittest.TestCase):
def test_a_volume_in_use_is_refused(self):
for case, domains, volumes in (
("disk", [domain("u1", ["/pool/disk.qcow2"])], [volume("/pool/disk.qcow2")]),
("backing file", [], [volume("/pool/disk.qcow2"), volume("/pool/top.qcow2", backing="/pool/disk.qcow2")]),
("snapshot point", [domain("u1", ["/pool/vm.s"], [SNAPSHOT.replace("vm.qcow2", "disk.qcow2")])],
[volume("/pool/disk.qcow2")]),
):
with self.subTest(case):
conn, vols = storage(domains, volumes)
with self.assertRaisesRegex(util.OperationError, "disk.qcow2 is in use"):
conn.del_volume("disk.qcow2")
vols["disk.qcow2"].delete.assert_not_called()
def test_an_unused_volume_is_deleted(self):
conn, vols = storage([domain("u1", ["/pool/other.qcow2"])], [volume("/pool/disk.qcow2")])
conn.del_volume("disk.qcow2")
vols["disk.qcow2"].delete.assert_called_once_with(0)
class RunningCopyTestCase(unittest.TestCase):
"""A running guest's disk is not copied: the copy would mix instants."""
def test_each_way_a_disk_names_the_volume_refuses_its_copy(self):
for source in ("file='/pool/disk.qcow2'", "dev='/pool/disk.qcow2'", "protocol='rbd' name='/pool/disk.qcow2'",
"pool='pool' volume='disk.qcow2'"):
with self.subTest(source=source):
dom = MagicMock(**{"name.return_value": "vm"})
dom.XMLDesc.return_value = f""
conn, _ = storage([dom], [volume("/pool/disk.qcow2")])
conn.pool.name.return_value = "pool"
with self.assertRaisesRegex(util.OperationError, "The running VMs vm use volume disk.qcow2"):
conn.refuse_running_copy("disk.qcow2")
conn.wvm.listAllDomains.assert_called_once_with(libvirt.VIR_CONNECT_LIST_DOMAINS_ACTIVE)
def test_another_disk_does_not(self):
conn, _ = storage([domain("u1", ["/pool/other.qcow2"])], [volume("/pool/disk.qcow2")])
conn.pool.name.return_value = "pool"
conn.refuse_running_copy("disk.qcow2")