diskbackend: Add lots of test coverage

Signed-off-by: Cole Robinson <crobinso@redhat.com>
This commit is contained in:
Cole Robinson 2020-01-29 07:30:51 -05:00
parent 04c0d48ef7
commit e7bb021c4c
6 changed files with 149 additions and 120 deletions

View File

@ -217,6 +217,11 @@
<source file="/var/lib/libvirt/images/disk.img"/> <source file="/var/lib/libvirt/images/disk.img"/>
<target dev="vdp" bus="virtio"/> <target dev="vdp" bus="virtio"/>
</disk> </disk>
<disk type="block" device="disk">
<driver name="qemu" type="raw" cache="none" io="native"/>
<source dev="/dev/disk-pool/disk"/>
<target dev="vdq" bus="virtio"/>
</disk>
<controller type="usb" index="0" model="ich9-ehci1"> <controller type="usb" index="0" model="ich9-ehci1">
<address type="pci" domain="0" bus="0" slot="4" function="7"/> <address type="pci" domain="0" bus="0" slot="4" function="7"/>
</controller> </controller>

View File

@ -615,6 +615,7 @@ vcpus.vcpu1.id=2,vcpus.vcpu1.enabled=yes
source.reservations.managed=no,source.reservations.source.type=unix,source.reservations.source.path=/var/run/test/pr-helper0.sock,source.reservations.source.mode=client source.reservations.managed=no,source.reservations.source.type=unix,source.reservations.source.path=/var/run/test/pr-helper0.sock,source.reservations.source.mode=client
--disk vol=iscsi-direct/unit:0:0:1 --disk vol=iscsi-direct/unit:0:0:1
--disk size=.0001,format=raw --disk size=.0001,format=raw
--disk size=.0001,pool=disk-pool
--network user,mac=12:34:56:78:11:22,portgroup=foo,link_state=down,rom_bar=on,rom_file=/tmp/foo --network user,mac=12:34:56:78:11:22,portgroup=foo,link_state=down,rom_bar=on,rom_file=/tmp/foo
--network bridge=foobar,model=virtio,driver_name=qemu,driver_queues=3,filterref=foobar,rom.bar=off,rom.file=/some/rom,source.portgroup=foo --network bridge=foobar,model=virtio,driver_name=qemu,driver_queues=3,filterref=foobar,rom.bar=off,rom.file=/some/rom,source.portgroup=foo

View File

@ -206,6 +206,17 @@ class TestXMLMisc(unittest.TestCase):
errdict = virtinst.DeviceDisk.fix_path_search(searchdata) errdict = virtinst.DeviceDisk.fix_path_search(searchdata)
self.assertTrue(not bool(errdict)) self.assertTrue(not bool(errdict))
# Mock setfacl to definitely, as getfacl won't accept args
with unittest.mock.patch("virtinst.diskbackend.SETFACL",
"getfacl"):
errdict = virtinst.DeviceDisk.fix_path_search(searchdata)
# Test uid check short circuiting
searchdata.uid = os.getuid()
os.chown(tmpdir, os.getuid(), os.getgid())
assert virtinst.diskbackend.is_path_searchable(
tmpdir, os.getuid(), "foo") == []
def test_path_in_use(self): def test_path_in_use(self):
# Extra tests for DeviceDisk.path_in_use # Extra tests for DeviceDisk.path_in_use
conn = utils.URIs.open_kvm() conn = utils.URIs.open_kvm()
@ -238,3 +249,20 @@ class TestXMLMisc(unittest.TestCase):
except Exception as e: except Exception as e:
if not self.conn.support.is_libvirt_error_no_domain(e): if not self.conn.support.is_libvirt_error_no_domain(e):
raise raise
def test_disk_backend(self):
# Test get_size() with vol_install
disk = virtinst.DeviceDisk(self.conn)
pool = self.conn.storagePoolLookupByName("default-pool")
vol_install = disk.build_vol_install(self.conn, "newvol1.img",
pool, 1, False)
disk.set_vol_install(vol_install)
assert disk.get_size() == 1.0
# Test some blockdev inspecting
if os.path.exists("/dev/loop0"):
conn = utils.URIs.openconn("test:///default")
disk = virtinst.DeviceDisk(conn)
disk.path = "/dev/loop0"
assert disk.type == "block"
disk.get_size()

View File

@ -1558,11 +1558,9 @@ class XMLParseTest(unittest.TestCase):
guest.devices.replace_child(guest.devices.disk[4], newdisk) guest.devices.replace_child(guest.devices.disk[4], newdisk)
utils.diff_compare(guest.get_xml(), parsefile) utils.diff_compare(guest.get_xml(), parsefile)
def testDiskRevalidate(self): def testDiskBackend(self):
""" # Test that calling validate() on parsed disk XML doesn't attempt
Test that calling validate() on parsed disk XML doesn't attempt # to verify the path exists. Assume it's a working config
to verify the path exists. Assume it's a working config
"""
xml = ("<disk type='file' device='disk'>" xml = ("<disk type='file' device='disk'>"
"<source file='/A/B/C/D/NOPE'/>" "<source file='/A/B/C/D/NOPE'/>"
"</disk>") "</disk>")
@ -1572,6 +1570,17 @@ class XMLParseTest(unittest.TestCase):
disk.build_storage(None) disk.build_storage(None)
self.assertTrue(getattr(disk, "_storage_backend").is_stub()) self.assertTrue(getattr(disk, "_storage_backend").is_stub())
# Stub backend coverage testing
backend = getattr(disk, "_storage_backend")
assert disk.get_parent_pool() is None
assert disk.get_vol_object() is None
assert disk.get_vol_install() is None
assert disk.get_size() == 0
assert backend.get_vol_xml() is None
assert backend.get_dev_type() == "file"
assert backend.get_driver_type() is None
assert backend.get_parent_pool() is None
disk.set_backend_for_existing_path() disk.set_backend_for_existing_path()
self.assertFalse(getattr(disk, "_storage_backend").is_stub()) self.assertFalse(getattr(disk, "_storage_backend").is_stub())

View File

@ -734,13 +734,13 @@ class DeviceDisk(Device):
return return
if (not self._storage_backend.will_create_storage() and if (not self._storage_backend.exists() and
not self._storage_backend.exists()): not self._storage_backend.will_create_storage()):
raise ValueError( raise ValueError(
_("Must specify storage creation parameters for " _("Must specify storage creation parameters for "
"non-existent path '%s'.") % self.path) "non-existent path '%s'.") % self.path)
self._storage_backend.validate(self) self._storage_backend.validate()
def build_storage(self, meter): def build_storage(self, meter):
""" """

View File

@ -50,29 +50,29 @@ def _lookup_vol_by_basename(pool, path):
return pool.storageVolLookupByName(name) return pool.storageVolLookupByName(name)
def _stat_disk(path): def _get_block_size(path):
""" try:
Returns the tuple (isreg, size) fd = os.open(path, os.O_RDONLY)
""" # os.SEEK_END is not present on all systems
size = os.lseek(fd, 0, 2) # pragma: no cover
os.close(fd) # pragma: no cover
except Exception:
size = 0
return size
def _get_size(path):
if not os.path.exists(path): if not os.path.exists(path):
return True, 0 return 0
if _stat_is_block(path):
return _get_block_size(path)
return os.path.getsize(path)
mode = os.stat(path)[stat.ST_MODE]
# os.path.getsize('/dev/..') can be zero on some platforms def _stat_is_block(path):
if stat.S_ISBLK(mode): if not os.path.exists(path):
try: return False
fd = os.open(path, os.O_RDONLY) return stat.S_ISBLK(os.stat(path)[stat.ST_MODE])
# os.SEEK_END is not present on all systems
size = os.lseek(fd, 0, 2)
os.close(fd)
except Exception:
size = 0
return False, size
elif stat.S_ISREG(mode):
return True, os.path.getsize(path)
return True, 0
def _check_if_path_managed(conn, path): def _check_if_path_managed(conn, path):
@ -98,14 +98,14 @@ def _check_if_path_managed(conn, path):
if verr: if verr:
try: try:
vol = _lookup_vol_by_basename(pool, path) vol = _lookup_vol_by_basename(pool, path)
except Exception: except Exception: # pragma: no cover
pass pass
except Exception as e: except Exception as e: # pragma: no cover
vol = None vol = None
pool = None pool = None
verr = str(e) verr = str(e)
if not vol and not pool and verr: if not vol and not pool and verr: # pragma: no cover
raise ValueError(_("Cannot use storage %(path)s: %(err)s") % raise ValueError(_("Cannot use storage %(path)s: %(err)s") %
{'path': path, 'err': verr}) {'path': path, 'err': verr})
@ -178,25 +178,8 @@ def _get_dev_type(path, vol_xml, vol_object, pool_xml, remote):
""" """
Try to get device type for volume. Try to get device type for volume.
""" """
if vol_xml: if vol_xml and vol_xml.type:
if vol_xml.type: return vol_xml.type
return vol_xml.type
# If vol_xml.type is None the vol_xml.file_type can return only
# these types: block, network or file
if vol_xml.file_type == libvirt.VIR_STORAGE_VOL_BLOCK:
return "block"
elif vol_xml.file_type == libvirt.VIR_STORAGE_VOL_NETWORK:
return "network"
if vol_object:
t = vol_object.info()[0]
if t == StorageVolume.TYPE_FILE:
return "file"
elif t == StorageVolume.TYPE_BLOCK:
return "block"
elif t == StorageVolume.TYPE_NETWORK:
return "network"
if pool_xml: if pool_xml:
t = pool_xml.get_disk_type() t = pool_xml.get_disk_type()
@ -205,6 +188,17 @@ def _get_dev_type(path, vol_xml, vol_object, pool_xml, remote):
elif t == StorageVolume.TYPE_NETWORK: elif t == StorageVolume.TYPE_NETWORK:
return "network" return "network"
if vol_object: # pragma: no cover
# This path is hard to test, because test suite XML always has
# the vol_xml.type set
t = vol_object.info()[0]
if t == StorageVolume.TYPE_FILE:
return "file"
elif t == StorageVolume.TYPE_BLOCK:
return "block"
elif t == StorageVolume.TYPE_NETWORK:
return "network"
if path: if path:
if path_is_url(path): if path_is_url(path):
return "network" return "network"
@ -212,9 +206,7 @@ def _get_dev_type(path, vol_xml, vol_object, pool_xml, remote):
if not remote: if not remote:
if os.path.isdir(path): if os.path.isdir(path):
return "dir" return "dir"
elif _stat_disk(path)[0]: elif _stat_is_block(path):
return "file"
else:
return "block" return "block"
return "file" return "file"
@ -246,8 +238,11 @@ def path_definitely_exists(conn, path):
# ACL/path perm helpers # # ACL/path perm helpers #
######################### #########################
SETFACL = "setfacl"
def _fix_perms_acl(dirname, username): def _fix_perms_acl(dirname, username):
cmd = ["setfacl", "--modify", "user:%s:x" % username, dirname] cmd = [SETFACL, "--modify", "user:%s:x" % username, dirname]
proc = subprocess.Popen(cmd, proc = subprocess.Popen(cmd,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
@ -269,8 +264,8 @@ def _fix_perms_chmod(dirname):
if os.stat(dirname).st_mode != newmode: if os.stat(dirname).st_mode != newmode:
# Trying to change perms on vfat at least doesn't work # Trying to change perms on vfat at least doesn't work
# but also doesn't seem to error. Try and detect that # but also doesn't seem to error. Try and detect that
raise ValueError(_("Permissions on '%s' did not stick") % raise ValueError( # pragma: no cover
dirname) _("Permissions on '%s' did not stick") % dirname)
def set_dirs_searchable(dirlist, username): def set_dirs_searchable(dirlist, username):
@ -289,7 +284,7 @@ def set_dirs_searchable(dirlist, username):
try: try:
# If we reach here, ACL setting failed, try chmod # If we reach here, ACL setting failed, try chmod
_fix_perms_chmod(dirname) _fix_perms_chmod(dirname)
except Exception as e: except Exception as e: # pragma: no cover
errdict[dirname] = str(e) errdict[dirname] = str(e)
return errdict return errdict
@ -304,13 +299,13 @@ def _is_dir_searchable(dirname, uid, username):
try: try:
statinfo = os.stat(dirname) statinfo = os.stat(dirname)
except OSError: except OSError: # pragma: no cover
return False return False
if uid == statinfo.st_uid: if uid == statinfo.st_uid:
flag = stat.S_IXUSR flag = stat.S_IXUSR
elif uid == statinfo.st_gid: elif uid == statinfo.st_gid:
flag = stat.S_IXGRP flag = stat.S_IXGRP # pragma: no cover
else: else:
flag = stat.S_IXOTH flag = stat.S_IXOTH
@ -324,11 +319,11 @@ def _is_dir_searchable(dirname, uid, username):
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
out, err = proc.communicate() out, err = proc.communicate()
except OSError: except OSError: # pragma: no cover
log.debug("Didn't find the getfacl command.") log.debug("Didn't find the getfacl command.")
return False return False
if proc.returncode != 0: if proc.returncode != 0: # pragma: no cover
log.debug("Cmd '%s' failed: %s", cmd, err) log.debug("Cmd '%s' failed: %s", cmd, err)
return False return False
@ -386,7 +381,7 @@ class _StorageBase(object):
self._parent_pool_xml = StoragePool(self._conn, self._parent_pool_xml = StoragePool(self._conn,
parsexml=self.get_parent_pool().XMLDesc(0)) parsexml=self.get_parent_pool().XMLDesc(0))
return self._parent_pool_xml return self._parent_pool_xml
def validate(self, disk): def validate(self):
raise NotImplementedError() raise NotImplementedError()
def get_path(self): def get_path(self):
raise NotImplementedError() raise NotImplementedError()
@ -424,7 +419,11 @@ class _StorageCreator(_StorageBase):
############## ##############
def create(self, progresscb): def create(self, progresscb):
raise NotImplementedError() raise NotImplementedError
def validate(self):
raise NotImplementedError
def get_size(self):
raise NotImplementedError
def get_path(self): def get_path(self):
if self._vol_install and not self._path: if self._vol_install and not self._path:
@ -445,12 +444,6 @@ class _StorageCreator(_StorageBase):
def get_vol_xml(self): def get_vol_xml(self):
return self._vol_install return self._vol_install
def get_size(self):
if self._size is None:
self._size = (float(self._vol_install.capacity) /
1024.0 / 1024.0 / 1024.0)
return self._size
def get_dev_type(self): def get_dev_type(self):
if not self._dev_type: if not self._dev_type:
self._dev_type = _get_dev_type(self._path, self._vol_install, None, self._dev_type = _get_dev_type(self._path, self._vol_install, None,
@ -464,25 +457,6 @@ class _StorageCreator(_StorageBase):
return self._vol_install.format return self._vol_install.format
return "raw" return "raw"
def validate(self, disk):
if disk.device in ["floppy", "cdrom"]:
raise ValueError(_("Cannot create storage for %s device.") %
disk.device)
if self._vol_install:
self._vol_install.validate()
return
if self._size is None:
raise ValueError(_("size is required for non-existent disk "
"'%s'" % self.get_path()))
err, msg = self.is_size_conflict()
if err:
raise ValueError(msg)
if msg:
log.warning(msg)
def will_create_storage(self): def will_create_storage(self):
return True return True
def get_vol_object(self): def get_vol_object(self):
@ -495,6 +469,28 @@ class _StorageCreator(_StorageBase):
return False return False
class ManagedStorageCreator(_StorageCreator):
"""
Handles storage creation via libvirt APIs. All the actual creation
logic lives in StorageVolume, this is mostly about pulling out bits
from that class and mapping them to DeviceDisk elements
"""
def __init__(self, conn, vol_install):
_StorageCreator.__init__(self, conn)
self._pool = vol_install.pool
self._vol_install = vol_install
def create(self, progresscb):
return self._vol_install.install(meter=progresscb)
def is_size_conflict(self):
return self._vol_install.is_size_conflict()
def validate(self):
return self._vol_install.validate()
def get_size(self):
return float(self._vol_install.capacity) / 1024.0 / 1024.0 / 1024.0
class CloneStorageCreator(_StorageCreator): class CloneStorageCreator(_StorageCreator):
""" """
Handles manually copying local files for Cloner Handles manually copying local files for Cloner
@ -511,11 +507,14 @@ class CloneStorageCreator(_StorageCreator):
self._size = size self._size = size
self._sparse = sparse self._sparse = sparse
def get_size(self):
return self._size
def is_size_conflict(self): def is_size_conflict(self):
ret = False ret = False
msg = None msg = None
if self.get_dev_type() == "block": if self.get_dev_type() == "block":
avail = _stat_disk(self._path)[1] avail = _get_size(self._path)
else: else:
vfs = os.statvfs(os.path.dirname(self._path)) vfs = os.statvfs(os.path.dirname(self._path))
avail = vfs.f_frsize * vfs.f_bavail avail = vfs.f_frsize * vfs.f_bavail
@ -535,6 +534,17 @@ class CloneStorageCreator(_StorageCreator):
((need // (1024 * 1024)), (avail // (1024 * 1024)))) ((need // (1024 * 1024)), (avail // (1024 * 1024))))
return (ret, msg) return (ret, msg)
def validate(self):
if self._size is None:
raise ValueError(_("size is required for non-existent disk "
"'%s'" % self.get_path()))
err, msg = self.is_size_conflict()
if err:
raise ValueError(msg)
if msg:
log.warning(msg)
def create(self, progresscb): def create(self, progresscb):
text = (_("Cloning %(srcfile)s") % text = (_("Cloning %(srcfile)s") %
{'srcfile': os.path.basename(self._input_path)}) {'srcfile': os.path.basename(self._input_path)})
@ -547,7 +557,7 @@ class CloneStorageCreator(_StorageCreator):
self._clone_local(progresscb, size_bytes) self._clone_local(progresscb, size_bytes)
def _clone_local(self, meter, size_bytes): def _clone_local(self, meter, size_bytes):
if self._input_path == "/dev/null": if self._input_path == "/dev/null": # pragma: no cover
# Not really sure why this check is here, # Not really sure why this check is here,
# but keeping for compat # but keeping for compat
log.debug("Source dev was /dev/null. Skipping") log.debug("Source dev was /dev/null. Skipping")
@ -605,7 +615,7 @@ class CloneStorageCreator(_StorageCreator):
i += s i += s
if i < size_bytes: if i < size_bytes:
meter.update(i) meter.update(i)
except OSError as e: except OSError as e: # pragma: no cover
raise RuntimeError(_("Error cloning diskimage %s to %s: %s") % raise RuntimeError(_("Error cloning diskimage %s to %s: %s") %
(self._input_path, self._output_path, str(e))) (self._input_path, self._output_path, str(e)))
finally: finally:
@ -615,24 +625,6 @@ class CloneStorageCreator(_StorageCreator):
os.close(dst_fd) os.close(dst_fd)
class ManagedStorageCreator(_StorageCreator):
"""
Handles storage creation via libvirt APIs. All the actual creation
logic lives in StorageVolume, this is mostly about pulling out bits
from that class and mapping them to DeviceDisk elements
"""
def __init__(self, conn, vol_install):
_StorageCreator.__init__(self, conn)
self._pool = vol_install.pool
self._vol_install = vol_install
def create(self, progresscb):
return self._vol_install.install(meter=progresscb)
def is_size_conflict(self):
return self._vol_install.is_size_conflict()
class StorageBackendStub(_StorageBase): class StorageBackendStub(_StorageBase):
""" """
Class representing a storage path for a parsed XML disk, that we Class representing a storage path for a parsed XML disk, that we
@ -662,8 +654,7 @@ class StorageBackendStub(_StorageBase):
def get_driver_type(self): def get_driver_type(self):
return self._driver_type return self._driver_type
def validate(self, disk): def validate(self):
ignore = disk
return return
def get_vol_install(self): def get_vol_install(self):
return None return None
@ -732,15 +723,15 @@ class StorageBackend(_StorageBase):
if self._vol_object: if self._vol_object:
ret = self.get_vol_xml().capacity ret = self.get_vol_xml().capacity
elif self._path: elif self._path:
ret = _stat_disk(self._path)[1] ret = _get_size(self._path)
self._size = (float(ret) / 1024.0 / 1024.0 / 1024.0) self._size = (float(ret) / 1024.0 / 1024.0 / 1024.0)
return self._size return self._size
def exists(self): def exists(self):
if self._exists is None: if self._exists is None:
if self._path is None: if self._vol_object:
self._exists = True self._exists = True
elif self._vol_object: elif self._path is None:
self._exists = True self._exists = True
elif (not self.get_dev_type() == "network" and elif (not self.get_dev_type() == "network" and
not self._conn.is_remote() and not self._conn.is_remote() and
@ -781,8 +772,7 @@ class StorageBackend(_StorageBase):
return ret return ret
return None return None
def validate(self, disk): def validate(self):
ignore = disk
return return
def get_vol_install(self): def get_vol_install(self):
return None return None
@ -790,7 +780,3 @@ class StorageBackend(_StorageBase):
return (False, None) return (False, None)
def will_create_storage(self): def will_create_storage(self):
return False return False
def create(self, progresscb):
ignore = progresscb
raise RuntimeError("programming error: %s can't create storage" %
self.__class__.__name__)