From 78506561571df5b62ad469fab914cb8fdcab1b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Bosdonnat?= Date: Mon, 18 Nov 2019 15:17:41 +0100 Subject: [PATCH] Add virt.pool_capabilities function Not all storage backends are supported for a given libvirt hypervisor. For the user to know what is supported and what is not expose the recently added libvirt function providing pool capabilities. For older libvirt version, let's craft data that are reasonable by adding a computed flag to warn these data may not be 100% accurate. --- salt/modules/virt.py | 158 ++++++++++++++++++++++++++++++++ tests/unit/modules/test_virt.py | 120 ++++++++++++++++++++++++ 2 files changed, 278 insertions(+) diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 9ccc8601d046..032efa5d6811 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -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, diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index 82cf26e15a3e..5b0c303d9753 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -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 = ''' + + + + + + unknown + dos + dvh + + + + + + none + linux + + + + + + + + + + + + + + + + ''' + 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'])