Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add virt.pool_capabilities function #55346

Merged
merged 2 commits into from
Dec 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions salt/modules/virt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4523,6 +4523,164 @@ def network_set_autostart(name, state='on', **kwargs):
conn.close()


def _parse_pools_caps(doc):
'''
Parse libvirt pool capabilities XML
'''
def _parse_pool_caps(pool):
pool_caps = {
'name': pool.get('type'),
'supported': pool.get('supported', 'no') == 'yes'
}
for option_kind in ['pool', 'vol']:
options = {}
default_format_node = pool.find('{0}Options/defaultFormat'.format(option_kind))
if default_format_node is not None:
options['default_format'] = default_format_node.get('type')
options_enums = {enum.get('name'): [value.text for value in enum.findall('value')]
for enum in pool.findall('{0}Options/enum'.format(option_kind))}
if options_enums:
options.update(options_enums)
if options:
if 'options' not in pool_caps:
pool_caps['options'] = {}
kind = option_kind if option_kind is not 'vol' else 'volume'
pool_caps['options'][kind] = options
return pool_caps

return [_parse_pool_caps(pool) for pool in doc.findall('pool')]


def pool_capabilities(**kwargs):
'''
Return the hypervisor connection storage pool capabilities.

The returned data are either directly extracted from libvirt or computed.
In the latter case some pool types could be listed as supported while they
are not. To distinguish between the two cases, check the value of the ``computed`` property.

: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.pool_capabilities

'''
try:
conn = __get_conn(**kwargs)
has_pool_capabilities = bool(getattr(conn, 'getStoragePoolCapabilities', None))
if has_pool_capabilities:
caps = ElementTree.fromstring(conn.getStoragePoolCapabilities())
pool_types = _parse_pools_caps(caps)
else:
# Compute reasonable values
all_hypervisors = ['xen', 'kvm', 'bhyve']
images_formats = ['none', 'raw', 'dir', 'bochs', 'cloop', 'dmg', 'iso', 'vpc', 'vdi',
'fat', 'vhd', 'ploop', 'cow', 'qcow', 'qcow2', 'qed', 'vmdk']
common_drivers = [
{
'name': 'fs',
'default_source_format': 'auto',
'source_formats': ['auto', 'ext2', 'ext3', 'ext4', 'ufs', 'iso9660', 'udf', 'gfs', 'gfs2',
'vfat', 'hfs+', 'xfs', 'ocfs2'],
'default_target_format': 'raw',
'target_formats': images_formats
},
{
'name': 'dir',
'default_target_format': 'raw',
'target_formats': images_formats
},
{'name': 'iscsi'},
{'name': 'scsi'},
{
'name': 'logical',
'default_source_format': 'lvm2',
'source_formats': ['unknown', 'lvm2'],
},
{
'name': 'netfs',
'default_source_format': 'auto',
'source_formats': ['auto', 'nfs', 'glusterfs', 'cifs'],
'default_target_format': 'raw',
'target_formats': images_formats
},
{
'name': 'disk',
'default_source_format': 'unknown',
'source_formats': ['unknown', 'dos', 'dvh', 'gpt', 'mac', 'bsd', 'pc98', 'sun', 'lvm2'],
'default_target_format': 'none',
'target_formats': ['none', 'linux', 'fat16', 'fat32', 'linux-swap', 'linux-lvm',
'linux-raid', 'extended']
},
{'name': 'mpath'},
{
'name': 'rbd',
'default_target_format': 'raw',
'target_formats': []
},
{
'name': 'sheepdog',
'version': 10000,
'hypervisors': ['kvm'],
'default_target_format': 'raw',
'target_formats': images_formats
},
{
'name': 'gluster',
'version': 1002000,
'hypervisors': ['kvm'],
'default_target_format': 'raw',
'target_formats': images_formats
},
{'name': 'zfs', 'version': 1002008, 'hypervisors': ['bhyve']},
{'name': 'iscsi-direct', 'version': 4007000, 'hypervisors': ['kvm', 'xen']}
]

libvirt_version = conn.getLibVersion()
hypervisor = get_hypervisor()

def _get_backend_output(backend):
output = {
'name': backend['name'],
'supported': (not backend.get('version') or libvirt_version >= backend['version']) and
hypervisor in backend.get('hypervisors', all_hypervisors),
'options': {
'pool': {
'default_format': backend.get('default_source_format'),
'sourceFormatType': backend.get('source_formats')
},
'volume': {
'default_format': backend.get('default_target_format'),
'targetFormatType': backend.get('target_formats')
}
}
}

# Cleanup the empty members to match the libvirt output
for option_kind in ['pool', 'volume']:
if not [value for value in output['options'][option_kind].values() if value is not None]:
del output['options'][option_kind]
if not output['options']:
del output['options']

return output
pool_types = [_get_backend_output(backend) for backend in common_drivers]
finally:
conn.close()

return {
'computed': not has_pool_capabilities,
'pool_types': pool_types,
}


def pool_define(name,
ptype,
target=None,
Expand Down
120 changes: 120 additions & 0 deletions tests/unit/modules/test_virt.py
Original file line number Diff line number Diff line change
Expand Up @@ -3018,3 +3018,123 @@ def test_pool_update_password_create(self):
'password': 'c2VjcmV0'}))
self.mock_conn.storagePoolDefineXML.assert_called_once_with(expected_xml)
mock_secret.setValue.assert_called_once_with(b'secret')

def test_pool_capabilities(self):
'''
Test virt.pool_capabilities where libvirt has the pool-capabilities feature
'''
xml_caps = '''
<storagepoolCapabilities>
<pool type='disk' supported='yes'>
<poolOptions>
<defaultFormat type='unknown'/>
<enum name='sourceFormatType'>
<value>unknown</value>
<value>dos</value>
<value>dvh</value>
</enum>
</poolOptions>
<volOptions>
<defaultFormat type='none'/>
<enum name='targetFormatType'>
<value>none</value>
<value>linux</value>
</enum>
</volOptions>
</pool>
<pool type='iscsi' supported='yes'>
</pool>
<pool type='rbd' supported='yes'>
<volOptions>
<defaultFormat type='raw'/>
<enum name='targetFormatType'>
</enum>
</volOptions>
</pool>
<pool type='sheepdog' supported='no'>
</pool>
</storagepoolCapabilities>
'''
self.mock_conn.getStoragePoolCapabilities = MagicMock(return_value=xml_caps)

actual = virt.pool_capabilities()
self.assertEqual({
'computed': False,
'pool_types': [{
'name': 'disk',
'supported': True,
'options': {
'pool': {
'default_format': 'unknown',
'sourceFormatType': ['unknown', 'dos', 'dvh']
},
'volume': {
'default_format': 'none',
'targetFormatType': ['none', 'linux']
}
}
},
{
'name': 'iscsi',
'supported': True,
},
{
'name': 'rbd',
'supported': True,
'options': {
'volume': {
'default_format': 'raw',
'targetFormatType': []
}
}
},
{
'name': 'sheepdog',
'supported': False,
},
]}, actual)

@patch('salt.modules.virt.get_hypervisor', return_value='kvm')
def test_pool_capabilities_computed(self, mock_get_hypervisor):
'''
Test virt.pool_capabilities where libvirt doesn't have the pool-capabilities feature
'''
self.mock_conn.getLibVersion = MagicMock(return_value=4006000)
del self.mock_conn.getStoragePoolCapabilities

actual = virt.pool_capabilities()

self.assertTrue(actual['computed'])
backends = actual['pool_types']

# libvirt version matching check
self.assertFalse([backend for backend in backends if backend['name'] == 'iscsi-direct'][0]['supported'])
self.assertTrue([backend for backend in backends if backend['name'] == 'gluster'][0]['supported'])
self.assertFalse([backend for backend in backends if backend['name'] == 'zfs'][0]['supported'])

# test case matching other hypervisors
mock_get_hypervisor.return_value = 'xen'
backends = virt.pool_capabilities()['pool_types']
self.assertFalse([backend for backend in backends if backend['name'] == 'gluster'][0]['supported'])

mock_get_hypervisor.return_value = 'bhyve'
backends = virt.pool_capabilities()['pool_types']
self.assertFalse([backend for backend in backends if backend['name'] == 'gluster'][0]['supported'])
self.assertTrue([backend for backend in backends if backend['name'] == 'zfs'][0]['supported'])

# Test options output
self.assertNotIn('options', [backend for backend in backends if backend['name'] == 'iscsi'][0])
self.assertNotIn('pool', [backend for backend in backends if backend['name'] == 'dir'][0]['options'])
self.assertNotIn('volume', [backend for backend in backends if backend['name'] == 'logical'][0]['options'])
self.assertEqual({
'pool': {
'default_format': 'auto',
'sourceFormatType': ['auto', 'nfs', 'glusterfs', 'cifs']
},
'volume': {
'default_format': 'raw',
'targetFormatType': ['none', 'raw', 'dir', 'bochs', 'cloop', 'dmg', 'iso', 'vpc', 'vdi',
'fat', 'vhd', 'ploop', 'cow', 'qcow', 'qcow2', 'qed', 'vmdk']
}
},
[backend for backend in backends if backend['name'] == 'netfs'][0]['options'])