salt/add-virt.volume_infos-and-virt.volume_delete.patch

335 lines
13 KiB
Diff
Raw Normal View History

From 2536ee56bd0060c024994f97388f9975ccbe1ee1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?C=C3=A9dric=20Bosdonnat?= <cbosdonnat@suse.com>
Date: Fri, 15 Feb 2019 17:28:00 +0100
Subject: [PATCH] Add virt.volume_infos() and virt.volume_delete()
Expose more functions to handle libvirt storage volumes.
virt.volume_infos() expose informations of the volumes, either for one or
all the volumes. Among the provided data, this function exposes the
names of the virtual machines using the volumes of file type.
virt.volume_delete() allows removing a given volume.
---
salt/modules/virt.py | 113 ++++++++++++++++++++
tests/unit/modules/test_virt.py | 184 ++++++++++++++++++++++++++++++++
2 files changed, 297 insertions(+)
diff --git a/salt/modules/virt.py b/salt/modules/virt.py
index 0921122a8a..4a301f289c 100644
--- a/salt/modules/virt.py
+++ b/salt/modules/virt.py
@@ -4988,3 +4988,116 @@ def pool_list_volumes(name, **kwargs):
return pool.listVolumes()
finally:
conn.close()
+
+
+def _get_storage_vol(conn, pool, vol):
+ '''
+ Helper function getting a storage volume. Will throw a libvirtError
+ if the pool or the volume couldn't be found.
+ '''
+ pool_obj = conn.storagePoolLookupByName(pool)
+ return pool_obj.storageVolLookupByName(vol)
+
+
+def _get_all_volumes_paths(conn):
+ '''
+ Extract the path and backing stores path of all volumes.
+
+ :param conn: libvirt connection to use
+ '''
+ volumes = [vol for l in [obj.listAllVolumes() for obj in conn.listAllStoragePools()] for vol in l]
+ return {vol.path(): [path.text for path in ElementTree.fromstring(vol.XMLDesc()).findall('.//backingStore/path')]
+ for vol in volumes}
+
+
+def volume_infos(pool, volume, **kwargs):
+ '''
+ Provide details on a storage volume. If no volume name is provided, the infos
+ all the volumes contained in the pool are provided. If no pool is provided,
+ the infos of the volumes of all pools are output.
+
+ :param pool: libvirt storage pool name
+ :param volume: name of the volume to get infos from
+ :param connection: libvirt connection URI, overriding defaults
+ :param username: username to connect with, overriding defaults
+ :param password: password to connect with, overriding defaults
+
+ .. versionadded:: Neon
+
+ CLI Example:
+
+ .. code-block:: bash
+
+ salt "*" virt.volume_infos <pool> <volume>
+ '''
+ result = {}
+ conn = __get_conn(**kwargs)
+ try:
+ backing_stores = _get_all_volumes_paths(conn)
+ disks = {domain.name():
+ {node.get('file') for node
+ in ElementTree.fromstring(domain.XMLDesc(0)).findall('.//disk/source/[@file]')}
+ for domain in _get_domain(conn)}
+
+ def _volume_extract_infos(vol):
+ '''
+ Format the volume info dictionary
+
+ :param vol: the libvirt storage volume object.
+ '''
+ types = ['file', 'block', 'dir', 'network', 'netdir', 'ploop']
+ infos = vol.info()
+
+ # If we have a path, check its use.
+ used_by = []
+ if vol.path():
+ as_backing_store = {path for (path, all_paths) in backing_stores.items() if vol.path() in all_paths}
+ used_by = [vm_name for (vm_name, vm_disks) in disks.items()
+ if vm_disks & as_backing_store or vol.path() in vm_disks]
+
+ return {
+ 'type': types[infos[0]] if infos[0] < len(types) else 'unknown',
+ 'key': vol.key(),
+ 'path': vol.path(),
+ 'capacity': infos[1],
+ 'allocation': infos[2],
+ 'used_by': used_by,
+ }
+
+ pools = [obj for obj in conn.listAllStoragePools() if pool is None or obj.name() == pool]
+ vols = {pool_obj.name(): {vol.name(): _volume_extract_infos(vol)
+ for vol in pool_obj.listAllVolumes()
+ if volume is None or vol.name() == volume}
+ for pool_obj in pools}
+ return {pool_name: volumes for (pool_name, volumes) in vols.items() if volumes}
+ except libvirt.libvirtError as err:
+ log.debug('Silenced libvirt error: %s', str(err))
+ finally:
+ conn.close()
+ return result
+
+
+def volume_delete(pool, volume, **kwargs):
+ '''
+ Delete a libvirt managed volume.
+
+ :param pool: libvirt storage pool name
+ :param volume: name of the volume to delete
+ :param connection: libvirt connection URI, overriding defaults
+ :param username: username to connect with, overriding defaults
+ :param password: password to connect with, overriding defaults
+
+ .. versionadded:: Neon
+
+ CLI Example:
+
+ .. code-block:: bash
+
+ salt "*" virt.volume_delete <pool> <volume>
+ '''
+ conn = __get_conn(**kwargs)
+ try:
+ vol = _get_storage_vol(conn, pool, volume)
+ return not bool(vol.delete())
+ finally:
+ conn.close()
diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py
index bd34962a6a..55005f1d04 100644
--- a/tests/unit/modules/test_virt.py
+++ b/tests/unit/modules/test_virt.py
@@ -2698,3 +2698,187 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin):
self.mock_conn.storagePoolLookupByName.return_value = mock_pool
# pylint: enable=no-member
self.assertEqual(names, virt.pool_list_volumes('default'))
+
+ def test_volume_infos(self):
+ '''
+ Test virt.volume_infos
+ '''
+ vms_disks = [
+ '''
+ <disk type='file' device='disk'>
+ <driver name='qemu' type='qcow2'/>
+ <source file='/path/to/vol0.qcow2'/>
+ <target dev='vda' bus='virtio'/>
+ </disk>
+ ''',
+ '''
+ <disk type='file' device='disk'>
+ <driver name='qemu' type='qcow2'/>
+ <source file='/path/to/vol3.qcow2'/>
+ <target dev='vda' bus='virtio'/>
+ </disk>
+ ''',
+ '''
+ <disk type='file' device='disk'>
+ <driver name='qemu' type='qcow2'/>
+ <source file='/path/to/vol2.qcow2'/>
+ <target dev='vda' bus='virtio'/>
+ </disk>
+ '''
+ ]
+ mock_vms = []
+ for idx, disk in enumerate(vms_disks):
+ vm = MagicMock()
+ # pylint: disable=no-member
+ vm.name.return_value = 'vm{0}'.format(idx)
+ vm.XMLDesc.return_value = '''
+ <domain type='kvm' id='1'>
+ <name>vm{0}</name>
+ <devices>{1}</devices>
+ </domain>
+ '''.format(idx, disk)
+ # pylint: enable=no-member
+ mock_vms.append(vm)
+
+ mock_pool_data = [
+ {
+ 'name': 'pool0',
+ 'volumes': [
+ {
+ 'key': '/key/of/vol0',
+ 'name': 'vol0',
+ 'path': '/path/to/vol0.qcow2',
+ 'info': [0, 123456789, 123456],
+ 'backingStore': None
+ }
+ ]
+ },
+ {
+ 'name': 'pool1',
+ 'volumes': [
+ {
+ 'key': '/key/of/vol1',
+ 'name': 'vol1',
+ 'path': '/path/to/vol1.qcow2',
+ 'info': [0, 12345, 1234],
+ 'backingStore': None
+ },
+ {
+ 'key': '/key/of/vol2',
+ 'name': 'vol2',
+ 'path': '/path/to/vol2.qcow2',
+ 'info': [0, 12345, 1234],
+ 'backingStore': '/path/to/vol0.qcow2'
+ },
+ ],
+ }
+ ]
+ mock_pools = []
+ for pool_data in mock_pool_data:
+ mock_pool = MagicMock()
+ mock_pool.name.return_value = pool_data['name'] # pylint: disable=no-member
+ mock_volumes = []
+ for vol_data in pool_data['volumes']:
+ mock_volume = MagicMock()
+ # pylint: disable=no-member
+ mock_volume.name.return_value = vol_data['name']
+ mock_volume.key.return_value = vol_data['key']
+ mock_volume.path.return_value = '/path/to/{0}.qcow2'.format(vol_data['name'])
+ mock_volume.info.return_value = vol_data['info']
+ backing_store = '''
+ <backingStore>
+ <format>qcow2</format>
+ <path>{0}</path>
+ </backingStore>
+ '''.format(vol_data['backingStore']) if vol_data['backingStore'] else '<backingStore/>'
+ mock_volume.XMLDesc.return_value = '''
+ <volume type='file'>
+ <name>{0}</name>
+ <target>
+ <format>qcow2</format>
+ <path>/path/to/{0}.qcow2</path>
+ </target>
+ {1}
+ </volume>
+ '''.format(vol_data['name'], backing_store)
+ mock_volumes.append(mock_volume)
+ # pylint: enable=no-member
+ mock_pool.listAllVolumes.return_value = mock_volumes # pylint: disable=no-member
+ mock_pools.append(mock_pool)
+
+ self.mock_conn.listAllStoragePools.return_value = mock_pools # pylint: disable=no-member
+
+ with patch('salt.modules.virt._get_domain', MagicMock(return_value=mock_vms)):
+ actual = virt.volume_infos('pool0', 'vol0')
+ self.assertEqual(1, len(actual.keys()))
+ self.assertEqual(1, len(actual['pool0'].keys()))
+ self.assertEqual(['vm0', 'vm2'], sorted(actual['pool0']['vol0']['used_by']))
+ self.assertEqual('/path/to/vol0.qcow2', actual['pool0']['vol0']['path'])
+ self.assertEqual('file', actual['pool0']['vol0']['type'])
+ self.assertEqual('/key/of/vol0', actual['pool0']['vol0']['key'])
+ self.assertEqual(123456789, actual['pool0']['vol0']['capacity'])
+ self.assertEqual(123456, actual['pool0']['vol0']['allocation'])
+
+ self.assertEqual(virt.volume_infos('pool1', None), {
+ 'pool1': {
+ 'vol1': {
+ 'type': 'file',
+ 'key': '/key/of/vol1',
+ 'path': '/path/to/vol1.qcow2',
+ 'capacity': 12345,
+ 'allocation': 1234,
+ 'used_by': [],
+ },
+ 'vol2': {
+ 'type': 'file',
+ 'key': '/key/of/vol2',
+ 'path': '/path/to/vol2.qcow2',
+ 'capacity': 12345,
+ 'allocation': 1234,
+ 'used_by': ['vm2'],
+ }
+ }
+ })
+
+ self.assertEqual(virt.volume_infos(None, 'vol2'), {
+ 'pool1': {
+ 'vol2': {
+ 'type': 'file',
+ 'key': '/key/of/vol2',
+ 'path': '/path/to/vol2.qcow2',
+ 'capacity': 12345,
+ 'allocation': 1234,
+ 'used_by': ['vm2'],
+ }
+ }
+ })
+
+ def test_volume_delete(self):
+ '''
+ Test virt.volume_delete
+ '''
+ mock_delete = MagicMock(side_effect=[0, 1])
+ mock_volume = MagicMock()
+ mock_volume.delete = mock_delete # pylint: disable=no-member
+ mock_pool = MagicMock()
+ # pylint: disable=no-member
+ mock_pool.storageVolLookupByName.side_effect = [
+ mock_volume,
+ mock_volume,
+ self.mock_libvirt.libvirtError("Missing volume"),
+ mock_volume,
+ ]
+ self.mock_conn.storagePoolLookupByName.side_effect = [
+ mock_pool,
+ mock_pool,
+ mock_pool,
+ self.mock_libvirt.libvirtError("Missing pool"),
+ ]
+
+ # pylint: enable=no-member
+ self.assertTrue(virt.volume_delete('default', 'test_volume'))
+ self.assertFalse(virt.volume_delete('default', 'test_volume'))
+ with self.assertRaises(self.mock_libvirt.libvirtError):
+ virt.volume_delete('default', 'missing')
+ virt.volume_delete('missing', 'test_volume')
+ self.assertEqual(mock_delete.call_count, 2)
--
2.20.1