diff --git a/salt/modules/virt.py b/salt/modules/virt.py index dedcf8cb6f5..88a8994d7d5 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -4546,6 +4546,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 6f594a8ff34..e3b93aae6b8 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -3058,3 +3058,123 @@ def test_get_hypervisor(self, isxen_mock, iskvm_mock): isxen_mock.return_value = True self.assertEqual('xen', virt.get_hypervisor()) + + 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'])