51d9b78cf8
OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=156
359 lines
14 KiB
Diff
359 lines
14 KiB
Diff
From 3bb798795f89e1af5132c6a97ddb0cf414912265 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 | 126 ++++++++++++++++++++++++++
|
|
tests/unit/modules/test_virt.py | 195 ++++++++++++++++++++++++++++++++++++++++
|
|
2 files changed, 321 insertions(+)
|
|
|
|
diff --git a/salt/modules/virt.py b/salt/modules/virt.py
|
|
index 195da49296..9bb3636bd1 100644
|
|
--- a/salt/modules/virt.py
|
|
+++ b/salt/modules/virt.py
|
|
@@ -4991,3 +4991,129 @@ 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 _is_valid_volume(vol):
|
|
+ '''
|
|
+ Checks whether a volume is valid for further use since those may have disappeared since
|
|
+ the last pool refresh.
|
|
+ '''
|
|
+ try:
|
|
+ # Getting info on an invalid volume raises error
|
|
+ vol.info()
|
|
+ return True
|
|
+ except libvirt.libvirtError as err:
|
|
+ return False
|
|
+
|
|
+
|
|
+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 if _is_valid_volume(vol)}
|
|
+
|
|
+
|
|
+def volume_infos(pool=None, volume=None, **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 (default: ``None``)
|
|
+ :param volume: name of the volume to get infos from (default: ``None``)
|
|
+ :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) and _is_valid_volume(vol)}
|
|
+ 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 36fcf0d4c0..6f9eea241a 100644
|
|
--- a/tests/unit/modules/test_virt.py
|
|
+++ b/tests/unit/modules/test_virt.py
|
|
@@ -2698,3 +2698,198 @@ 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/vol0bad',
|
|
+ 'name': 'vol0bad',
|
|
+ 'path': '/path/to/vol0bad.qcow2',
|
|
+ 'info': None,
|
|
+ 'backingStore': None
|
|
+ },
|
|
+ {
|
|
+ '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'])
|
|
+ if vol_data['info']:
|
|
+ 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)
|
|
+ else:
|
|
+ mock_volume.info.side_effect = self.mock_libvirt.libvirtError('No such volume')
|
|
+ mock_volume.XMLDesc.side_effect = self.mock_libvirt.libvirtError('No such volume')
|
|
+ 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.16.4
|
|
|
|
|