diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 8f1426827374..9ccc8601d046 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -74,6 +74,7 @@ # Import python libs from __future__ import absolute_import, print_function, unicode_literals +import base64 import copy import os import re @@ -683,24 +684,31 @@ def _gen_pool_xml(name, source_hosts=None, source_auth=None, source_name=None, - source_format=None): + source_format=None, + source_initiator=None): ''' Generate the XML string to define a libvirt storage pool ''' hosts = [host.split(':') for host in source_hosts or []] - context = { - 'name': name, - 'ptype': ptype, - 'target': {'path': target, 'permissions': permissions}, - 'source': { + source = None + if any([source_devices, source_dir, source_adapter, hosts, source_auth, source_name, source_format, + source_initiator]): + source = { 'devices': source_devices or [], - 'dir': source_dir, + 'dir': source_dir if source_format != 'cifs' or not source_dir else source_dir.lstrip('/'), 'adapter': source_adapter, 'hosts': [{'name': host[0], 'port': host[1] if len(host) > 1 else None} for host in hosts], 'auth': source_auth, 'name': source_name, - 'format': source_format + 'format': source_format, + 'initiator': source_initiator, } + + context = { + 'name': name, + 'ptype': ptype, + 'target': {'path': target, 'permissions': permissions}, + 'source': source } fn_ = 'libvirt_pool.jinja' try: @@ -711,6 +719,24 @@ def _gen_pool_xml(name, return template.render(**context) +def _gen_secret_xml(auth_type, usage, description): + ''' + Generate a libvirt secret definition XML + ''' + context = { + 'type': auth_type, + 'usage': usage, + 'description': description, + } + fn_ = 'libvirt_secret.jinja' + try: + template = JINJA.get_template(fn_) + except jinja2.exceptions.TemplateNotFound: + log.error('Could not load template %s', fn_) + return '' + return template.render(**context) + + def _get_images_dir(): ''' Extract the images dir from the configuration. First attempts to @@ -4503,6 +4529,7 @@ def pool_define(name, permissions=None, source_devices=None, source_dir=None, + source_initiator=None, source_adapter=None, source_hosts=None, source_auth=None, @@ -4533,6 +4560,10 @@ def pool_define(name, :param source_dir: Path to the source directory for pools of type ``dir``, ``netfs`` or ``gluster``. (Default: ``None``) + :param source_initiator: + Initiator IQN for libiscsi-direct pool types. (Default: ``None``) + + .. versionadded:: neon :param source_adapter: SCSI source definition. The value is a dictionary with ``type``, ``name``, ``parent``, ``managed``, ``parent_wwnn``, ``parent_wwpn``, ``parent_fabric_wwn``, ``wwnn``, ``wwpn`` @@ -4564,7 +4595,7 @@ def pool_define(name, 'username': 'admin', 'secret': { 'type': 'uuid', - 'uuid': '2ec115d7-3a88-3ceb-bc12-0ac909a6fd87' + 'value': '2ec115d7-3a88-3ceb-bc12-0ac909a6fd87' } } @@ -4575,10 +4606,14 @@ def pool_define(name, 'username': 'myname', 'secret': { 'type': 'usage', - 'uuid': 'mycluster_myname' + 'value': 'mycluster_myname' } } + Since neon, instead the source authentication can only contain ``username`` + and ``password`` properties. In this case the libvirt secret will be defined and used. + For Ceph authentications a base64 encoded key is expected. + :param source_name: Identifier of name-based sources. :param source_format: @@ -4631,6 +4666,8 @@ def pool_define(name, .. versionadded:: 2019.2.0 ''' conn = __get_conn(**kwargs) + auth = _pool_set_secret(conn, ptype, name, source_auth) + pool_xml = _gen_pool_xml( name, ptype, @@ -4640,9 +4677,10 @@ def pool_define(name, source_dir=source_dir, source_adapter=source_adapter, source_hosts=source_hosts, - source_auth=source_auth, + source_auth=auth, source_name=source_name, - source_format=source_format + source_format=source_format, + source_initiator=source_initiator ) try: if transient: @@ -4660,6 +4698,236 @@ def pool_define(name, return True +def _pool_set_secret(conn, pool_type, pool_name, source_auth, uuid=None, usage=None, test=False): + secret_types = { + 'rbd': 'ceph', + 'iscsi': 'chap', + 'iscsi-direct': 'chap' + } + secret_type = secret_types.get(pool_type) + auth = source_auth + if source_auth and 'username' in source_auth and 'password' in source_auth: + if secret_type: + # Get the previously defined secret if any + secret = None + if usage: + usage_type = libvirt.VIR_SECRET_USAGE_TYPE_CEPH if secret_type == 'ceph' \ + else libvirt.VIR_SECRET_USAGE_TYPE_ISCSI + secret = conn.secretLookupByUsage(usage_type, usage) + elif uuid: + secret = conn.secretLookupByUUIDString(uuid) + + # Create secret if needed + if not secret: + description = 'Passphrase for {} pool created by Salt'.format(pool_name) + if not usage: + usage = 'pool_{}'.format(pool_name) + secret_xml = _gen_secret_xml(secret_type, usage, description) + if not test: + secret = conn.secretDefineXML(secret_xml) + + # Assign the password to it + password = auth['password'] + if pool_type == 'rbd': + # RBD password are already base64-encoded, but libvirt will base64-encode them later + password = base64.b64decode(salt.utils.stringutils.to_bytes(password)) + if not test: + secret.setValue(password) + + # update auth with secret reference + auth['type'] = secret_type + auth['secret'] = { + 'type': 'uuid' if uuid else 'usage', + 'value': uuid if uuid else usage, + } + return auth + + +def pool_update(name, + ptype, + target=None, + permissions=None, + source_devices=None, + source_dir=None, + source_initiator=None, + source_adapter=None, + source_hosts=None, + source_auth=None, + source_name=None, + source_format=None, + test=False, + **kwargs): + ''' + Update a libvirt storage pool if needed. + If called with test=True, this is also reporting whether an update would be performed. + + :param name: Pool name + :param ptype: + Pool type. See `libvirt documentation `_ for the + possible values. + :param target: Pool full path target + :param permissions: + Permissions to set on the target folder. This is mostly used for filesystem-based + pool types. See :ref:`pool-define-permissions` for more details on this structure. + :param source_devices: + List of source devices for pools backed by physical devices. (Default: ``None``) + + Each item in the list is a dictionary with ``path`` and optionally ``part_separator`` + keys. The path is the qualified name for iSCSI devices. + + Report to `this libvirt page `_ + for more informations on the use of ``part_separator`` + :param source_dir: + Path to the source directory for pools of type ``dir``, ``netfs`` or ``gluster``. + (Default: ``None``) + :param source_initiator: + Initiator IQN for libiscsi-direct pool types. (Default: ``None``) + + .. versionadded:: neon + :param source_adapter: + SCSI source definition. The value is a dictionary with ``type``, ``name``, ``parent``, + ``managed``, ``parent_wwnn``, ``parent_wwpn``, ``parent_fabric_wwn``, ``wwnn``, ``wwpn`` + and ``parent_address`` keys. + + The ``parent_address`` value is a dictionary with ``unique_id`` and ``address`` keys. + The address represents a PCI address and is itself a dictionary with ``domain``, ``bus``, + ``slot`` and ``function`` properties. + Report to `this libvirt page `_ + for the meaning and possible values of these properties. + :param source_hosts: + List of source for pools backed by storage from remote servers. Each item is the hostname + optionally followed by the port separated by a colon. (Default: ``None``) + :param source_auth: + Source authentication details. (Default: ``None``) + + The value is a dictionary with ``type``, ``username`` and ``secret`` keys. The type + can be one of ``ceph`` for Ceph RBD or ``chap`` for iSCSI sources. + + The ``secret`` value links to a libvirt secret object. It is a dictionary with + ``type`` and ``value`` keys. The type value can be either ``uuid`` or ``usage``. + + Examples: + + .. code-block:: python + + source_auth={ + 'type': 'ceph', + 'username': 'admin', + 'secret': { + 'type': 'uuid', + 'uuid': '2ec115d7-3a88-3ceb-bc12-0ac909a6fd87' + } + } + + .. code-block:: python + + source_auth={ + 'type': 'chap', + 'username': 'myname', + 'secret': { + 'type': 'usage', + 'uuid': 'mycluster_myname' + } + } + + Since neon, instead the source authentication can only contain ``username`` + and ``password`` properties. In this case the libvirt secret will be defined and used. + For Ceph authentications a base64 encoded key is expected. + + :param source_name: + Identifier of name-based sources. + :param source_format: + String representing the source format. The possible values are depending on the + source type. See `libvirt documentation `_ for + the possible values. + :param test: run in dry-run mode if set to True + :param connection: libvirt connection URI, overriding defaults + :param username: username to connect with, overriding defaults + :param password: password to connect with, overriding defaults + + .. rubric:: Example: + + Local folder pool: + + .. code-block:: bash + + salt '*' virt.pool_update somepool dir target=/srv/mypool \ + permissions="{'mode': '0744' 'ower': 107, 'group': 107 }" + + CIFS backed pool: + + .. code-block:: bash + + salt '*' virt.pool_update myshare netfs source_format=cifs \ + source_dir=samba_share source_hosts="['example.com']" target=/mnt/cifs + + .. versionadded:: neon + ''' + # Get the current definition to compare the two + conn = __get_conn(**kwargs) + needs_update = False + try: + pool = conn.storagePoolLookupByName(name) + old_xml = ElementTree.fromstring(pool.XMLDesc()) + + # If we have username and password in source_auth generate a new secret + # Or change the value of the existing one + secret_node = old_xml.find('source/auth/secret') + usage = secret_node.get('usage') if secret_node is not None else None + uuid = secret_node.get('uuid') if secret_node is not None else None + auth = _pool_set_secret(conn, ptype, name, source_auth, uuid=uuid, usage=usage, test=test) + + # Compute new definition + new_xml = ElementTree.fromstring(_gen_pool_xml( + name, + ptype, + target, + permissions=permissions, + source_devices=source_devices, + source_dir=source_dir, + source_initiator=source_initiator, + source_adapter=source_adapter, + source_hosts=source_hosts, + source_auth=auth, + source_name=source_name, + source_format=source_format + )) + + # Copy over the uuid, capacity, allocation, available elements + elements_to_copy = ['available', 'allocation', 'capacity', 'uuid'] + for to_copy in elements_to_copy: + element = old_xml.find(to_copy) + new_xml.insert(1, element) + + # Filter out spaces and empty elements like since those would mislead the comparison + def visit_xml(node, fn): + fn(node) + for child in node: + visit_xml(child, fn) + + def space_stripper(node): + if node.tail is not None: + node.tail = node.tail.strip(' \t\n') + if node.text is not None: + node.text = node.text.strip(' \t\n') + + visit_xml(old_xml, space_stripper) + visit_xml(new_xml, space_stripper) + + def empty_node_remover(node): + for child in node: + if not child.tail and not child.text and not child.items() and not child: + node.remove(child) + visit_xml(old_xml, empty_node_remover) + + needs_update = ElementTree.tostring(old_xml) != ElementTree.tostring(new_xml) + if needs_update and not test: + conn.storagePoolDefineXML(salt.utils.stringutils.to_str(ElementTree.tostring(new_xml))) + finally: + conn.close() + return needs_update + + def list_pools(**kwargs): ''' List all storage pools. diff --git a/salt/states/virt.py b/salt/states/virt.py index 8504d7609125..fb3980e19610 100644 --- a/salt/states/virt.py +++ b/salt/states/virt.py @@ -763,55 +763,118 @@ def pool_running(name, ''' ret = {'name': name, 'changes': {}, - 'result': True, + 'result': True if not __opts__['test'] else None, 'comment': '' } try: info = __salt__['virt.pool_info'](name, connection=connection, username=username, password=password) + needs_autostart = False if info: - if info[name]['state'] == 'running': - ret['comment'] = 'Pool {0} exists and is running'.format(name) + needs_autostart = info[name]['autostart'] and not autostart or not info[name]['autostart'] and autostart + + # Update can happen for both running and stopped pools + needs_update = __salt__['virt.pool_update'](name, + ptype=ptype, + target=target, + permissions=permissions, + source_devices=(source or {}).get('devices'), + source_dir=(source or {}).get('dir'), + source_initiator=(source or {}).get('initiator'), + source_adapter=(source or {}).get('adapter'), + source_hosts=(source or {}).get('hosts'), + source_auth=(source or {}).get('auth'), + source_name=(source or {}).get('name'), + source_format=(source or {}).get('format'), + test=True, + connection=connection, + username=username, + password=password) + if needs_update: + if not __opts__['test']: + __salt__['virt.pool_update'](name, + ptype=ptype, + target=target, + permissions=permissions, + source_devices=(source or {}).get('devices'), + source_dir=(source or {}).get('dir'), + source_initiator=(source or {}).get('initiator'), + source_adapter=(source or {}).get('adapter'), + source_hosts=(source or {}).get('hosts'), + source_auth=(source or {}).get('auth'), + source_name=(source or {}).get('name'), + source_format=(source or {}).get('format'), + connection=connection, + username=username, + password=password) + + action = "started" + if info[name]['state'] == 'running': + action = "restarted" + if not __opts__['test']: + __salt__['virt.pool_stop'](name, connection=connection, username=username, password=password) + + if not __opts__['test']: + __salt__['virt.pool_build'](name, connection=connection, username=username, password=password) + __salt__['virt.pool_start'](name, connection=connection, username=username, password=password) + + autostart_str = ', autostart flag changed' if needs_autostart else '' + ret['changes'][name] = 'Pool updated, built{0} and {1}'.format(autostart_str, action) + ret['comment'] = 'Pool {0} updated, built{1} and {2}'.format(name, autostart_str, action) + else: - __salt__['virt.pool_start'](name, connection=connection, username=username, password=password) - ret['changes'][name] = 'Pool started' - ret['comment'] = 'Pool {0} started'.format(name) + if info[name]['state'] == 'running': + ret['comment'] = 'Pool {0} unchanged and is running'.format(name) + ret['result'] = True + else: + ret['changes'][name] = 'Pool started' + ret['comment'] = 'Pool {0} started'.format(name) + if not __opts__['test']: + __salt__['virt.pool_start'](name, connection=connection, username=username, password=password) else: - __salt__['virt.pool_define'](name, - ptype=ptype, - target=target, - permissions=permissions, - source_devices=(source or {}).get('devices', None), - source_dir=(source or {}).get('dir', None), - source_adapter=(source or {}).get('adapter', None), - source_hosts=(source or {}).get('hosts', None), - source_auth=(source or {}).get('auth', None), - source_name=(source or {}).get('name', None), - source_format=(source or {}).get('format', None), - transient=transient, - start=False, - connection=connection, - username=username, - password=password) - if autostart: + needs_autostart = autostart + if not __opts__['test']: + __salt__['virt.pool_define'](name, + ptype=ptype, + target=target, + permissions=permissions, + source_devices=(source or {}).get('devices'), + source_dir=(source or {}).get('dir'), + source_initiator=(source or {}).get('initiator'), + source_adapter=(source or {}).get('adapter'), + source_hosts=(source or {}).get('hosts'), + source_auth=(source or {}).get('auth'), + source_name=(source or {}).get('name'), + source_format=(source or {}).get('format'), + transient=transient, + start=False, + connection=connection, + username=username, + password=password) + + __salt__['virt.pool_build'](name, + connection=connection, + username=username, + password=password) + + __salt__['virt.pool_start'](name, + connection=connection, + username=username, + password=password) + if needs_autostart: + ret['changes'][name] = 'Pool defined, started and marked for autostart' + ret['comment'] = 'Pool {0} defined, started and marked for autostart'.format(name) + else: + ret['changes'][name] = 'Pool defined and started' + ret['comment'] = 'Pool {0} defined and started'.format(name) + + if needs_autostart: + if not __opts__['test']: __salt__['virt.pool_set_autostart'](name, state='on' if autostart else 'off', connection=connection, username=username, password=password) - - __salt__['virt.pool_build'](name, - connection=connection, - username=username, - password=password) - - __salt__['virt.pool_start'](name, - connection=connection, - username=username, - password=password) - - ret['changes'][name] = 'Pool defined and started' - ret['comment'] = 'Pool {0} defined and started'.format(name) except libvirt.libvirtError as err: ret['comment'] = err.get_error_message() ret['result'] = False diff --git a/salt/templates/virt/libvirt_pool.jinja b/salt/templates/virt/libvirt_pool.jinja index 58c82f717788..515702cf46a4 100644 --- a/salt/templates/virt/libvirt_pool.jinja +++ b/salt/templates/virt/libvirt_pool.jinja @@ -18,7 +18,7 @@ {% endif %} {% if source %} - {% if ptype in ['fs', 'logical', 'disk', 'iscsi', 'zfs', 'vstorage'] %} + {% if ptype in ['fs', 'logical', 'disk', 'iscsi', 'zfs', 'vstorage', 'iscsi-direct'] %} {% for device in source.devices %} @@ -43,14 +43,14 @@ {% endif %} {% endif %} - {% if ptype in ['netfs', 'iscsi', 'rbd', 'sheepdog', 'gluster'] and source.hosts %} + {% if ptype in ['netfs', 'iscsi', 'rbd', 'sheepdog', 'gluster', 'iscsi-direct'] and source.hosts %} {% for host in source.hosts %} {% endfor %} {% endif %} - {% if ptype in ['iscsi', 'rbd'] and source.auth %} + {% if ptype in ['iscsi', 'rbd', 'iscsi-direct'] and source.auth %} - + {% endif %} {% if ptype in ['logical', 'rbd', 'sheepdog', 'gluster'] and source.name %} @@ -59,6 +59,11 @@ {% if ptype in ['fs', 'netfs', 'logical', 'disk'] and source.format %} {% endif %} + {% if ptype == 'iscsi-direct' and source.initiator %} + + + + {% endif %} {% endif %} diff --git a/salt/templates/virt/libvirt_secret.jinja b/salt/templates/virt/libvirt_secret.jinja new file mode 100644 index 000000000000..41c6dd811a1b --- /dev/null +++ b/salt/templates/virt/libvirt_secret.jinja @@ -0,0 +1,12 @@ + + {{ description }} + {% if type == 'chap' %} + + {{ usage }} + + {% elif type == 'ceph' %} + + {{ usage }} + + {% endif %} + diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index 333df1d23a49..82cf26e15a3e 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -2335,7 +2335,6 @@ def test_pool_with_rbd(self): self.assertEqual(root.findall('source/host')[1].attrib['port'], '69') self.assertEqual(root.find('source/auth').attrib['type'], 'ceph') self.assertEqual(root.find('source/auth').attrib['username'], 'admin') - self.assertEqual(root.find('source/auth/secret').attrib['type'], 'uuid') self.assertEqual(root.find('source/auth/secret').attrib['uuid'], 'someuuid') def test_pool_with_netfs(self): @@ -2373,6 +2372,114 @@ def test_pool_with_netfs(self): self.assertEqual(root.find('source/host').attrib['name'], 'nfs.host') self.assertEqual(root.find('source/auth'), None) + def test_pool_with_iscsi_direct(self): + ''' + Test virt._gen_pool_xml() with a iscsi-direct source + ''' + xml_data = virt._gen_pool_xml('pool', + 'iscsi-direct', + source_hosts=['iscsi.example.com'], + source_devices=[{'path': 'iqn.2013-06.com.example:iscsi-pool'}], + source_initiator='iqn.2013-06.com.example:iscsi-initiator') + root = ET.fromstring(xml_data) + self.assertEqual(root.find('name').text, 'pool') + self.assertEqual(root.attrib['type'], 'iscsi-direct') + self.assertEqual(root.find('target'), None) + self.assertEqual(root.find('source/device').attrib['path'], 'iqn.2013-06.com.example:iscsi-pool') + self.assertEqual(root.findall('source/host')[0].attrib['name'], 'iscsi.example.com') + self.assertEqual(root.find('source/initiator/iqn').attrib['name'], 'iqn.2013-06.com.example:iscsi-initiator') + + def test_pool_define(self): + ''' + Test virt.pool_define() + ''' + mock_pool = MagicMock() + mock_secret = MagicMock() + mock_secret_define = MagicMock(return_value=mock_secret) + self.mock_conn.secretDefineXML = mock_secret_define + self.mock_conn.storagePoolCreateXML = MagicMock(return_value=mock_pool) + self.mock_conn.storagePoolDefineXML = MagicMock(return_value=mock_pool) + + mocks = [mock_pool, mock_secret, mock_secret_define, self.mock_conn.storagePoolCreateXML, + self.mock_conn.secretDefineXML, self.mock_conn.storagePoolDefineXML] + + # Test case with already defined secret and permanent pool + self.assertTrue(virt.pool_define('default', + 'rbd', + source_hosts=['one.example.com', 'two.example.com'], + source_name='rbdvol', + source_auth={ + 'type': 'ceph', + 'username': 'admin', + 'secret': { + 'type': 'uuid', + 'value': 'someuuid' + } + })) + self.mock_conn.storagePoolDefineXML.assert_called_once() + self.mock_conn.storagePoolCreateXML.assert_not_called() + mock_pool.create.assert_called_once() + mock_secret_define.assert_not_called() + + # Test case with Ceph secret to be defined and transient pool + for mock in mocks: + mock.reset_mock() + self.assertTrue(virt.pool_define('default', + 'rbd', + transient=True, + source_hosts=['one.example.com', 'two.example.com'], + source_name='rbdvol', + source_auth={ + 'username': 'admin', + 'password': 'c2VjcmV0' + })) + self.mock_conn.storagePoolDefineXML.assert_not_called() + + pool_xml = self.mock_conn.storagePoolCreateXML.call_args[0][0] + root = ET.fromstring(pool_xml) + self.assertEqual(root.find('source/auth').attrib['type'], 'ceph') + self.assertEqual(root.find('source/auth').attrib['username'], 'admin') + self.assertEqual(root.find('source/auth/secret').attrib['usage'], 'pool_default') + mock_pool.create.assert_not_called() + mock_secret.setValue.assert_called_once_with(b'secret') + + secret_xml = mock_secret_define.call_args[0][0] + root = ET.fromstring(secret_xml) + self.assertEqual(root.find('usage/name').text, 'pool_default') + self.assertEqual(root.find('usage').attrib['type'], 'ceph') + self.assertEqual(root.attrib['private'], 'yes') + self.assertEqual(root.find('description').text, 'Passphrase for default pool created by Salt') + + # Test case with iscsi secret not starting + for mock in mocks: + mock.reset_mock() + self.assertTrue(virt.pool_define('default', + 'iscsi', + target='/dev/disk/by-path', + source_hosts=['iscsi.example.com'], + source_devices=[{'path': 'iqn.2013-06.com.example:iscsi-pool'}], + source_auth={ + 'username': 'admin', + 'password': 'secret' + }, + start=False)) + self.mock_conn.storagePoolCreateXML.assert_not_called() + + pool_xml = self.mock_conn.storagePoolDefineXML.call_args[0][0] + root = ET.fromstring(pool_xml) + self.assertEqual(root.find('source/auth').attrib['type'], 'chap') + self.assertEqual(root.find('source/auth').attrib['username'], 'admin') + self.assertEqual(root.find('source/auth/secret').attrib['usage'], 'pool_default') + mock_pool.create.assert_not_called() + mock_secret.setValue.assert_called_once_with('secret') + + secret_xml = mock_secret_define.call_args[0][0] + root = ET.fromstring(secret_xml) + self.assertEqual(root.find('usage/target').text, 'pool_default') + self.assertEqual(root.find('usage').attrib['type'], 'iscsi') + self.assertEqual(root.attrib['private'], 'yes') + self.assertEqual(root.find('description').text, 'Passphrase for default pool created by Salt') + def test_list_pools(self): ''' Test virt.list_pools() @@ -2711,3 +2818,203 @@ def test_full_info(self): self.assertEqual('vnc', graphics['type']) self.assertEqual('5900', graphics['port']) self.assertEqual('0.0.0.0', graphics['listen']) + + def test_pool_update(self): + ''' + Test the pool_update function + ''' + current_xml = ''' + default + 20fbe05c-ab40-418a-9afa-136d512f0ede + 1999421108224 + 713207042048 + 1286214066176 + + + + /path/to/pool + + 0775 + 0 + 100 + + + ''' + + expected_xml = '' \ + 'default' \ + '20fbe05c-ab40-418a-9afa-136d512f0ede' \ + '1999421108224' \ + '713207042048' \ + '1286214066176' \ + '' \ + '/mnt/cifs' \ + '' \ + '0774' \ + '1234' \ + '123' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' + + mocked_pool = MagicMock() + mocked_pool.XMLDesc = MagicMock(return_value=current_xml) + self.mock_conn.storagePoolLookupByName = MagicMock(return_value=mocked_pool) + self.mock_conn.storagePoolDefineXML = MagicMock() + + self.assertTrue( + virt.pool_update('default', + 'netfs', + target='/mnt/cifs', + permissions={'mode': '0774', 'owner': '1234', 'group': '123'}, + source_format='cifs', + source_dir='samba_share', + source_hosts=['one.example.com', 'two.example.com'])) + self.mock_conn.storagePoolDefineXML.assert_called_once_with(expected_xml) + + def test_pool_update_nochange(self): + ''' + Test the pool_update function when no change is needed + ''' + + current_xml = ''' + default + 20fbe05c-ab40-418a-9afa-136d512f0ede + 1999421108224 + 713207042048 + 1286214066176 + + + + /path/to/pool + + 0775 + 0 + 100 + + + ''' + + mocked_pool = MagicMock() + mocked_pool.XMLDesc = MagicMock(return_value=current_xml) + self.mock_conn.storagePoolLookupByName = MagicMock(return_value=mocked_pool) + self.mock_conn.storagePoolDefineXML = MagicMock() + + self.assertFalse( + virt.pool_update('default', + 'dir', + target='/path/to/pool', + permissions={'mode': '0775', 'owner': '0', 'group': '100'}, + test=True)) + self.mock_conn.storagePoolDefineXML.assert_not_called() + + def test_pool_update_password(self): + ''' + Test the pool_update function, where the password only is changed + ''' + current_xml = ''' + default + 20fbe05c-ab40-418a-9afa-136d512f0ede + 1999421108224 + 713207042048 + 1286214066176 + + iscsi-images + + + + + + + ''' + + expected_xml = '' \ + 'default' \ + '20fbe05c-ab40-418a-9afa-136d512f0ede' \ + '1999421108224' \ + '713207042048' \ + '1286214066176' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + 'iscsi-images' \ + '' \ + '' + + mock_secret = MagicMock() + self.mock_conn.secretLookupByUUIDString = MagicMock(return_value=mock_secret) + + mocked_pool = MagicMock() + mocked_pool.XMLDesc = MagicMock(return_value=current_xml) + self.mock_conn.storagePoolLookupByName = MagicMock(return_value=mocked_pool) + self.mock_conn.storagePoolDefineXML = MagicMock() + + self.assertTrue( + virt.pool_update('default', + 'rbd', + source_name='iscsi-images', + source_hosts=['ses4.tf.local', 'ses5.tf.local'], + source_auth={'username': 'libvirt', + 'password': 'c2VjcmV0'})) + self.mock_conn.storagePoolDefineXML.assert_called_once_with(expected_xml) + mock_secret.setValue.assert_called_once_with(b'secret') + + def test_pool_update_password_create(self): + ''' + Test the pool_update function, where the password only is changed + ''' + current_xml = ''' + default + 20fbe05c-ab40-418a-9afa-136d512f0ede + 1999421108224 + 713207042048 + 1286214066176 + + iscsi-images + + + + ''' + + expected_xml = '' \ + 'default' \ + '20fbe05c-ab40-418a-9afa-136d512f0ede' \ + '1999421108224' \ + '713207042048' \ + '1286214066176' \ + '' \ + '' \ + '' \ + '' \ + '' \ + '' \ + 'iscsi-images' \ + '' \ + '' + + mock_secret = MagicMock() + self.mock_conn.secretDefineXML = MagicMock(return_value=mock_secret) + + mocked_pool = MagicMock() + mocked_pool.XMLDesc = MagicMock(return_value=current_xml) + self.mock_conn.storagePoolLookupByName = MagicMock(return_value=mocked_pool) + self.mock_conn.storagePoolDefineXML = MagicMock() + + self.assertTrue( + virt.pool_update('default', + 'rbd', + source_name='iscsi-images', + source_hosts=['ses4.tf.local', 'ses5.tf.local'], + source_auth={'username': 'libvirt', + 'password': 'c2VjcmV0'})) + self.mock_conn.storagePoolDefineXML.assert_called_once_with(expected_xml) + mock_secret.setValue.assert_called_once_with(b'secret') diff --git a/tests/unit/states/test_virt.py b/tests/unit/states/test_virt.py index b68d0b4d908b..8712e2e204df 100644 --- a/tests/unit/states/test_virt.py +++ b/tests/unit/states/test_virt.py @@ -648,95 +648,219 @@ def test_pool_running(self): pool_running state test cases. ''' ret = {'name': 'mypool', 'changes': {}, 'result': True, 'comment': ''} - mocks = {mock: MagicMock(return_value=True) for mock in ['define', 'autostart', 'build', 'start']} - with patch.dict(virt.__salt__, { # pylint: disable=no-member - 'virt.pool_info': MagicMock(return_value={}), - 'virt.pool_define': mocks['define'], - 'virt.pool_build': mocks['build'], - 'virt.pool_start': mocks['start'], - 'virt.pool_set_autostart': mocks['autostart'] - }): - ret.update({'changes': {'mypool': 'Pool defined and started'}, - 'comment': 'Pool mypool defined and started'}) - self.assertDictEqual(virt.pool_running('mypool', + mocks = {mock: MagicMock(return_value=True) for mock in ['define', 'autostart', 'build', 'start', 'stop']} + with patch.dict(virt.__opts__, {'test': False}): + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={}), + 'virt.pool_define': mocks['define'], + 'virt.pool_build': mocks['build'], + 'virt.pool_start': mocks['start'], + 'virt.pool_set_autostart': mocks['autostart'] + }): + ret.update({'changes': {'mypool': 'Pool defined, started and marked for autostart'}, + 'comment': 'Pool mypool defined, started and marked for autostart'}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + permissions={'mode': '0770', + 'owner': 1000, + 'group': 100, + 'label': 'seclabel'}, + source={'devices': [{'path': '/dev/sda'}]}, + transient=True, + autostart=True, + connection='myconnection', + username='user', + password='secret'), ret) + mocks['define'].assert_called_with('mypool', ptype='logical', target='/dev/base', permissions={'mode': '0770', 'owner': 1000, 'group': 100, 'label': 'seclabel'}, - source={'devices': [{'path': '/dev/sda'}]}, + source_devices=[{'path': '/dev/sda'}], + source_dir=None, + source_adapter=None, + source_hosts=None, + source_auth=None, + source_name=None, + source_format=None, + source_initiator=None, transient=True, - autostart=True, + start=False, connection='myconnection', username='user', - password='secret'), ret) - mocks['define'].assert_called_with('mypool', - ptype='logical', - target='/dev/base', - permissions={'mode': '0770', - 'owner': 1000, - 'group': 100, - 'label': 'seclabel'}, - source_devices=[{'path': '/dev/sda'}], - source_dir=None, - source_adapter=None, - source_hosts=None, - source_auth=None, - source_name=None, - source_format=None, - transient=True, - start=False, - connection='myconnection', - username='user', - password='secret') - mocks['autostart'].assert_called_with('mypool', - state='on', + password='secret') + mocks['autostart'].assert_called_with('mypool', + state='on', + connection='myconnection', + username='user', + password='secret') + mocks['build'].assert_called_with('mypool', + connection='myconnection', + username='user', + password='secret') + mocks['start'].assert_called_with('mypool', connection='myconnection', username='user', password='secret') - mocks['build'].assert_called_with('mypool', - connection='myconnection', - username='user', - password='secret') - mocks['start'].assert_called_with('mypool', - connection='myconnection', - username='user', - password='secret') - - with patch.dict(virt.__salt__, { # pylint: disable=no-member - 'virt.pool_info': MagicMock(return_value={'mypool': {'state': 'running'}}), - }): - ret.update({'changes': {}, 'comment': 'Pool mypool exists and is running'}) - self.assertDictEqual(virt.pool_running('mypool', - ptype='logical', - target='/dev/base', - source={'devices': [{'path': '/dev/sda'}]}), ret) - for mock in mocks: - mocks[mock].reset_mock() - with patch.dict(virt.__salt__, { # pylint: disable=no-member - 'virt.pool_info': MagicMock(return_value={'mypool': {'state': 'stopped'}}), - 'virt.pool_build': mocks['build'], - 'virt.pool_start': mocks['start'] - }): - ret.update({'changes': {'mypool': 'Pool started'}, 'comment': 'Pool mypool started'}) - self.assertDictEqual(virt.pool_running('mypool', + mocks['update'] = MagicMock(return_value=False) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={'mypool': {'state': 'running', 'autostart': True}}), + 'virt.pool_update': MagicMock(return_value=False), + }): + ret.update({'changes': {}, 'comment': 'Pool mypool unchanged and is running'}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + source={'devices': [{'path': '/dev/sda'}]}), ret) + + for mock in mocks: + mocks[mock].reset_mock() + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={'mypool': {'state': 'stopped', 'autostart': True}}), + 'virt.pool_update': mocks['update'], + 'virt.pool_build': mocks['build'], + 'virt.pool_start': mocks['start'] + }): + ret.update({'changes': {'mypool': 'Pool started'}, 'comment': 'Pool mypool started'}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + source={'devices': [{'path': '/dev/sda'}]}), ret) + mocks['start'].assert_called_with('mypool', connection=None, username=None, password=None) + mocks['build'].assert_not_called() + + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={}), + 'virt.pool_define': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {}, 'comment': 'Some error', 'result': False}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + source={'devices': [{'path': '/dev/sda'}]}), ret) + + # Test case with update and autostart change on stopped pool + for mock in mocks: + mocks[mock].reset_mock() + mocks['update'] = MagicMock(return_value=True) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={'mypool': {'state': 'stopped', 'autostart': True}}), + 'virt.pool_update': mocks['update'], + 'virt.pool_set_autostart': mocks['autostart'], + 'virt.pool_build': mocks['build'], + 'virt.pool_start': mocks['start'] + }): + ret.update({'changes': {'mypool': 'Pool updated, built, autostart flag changed and started'}, + 'comment': 'Pool mypool updated, built, autostart flag changed and started', + 'result': True}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + autostart=False, + permissions={'mode': '0770', + 'owner': 1000, + 'group': 100, + 'label': 'seclabel'}, + source={'devices': [{'path': '/dev/sda'}]}), ret) + mocks['start'].assert_called_with('mypool', connection=None, username=None, password=None) + mocks['build'].assert_called_with('mypool', connection=None, username=None, password=None) + mocks['autostart'].assert_called_with('mypool', state='off', + connection=None, username=None, password=None) + mocks['update'].assert_called_with('mypool', ptype='logical', target='/dev/base', - source={'devices': [{'path': '/dev/sda'}]}), ret) - mocks['start'].assert_called_with('mypool', connection=None, username=None, password=None) - mocks['build'].assert_not_called() - - with patch.dict(virt.__salt__, { # pylint: disable=no-member - 'virt.pool_info': MagicMock(return_value={}), - 'virt.pool_define': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) - }): - ret.update({'changes': {}, 'comment': 'Some error', 'result': False}) - self.assertDictEqual(virt.pool_running('mypool', + permissions={'mode': '0770', + 'owner': 1000, + 'group': 100, + 'label': 'seclabel'}, + source_devices=[{'path': '/dev/sda'}], + source_dir=None, + source_adapter=None, + source_hosts=None, + source_auth=None, + source_name=None, + source_format=None, + source_initiator=None, + connection=None, + username=None, + password=None) + + # test case with update and no autostart change on running pool + for mock in mocks: + mocks[mock].reset_mock() + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={'mypool': {'state': 'running', 'autostart': False}}), + 'virt.pool_update': mocks['update'], + 'virt.pool_build': mocks['build'], + 'virt.pool_start': mocks['start'], + 'virt.pool_stop': mocks['stop'] + }): + ret.update({'changes': {'mypool': 'Pool updated, built and restarted'}, + 'comment': 'Pool mypool updated, built and restarted', + 'result': True}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + autostart=False, + permissions={'mode': '0770', + 'owner': 1000, + 'group': 100, + 'label': 'seclabel'}, + source={'devices': [{'path': '/dev/sda'}]}), ret) + mocks['stop'].assert_called_with('mypool', connection=None, username=None, password=None) + mocks['start'].assert_called_with('mypool', connection=None, username=None, password=None) + mocks['build'].assert_called_with('mypool', connection=None, username=None, password=None) + mocks['update'].assert_called_with('mypool', ptype='logical', target='/dev/base', - source={'devices': [{'path': '/dev/sda'}]}), ret) + permissions={'mode': '0770', + 'owner': 1000, + 'group': 100, + 'label': 'seclabel'}, + source_devices=[{'path': '/dev/sda'}], + source_dir=None, + source_adapter=None, + source_hosts=None, + source_auth=None, + source_name=None, + source_format=None, + source_initiator=None, + connection=None, + username=None, + password=None) + + with patch.dict(virt.__opts__, {'test': True}): + # test case with test=True and no change + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={'mypool': {'state': 'running', 'autostart': True}}), + 'virt.pool_update': MagicMock(return_value=False), + }): + ret.update({'changes': {}, 'comment': 'Pool mypool unchanged and is running', + 'result': True}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + source={'devices': [{'path': '/dev/sda'}]}), ret) + + # test case with test=True and started + for mock in mocks: + mocks[mock].reset_mock() + mocks['update'] = MagicMock(return_value=False) + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.pool_info': MagicMock(return_value={'mypool': {'state': 'stopped', 'autostart': True}}), + 'virt.pool_update': mocks['update'] + }): + ret.update({'changes': {'mypool': 'Pool started'}, + 'comment': 'Pool mypool started', + 'result': None}) + self.assertDictEqual(virt.pool_running('mypool', + ptype='logical', + target='/dev/base', + source={'devices': [{'path': '/dev/sda'}]}), ret) def test_pool_deleted(self): '''