Skip to content

Commit

Permalink
Add virt.volume_infos() and virt.volume_delete()
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cbosdo authored and Mihai Dinca committed Mar 28, 2019
1 parent 0a9bca4 commit 5e20220
Show file tree
Hide file tree
Showing 2 changed files with 321 additions and 0 deletions.
126 changes: 126 additions & 0 deletions salt/modules/virt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4988,3 +4988,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()
195 changes: 195 additions & 0 deletions tests/unit/modules/test_virt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2698,3 +2698,198 @@ def test_pool_list_volumes(self):
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)

0 comments on commit 5e20220

Please sign in to comment.