diff --git a/_lastrevision b/_lastrevision index a4a9fc6..75c84a1 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -119d230d13c22207b56ca0276f65a25692e8f4bf \ No newline at end of file +d3f65020201314619013243463c3fe8098529e3e \ No newline at end of file diff --git a/add-virt.network_get_xml-function.patch b/add-virt.network_get_xml-function.patch new file mode 100644 index 0000000..d44e1e4 --- /dev/null +++ b/add-virt.network_get_xml-function.patch @@ -0,0 +1,123 @@ +From a5072a8e834127c9633c1af78631dcef6db0e6cb Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?C=C3=A9dric=20Bosdonnat?= +Date: Mon, 30 Dec 2019 17:28:50 +0100 +Subject: [PATCH] Add virt.network_get_xml function + +Users may want to see the full XML definition of a network. + +Add virt.pool_get_xml function + +Users may want to see the full XML definition of a virtual storage pool. +--- + salt/modules/virt.py | 48 +++++++++++++++++++++++++++++++++++++++++ + tests/unit/modules/test_virt.py | 20 +++++++++++++++++ + 2 files changed, 68 insertions(+) + +diff --git a/salt/modules/virt.py b/salt/modules/virt.py +index 44c7e78ef0..339760ead4 100644 +--- a/salt/modules/virt.py ++++ b/salt/modules/virt.py +@@ -4633,6 +4633,30 @@ def network_info(name=None, **kwargs): + return result + + ++def network_get_xml(name, **kwargs): ++ ''' ++ Return the XML definition of a virtual network ++ ++ :param name: libvirt network name ++ :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.network_get_xml default ++ ''' ++ conn = __get_conn(**kwargs) ++ try: ++ return conn.networkLookupByName(name).XMLDesc() ++ finally: ++ conn.close() ++ ++ + def network_start(name, **kwargs): + ''' + Start a defined virtual network. +@@ -5377,6 +5401,30 @@ def pool_info(name=None, **kwargs): + return result + + ++def pool_get_xml(name, **kwargs): ++ ''' ++ Return the XML definition of a virtual storage pool ++ ++ :param name: libvirt storage pool name ++ :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_get_xml default ++ ''' ++ conn = __get_conn(**kwargs) ++ try: ++ return conn.storagePoolLookupByName(name).XMLDesc() ++ finally: ++ conn.close() ++ ++ + def pool_start(name, **kwargs): + ''' + Start a defined libvirt storage pool. +diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py +index 698e1922fc..719f97a724 100644 +--- a/tests/unit/modules/test_virt.py ++++ b/tests/unit/modules/test_virt.py +@@ -2404,6 +2404,16 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + net = virt.network_info('foo') + self.assertEqual({}, net) + ++ def test_network_get_xml(self): ++ ''' ++ Test virt.network_get_xml ++ ''' ++ network_mock = MagicMock() ++ network_mock.XMLDesc.return_value = 'Raw XML' ++ self.mock_conn.networkLookupByName.return_value = network_mock ++ ++ self.assertEqual('Raw XML', virt.network_get_xml('default')) ++ + def test_pool(self): + ''' + Test virt._gen_pool_xml() +@@ -2806,6 +2816,16 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + } + }, pool) + ++ def test_pool_get_xml(self): ++ ''' ++ Test virt.pool_get_xml ++ ''' ++ pool_mock = MagicMock() ++ pool_mock.XMLDesc.return_value = 'Raw XML' ++ self.mock_conn.storagePoolLookupByName.return_value = pool_mock ++ ++ self.assertEqual('Raw XML', virt.pool_get_xml('default')) ++ + def test_pool_list_volumes(self): + ''' + Test virt.pool_list_volumes +-- +2.16.4 + + diff --git a/fix-virt-states-to-not-fail-on-vms-already-stopped.-.patch b/fix-virt-states-to-not-fail-on-vms-already-stopped.-.patch new file mode 100644 index 0000000..8c4d445 --- /dev/null +++ b/fix-virt-states-to-not-fail-on-vms-already-stopped.-.patch @@ -0,0 +1,217 @@ +From 46aeb732ed61051d436ac748bd95a50c6379682a Mon Sep 17 00:00:00 2001 +From: Cedric Bosdonnat +Date: Mon, 16 Dec 2019 11:27:49 +0100 +Subject: [PATCH] Fix virt states to not fail on VMs already stopped. + (#195) + +The virt.stopped and virt.powered_off states need to do nothing and +not fail if one of the targeted VMs is already in shutdown state. +--- + salt/states/virt.py | 45 ++++++++++++++++++++-------------- + tests/unit/states/test_virt.py | 36 +++++++++++++++++++++++++++ + 2 files changed, 63 insertions(+), 18 deletions(-) + +diff --git a/salt/states/virt.py b/salt/states/virt.py +index 32a9e31ae5..68e9ac6fb6 100644 +--- a/salt/states/virt.py ++++ b/salt/states/virt.py +@@ -145,35 +145,45 @@ def keys(name, basepath='/etc/pki', **kwargs): + return ret + + +-def _virt_call(domain, function, section, comment, ++def _virt_call(domain, function, section, comment, state=None, + connection=None, username=None, password=None, **kwargs): + ''' + Helper to call the virt functions. Wildcards supported. + +- :param domain: +- :param function: +- :param section: +- :param comment: +- :return: ++ :param domain: the domain to apply the function on. Can contain wildcards. ++ :param function: virt function to call ++ :param section: key for the changed domains in the return changes dictionary ++ :param comment: comment to return ++ :param state: the expected final state of the VM. If None the VM state won't be checked. ++ :return: the salt state return + ''' + ret = {'name': domain, 'changes': {}, 'result': True, 'comment': ''} + targeted_domains = fnmatch.filter(__salt__['virt.list_domains'](), domain) + changed_domains = list() + ignored_domains = list() ++ noaction_domains = list() + for targeted_domain in targeted_domains: + try: +- response = __salt__['virt.{0}'.format(function)](targeted_domain, +- connection=connection, +- username=username, +- password=password, +- **kwargs) +- if isinstance(response, dict): +- response = response['name'] +- changed_domains.append({'domain': targeted_domain, function: response}) ++ action_needed = True ++ # If a state has been provided, use it to see if we have something to do ++ if state is not None: ++ domain_state = __salt__['virt.vm_state'](targeted_domain) ++ action_needed = domain_state.get(targeted_domain) != state ++ if action_needed: ++ response = __salt__['virt.{0}'.format(function)](targeted_domain, ++ connection=connection, ++ username=username, ++ password=password, ++ **kwargs) ++ if isinstance(response, dict): ++ response = response['name'] ++ changed_domains.append({'domain': targeted_domain, function: response}) ++ else: ++ noaction_domains.append(targeted_domain) + except libvirt.libvirtError as err: + ignored_domains.append({'domain': targeted_domain, 'issue': six.text_type(err)}) + if not changed_domains: +- ret['result'] = False ++ ret['result'] = not ignored_domains and bool(targeted_domains) + ret['comment'] = 'No changes had happened' + if ignored_domains: + ret['changes'] = {'ignored': ignored_domains} +@@ -206,7 +216,7 @@ def stopped(name, connection=None, username=None, password=None): + virt.stopped + ''' + +- return _virt_call(name, 'shutdown', 'stopped', "Machine has been shut down", ++ return _virt_call(name, 'shutdown', 'stopped', 'Machine has been shut down', state='shutdown', + connection=connection, username=username, password=password) + + +@@ -231,8 +241,7 @@ def powered_off(name, connection=None, username=None, password=None): + domain_name: + virt.stopped + ''' +- +- return _virt_call(name, 'stop', 'unpowered', 'Machine has been powered off', ++ return _virt_call(name, 'stop', 'unpowered', 'Machine has been powered off', state='shutdown', + connection=connection, username=username, password=password) + + +diff --git a/tests/unit/states/test_virt.py b/tests/unit/states/test_virt.py +index 2904fa224d..2af5caca1b 100644 +--- a/tests/unit/states/test_virt.py ++++ b/tests/unit/states/test_virt.py +@@ -378,8 +378,11 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + 'result': True} + + shutdown_mock = MagicMock(return_value=True) ++ ++ # Normal case + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), ++ 'virt.vm_state': MagicMock(return_value={'myvm': 'running'}), + 'virt.shutdown': shutdown_mock + }): + ret.update({'changes': { +@@ -389,8 +392,10 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + self.assertDictEqual(virt.stopped('myvm'), ret) + shutdown_mock.assert_called_with('myvm', connection=None, username=None, password=None) + ++ # Normal case with user-provided connection parameters + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), ++ 'virt.vm_state': MagicMock(return_value={'myvm': 'running'}), + 'virt.shutdown': shutdown_mock, + }): + self.assertDictEqual(virt.stopped('myvm', +@@ -399,8 +404,10 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + password='secret'), ret) + shutdown_mock.assert_called_with('myvm', connection='myconnection', username='user', password='secret') + ++ # Case where an error occurred during the shutdown + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), ++ 'virt.vm_state': MagicMock(return_value={'myvm': 'running'}), + 'virt.shutdown': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {'ignored': [{'domain': 'myvm', 'issue': 'Some error'}]}, +@@ -408,10 +415,21 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.stopped('myvm'), ret) + ++ # Case there the domain doesn't exist + with patch.dict(virt.__salt__, {'virt.list_domains': MagicMock(return_value=[])}): # pylint: disable=no-member + ret.update({'changes': {}, 'result': False, 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.stopped('myvm'), ret) + ++ # Case where the domain is already stopped ++ with patch.dict(virt.__salt__, { # pylint: disable=no-member ++ 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), ++ 'virt.vm_state': MagicMock(return_value={'myvm': 'shutdown'}) ++ }): ++ ret.update({'changes': {}, ++ 'result': True, ++ 'comment': 'No changes had happened'}) ++ self.assertDictEqual(virt.stopped('myvm'), ret) ++ + def test_powered_off(self): + ''' + powered_off state test cases. +@@ -421,8 +439,11 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + 'result': True} + + stop_mock = MagicMock(return_value=True) ++ ++ # Normal case + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), ++ 'virt.vm_state': MagicMock(return_value={'myvm': 'running'}), + 'virt.stop': stop_mock + }): + ret.update({'changes': { +@@ -432,8 +453,10 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + self.assertDictEqual(virt.powered_off('myvm'), ret) + stop_mock.assert_called_with('myvm', connection=None, username=None, password=None) + ++ # Normal case with user-provided connection parameters + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), ++ 'virt.vm_state': MagicMock(return_value={'myvm': 'running'}), + 'virt.stop': stop_mock, + }): + self.assertDictEqual(virt.powered_off('myvm', +@@ -442,8 +465,10 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + password='secret'), ret) + stop_mock.assert_called_with('myvm', connection='myconnection', username='user', password='secret') + ++ # Case where an error occurred during the poweroff + with patch.dict(virt.__salt__, { # pylint: disable=no-member + 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), ++ 'virt.vm_state': MagicMock(return_value={'myvm': 'running'}), + 'virt.stop': MagicMock(side_effect=self.mock_libvirt.libvirtError('Some error')) + }): + ret.update({'changes': {'ignored': [{'domain': 'myvm', 'issue': 'Some error'}]}, +@@ -451,10 +476,21 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.powered_off('myvm'), ret) + ++ # Case there the domain doesn't exist + with patch.dict(virt.__salt__, {'virt.list_domains': MagicMock(return_value=[])}): # pylint: disable=no-member + ret.update({'changes': {}, 'result': False, 'comment': 'No changes had happened'}) + self.assertDictEqual(virt.powered_off('myvm'), ret) + ++ # Case where the domain is already stopped ++ with patch.dict(virt.__salt__, { # pylint: disable=no-member ++ 'virt.list_domains': MagicMock(return_value=['myvm', 'vm1']), ++ 'virt.vm_state': MagicMock(return_value={'myvm': 'shutdown'}) ++ }): ++ ret.update({'changes': {}, ++ 'result': True, ++ 'comment': 'No changes had happened'}) ++ self.assertDictEqual(virt.powered_off('myvm'), ret) ++ + def test_snapshot(self): + ''' + snapshot state test cases. +-- +2.23.0 + + diff --git a/list_downloaded-for-apt-module.patch b/list_downloaded-for-apt-module.patch new file mode 100644 index 0000000..e460538 --- /dev/null +++ b/list_downloaded-for-apt-module.patch @@ -0,0 +1,162 @@ +From a1e0904a640d01d4bab0871db1ab8ea653335443 Mon Sep 17 00:00:00 2001 +From: Jochen Breuer +Date: Thu, 9 Jan 2020 10:11:13 +0100 +Subject: [PATCH] list_downloaded for apt Module + +--- + salt/modules/aptpkg.py | 41 +++++++++++++++++++++++++++++++++++++++ + salt/states/pkg.py | 4 ++-- + tests/unit/modules/test_aptpkg.py | 29 +++++++++++++++++++++++++++ + 3 files changed, 72 insertions(+), 2 deletions(-) + +diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py +index 1a60255a1d..023049b2af 100644 +--- a/salt/modules/aptpkg.py ++++ b/salt/modules/aptpkg.py +@@ -18,6 +18,9 @@ import os + import re + import logging + import time ++import fnmatch ++import datetime ++ + + # Import third party libs + # pylint: disable=no-name-in-module,import-error,redefined-builtin +@@ -422,6 +425,7 @@ def install(name=None, + pkgs=None, + sources=None, + reinstall=False, ++ downloadonly=False, + ignore_epoch=False, + **kwargs): + ''' +@@ -768,6 +772,9 @@ def install(name=None, + cmd.extend(downgrade) + cmds.append(cmd) + ++ if downloadonly: ++ cmd.append("--download-only") ++ + if to_reinstall: + all_pkgs.extend(to_reinstall) + cmd = copy.deepcopy(cmd_prefix) +@@ -2917,3 +2924,37 @@ def _get_http_proxy_url(): + ) + + return http_proxy_url ++ ++ ++def list_downloaded(root=None, **kwargs): ++ ''' ++ .. versionadded:: 3000? ++ ++ List prefetched packages downloaded by apt in the local disk. ++ ++ root ++ operate on a different root directory. ++ ++ CLI example: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.list_downloaded ++ ''' ++ CACHE_DIR = '/var/cache/apt' ++ if root: ++ CACHE_DIR = os.path.join(root, os.path.relpath(CACHE_DIR, os.path.sep)) ++ ++ ret = {} ++ for root, dirnames, filenames in salt.utils.path.os_walk(CACHE_DIR): ++ for filename in fnmatch.filter(filenames, '*.deb'): ++ package_path = os.path.join(root, filename) ++ pkg_info = __salt__['lowpkg.bin_pkg_info'](package_path) ++ pkg_timestamp = int(os.path.getctime(package_path)) ++ ret.setdefault(pkg_info['name'], {})[pkg_info['version']] = { ++ 'path': package_path, ++ 'size': os.path.getsize(package_path), ++ 'creation_date_time_t': pkg_timestamp, ++ 'creation_date_time': datetime.datetime.utcfromtimestamp(pkg_timestamp).isoformat(), ++ } ++ return ret +diff --git a/salt/states/pkg.py b/salt/states/pkg.py +index 22a97fe98c..be00498135 100644 +--- a/salt/states/pkg.py ++++ b/salt/states/pkg.py +@@ -1975,7 +1975,7 @@ def downloaded(name, + (if specified). + + Currently supported for the following pkg providers: +- :mod:`yumpkg ` and :mod:`zypper ` ++ :mod:`yumpkg `, :mod:`zypper ` and :mod:`zypper ` + + :param str name: + The name of the package to be downloaded. This parameter is ignored if +@@ -2114,7 +2114,7 @@ def downloaded(name, + + if not ret['changes'] and not ret['comment']: + ret['result'] = True +- ret['comment'] = 'Packages are already downloaded: ' \ ++ ret['comment'] = 'Packages downloaded: ' \ + '{0}'.format(', '.join(targets)) + + return ret +diff --git a/tests/unit/modules/test_aptpkg.py b/tests/unit/modules/test_aptpkg.py +index bc6b610d86..5c7e38eae7 100644 +--- a/tests/unit/modules/test_aptpkg.py ++++ b/tests/unit/modules/test_aptpkg.py +@@ -413,14 +413,17 @@ class AptPkgTestCase(TestCase, LoaderModuleMockMixin): + with patch.multiple(aptpkg, **patch_kwargs): + aptpkg.upgrade() + args_matching = [True for args in patch_kwargs['__salt__']['cmd.run_all'].call_args[0] if "--download-only" in args] ++ # Here we shouldn't see the parameter and args_matching should be empty. + self.assertFalse(any(args_matching)) + + aptpkg.upgrade(downloadonly=True) + args_matching = [True for args in patch_kwargs['__salt__']['cmd.run_all'].call_args[0] if "--download-only" in args] ++ # --download-only should be in the args list and we should have at least on True in the list. + self.assertTrue(any(args_matching)) + + aptpkg.upgrade(download_only=True) + args_matching = [True for args in patch_kwargs['__salt__']['cmd.run_all'].call_args[0] if "--download-only" in args] ++ # --download-only should be in the args list and we should have at least on True in the list. + self.assertTrue(any(args_matching)) + + def test_show(self): +@@ -545,6 +548,32 @@ class AptPkgTestCase(TestCase, LoaderModuleMockMixin): + self.assert_called_once(refresh_mock) + refresh_mock.reset_mock() + ++ @patch('salt.utils.path.os_walk', MagicMock(return_value=[('test', 'test', 'test')])) ++ @patch('os.path.getsize', MagicMock(return_value=123456)) ++ @patch('os.path.getctime', MagicMock(return_value=1234567890.123456)) ++ @patch('fnmatch.filter', MagicMock(return_value=['/var/cache/apt/archive/test_package.rpm'])) ++ def test_list_downloaded(self): ++ ''' ++ Test downloaded packages listing. ++ :return: ++ ''' ++ DOWNLOADED_RET = { ++ 'test-package': { ++ '1.0': { ++ 'path': '/var/cache/apt/archive/test_package.rpm', ++ 'size': 123456, ++ 'creation_date_time_t': 1234567890, ++ 'creation_date_time': '2009-02-13T23:31:30', ++ } ++ } ++ } ++ ++ with patch.dict(aptpkg.__salt__, {'lowpkg.bin_pkg_info': MagicMock(return_value={'name': 'test-package', ++ 'version': '1.0'})}): ++ list_downloaded = aptpkg.list_downloaded() ++ self.assertEqual(len(list_downloaded), 1) ++ self.assertDictEqual(list_downloaded, DOWNLOADED_RET) ++ + + @skipIf(pytest is None, 'PyTest is missing') + class AptUtilsTestCase(TestCase, LoaderModuleMockMixin): +-- +2.16.4 + + diff --git a/salt.changes b/salt.changes index a78c4c7..fe25a62 100644 --- a/salt.changes +++ b/salt.changes @@ -1,3 +1,43 @@ +------------------------------------------------------------------- +Mon Jan 13 16:09:36 UTC 2020 - Jochen Breuer + +- Support for Btrfs and XFS in parted and mkfs added + +- Added: + * support-for-btrfs-and-xfs-in-parted-and-mkfs.patch + +------------------------------------------------------------------- +Thu Jan 9 19:20:34 UTC 2020 - Jochen Breuer + +- Adds list_downloaded for apt Module to enable pre-downloading support +- Adds virt.(pool|network)_get_xml functions +- Various libvirt updates + * Add virt.pool_capabilities function + * virt.pool_running improvements + * Add virt.pool_deleted state + * virt.network_define allow adding IP configuration + +- Added: + * virt.network_define-allow-adding-ip-configuration.patch + * list_downloaded-for-apt-module.patch + * add-virt.network_get_xml-function.patch + +------------------------------------------------------------------- +Tue Jan 7 10:28:04 UTC 2020 - Pablo Suárez Hernández + +- virt: adding kernel boot parameters to libvirt xml + +- Added: + * virt-adding-kernel-boot-parameters-to-libvirt-xml-55.patch + +------------------------------------------------------------------- +Mon Dec 16 10:36:42 UTC 2019 - Pablo Suárez Hernández + +- Fix virt states to not fail on VMs already stopped + +- Added: + * fix-virt-states-to-not-fail-on-vms-already-stopped.-.patch + ------------------------------------------------------------------- Thu Dec 12 10:21:15 UTC 2019 - Pablo Suárez Hernández diff --git a/salt.spec b/salt.spec index 90a4436..a226a99 100644 --- a/salt.spec +++ b/salt.spec @@ -272,6 +272,22 @@ Patch96: align-virt-full-info-fixes-with-upstream-192.patch Patch97: fix-virt.get_hypervisor-188.patch # PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/193 Patch98: xfs-do-not-fails-if-type-is-not-present.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/54196 +# PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/195 +Patch99: fix-virt-states-to-not-fail-on-vms-already-stopped.-.patch +# PATCH-FIX_UPSTREAM: https://github.com/saltstack/salt/pull/55245 +Patch100: virt-adding-kernel-boot-parameters-to-libvirt-xml-55.patch +# PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/189 +# PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/185 +# PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/184 +# PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/183 +Patch101: virt.network_define-allow-adding-ip-configuration.patch +# PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/196 +Patch102: add-virt.network_get_xml-function.patch +# PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/198 +Patch103: list_downloaded-for-apt-module.patch +# PATCH_FIX_OPENSUSE: https://github.com/openSUSE/salt/pull/200 +Patch104: support-for-btrfs-and-xfs-in-parted-and-mkfs.patch BuildRoot: %{_tmppath}/%{name}-%{version}-build BuildRequires: logrotate @@ -886,6 +902,12 @@ cp %{S:5} ./.travis.yml %patch96 -p1 %patch97 -p1 %patch98 -p1 +%patch99 -p1 +%patch100 -p1 +%patch101 -p1 +%patch102 -p1 +%patch103 -p1 +%patch104 -p1 %build %if 0%{?build_py2} diff --git a/support-for-btrfs-and-xfs-in-parted-and-mkfs.patch b/support-for-btrfs-and-xfs-in-parted-and-mkfs.patch new file mode 100644 index 0000000..932ecf4 --- /dev/null +++ b/support-for-btrfs-and-xfs-in-parted-and-mkfs.patch @@ -0,0 +1,56 @@ +From 92df25d10789e5d0686a882a1cbadbfc0602b2f5 Mon Sep 17 00:00:00 2001 +From: Jochen Breuer +Date: Fri, 10 Jan 2020 17:18:14 +0100 +Subject: [PATCH] Support for Btrfs and XFS in parted and mkfs + +--- + salt/modules/parted_partition.py | 4 ++-- + tests/unit/modules/test_parted_partition.py | 16 ++++++++++++++++ + 2 files changed, 18 insertions(+), 2 deletions(-) + +diff --git a/salt/modules/parted_partition.py b/salt/modules/parted_partition.py +index c2e0ebb882..e68124c245 100644 +--- a/salt/modules/parted_partition.py ++++ b/salt/modules/parted_partition.py +@@ -390,8 +390,8 @@ def _is_fstype(fs_type): + :param fs_type: file system type + :return: True if fs_type is supported in this module, False otherwise + ''' +- return fs_type in set(['ext2', 'ext3', 'ext4', 'fat32', 'fat16', 'linux-swap', 'reiserfs', +- 'hfs', 'hfs+', 'hfsx', 'NTFS', 'ntfs', 'ufs']) ++ return fs_type in set(['btrfs', 'ext2', 'ext3', 'ext4', 'fat32', 'fat16', 'linux-swap', 'reiserfs', ++ 'hfs', 'hfs+', 'hfsx', 'NTFS', 'ntfs', 'ufs', 'xfs']) + + + def mkfs(device, fs_type): +diff --git a/tests/unit/modules/test_parted_partition.py b/tests/unit/modules/test_parted_partition.py +index 1959e5978e..5d92bd6d14 100644 +--- a/tests/unit/modules/test_parted_partition.py ++++ b/tests/unit/modules/test_parted_partition.py +@@ -377,6 +377,22 @@ class PartedTestCase(TestCase, LoaderModuleMockMixin): + } + self.assertEqual(output, expected) + ++ def test_btrfs_fstypes(self): ++ '''Tests if we see btrfs as valid fs type''' ++ with patch('salt.modules.parted_partition._validate_device', MagicMock()): ++ try: ++ parted.mkfs('/dev/foo', 'btrfs') ++ except CommandExecutionError: ++ self.fail("Btrfs is not in the supported fstypes") ++ ++ def test_xfs_fstypes(self): ++ '''Tests if we see xfs as valid fs type''' ++ with patch('salt.modules.parted_partition._validate_device', MagicMock()): ++ try: ++ parted.mkfs('/dev/foo', 'xfs') ++ except CommandExecutionError: ++ self.fail("XFS is not in the supported fstypes") ++ + def test_disk_set(self): + with patch('salt.modules.parted_partition._validate_device', MagicMock()): + self.cmdrun.return_value = '' +-- +2.16.4 + + diff --git a/virt-adding-kernel-boot-parameters-to-libvirt-xml-55.patch b/virt-adding-kernel-boot-parameters-to-libvirt-xml-55.patch new file mode 100644 index 0000000..3899337 --- /dev/null +++ b/virt-adding-kernel-boot-parameters-to-libvirt-xml-55.patch @@ -0,0 +1,583 @@ +From 26a227868bcf1f790348e197e000561903f7fc72 Mon Sep 17 00:00:00 2001 +From: Larry Dewey +Date: Tue, 7 Jan 2020 02:48:11 -0700 +Subject: [PATCH] virt: adding kernel boot parameters to libvirt xml + #55245 (#197) + +* virt: adding kernel boot parameters to libvirt xml + +SUSE's autoyast and Red Hat's kickstart take advantage of kernel paths, +initrd paths, and kernel boot command line parameters. These changes +provide the option of using these, and will allow salt and +autoyast/kickstart to work together. + +Signed-off-by: Larry Dewey + +* virt: Download linux and initrd + +Signed-off-by: Larry Dewey +--- + salt/modules/virt.py | 129 ++++++++++++++++++++++- + salt/states/virt.py | 29 ++++- + salt/templates/virt/libvirt_domain.jinja | 12 ++- + salt/utils/virt.py | 45 +++++++- + tests/unit/modules/test_virt.py | 79 +++++++++++++- + tests/unit/states/test_virt.py | 19 +++- + 6 files changed, 302 insertions(+), 11 deletions(-) + +diff --git a/salt/modules/virt.py b/salt/modules/virt.py +index dedcf8cb6f..0f62856f5c 100644 +--- a/salt/modules/virt.py ++++ b/salt/modules/virt.py +@@ -106,6 +106,8 @@ import salt.utils.templates + import salt.utils.validate.net + import salt.utils.versions + import salt.utils.yaml ++ ++from salt.utils.virt import check_remote, download_remote + from salt.exceptions import CommandExecutionError, SaltInvocationError + from salt.ext import six + from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin +@@ -119,6 +121,8 @@ JINJA = jinja2.Environment( + ) + ) + ++CACHE_DIR = '/var/lib/libvirt/saltinst' ++ + VIRT_STATE_NAME_MAP = {0: 'running', + 1: 'running', + 2: 'running', +@@ -532,6 +536,7 @@ def _gen_xml(name, + os_type, + arch, + graphics=None, ++ boot=None, + **kwargs): + ''' + Generate the XML string to define a libvirt VM +@@ -568,11 +573,15 @@ def _gen_xml(name, + else: + context['boot_dev'] = ['hd'] + ++ context['boot'] = boot if boot else {} ++ + if os_type == 'xen': + # Compute the Xen PV boot method + if __grains__['os_family'] == 'Suse': +- context['kernel'] = '/usr/lib/grub2/x86_64-xen/grub.xen' +- context['boot_dev'] = [] ++ if not boot or not boot.get('kernel', None): ++ context['boot']['kernel'] = \ ++ '/usr/lib/grub2/x86_64-xen/grub.xen' ++ context['boot_dev'] = [] + + if 'serial_type' in kwargs: + context['serial_type'] = kwargs['serial_type'] +@@ -1115,6 +1124,34 @@ def _get_merged_nics(hypervisor, profile, interfaces=None, dmac=None): + return nicp + + ++def _handle_remote_boot_params(orig_boot): ++ """ ++ Checks if the boot parameters contain a remote path. If so, it will copy ++ the parameters, download the files specified in the remote path, and return ++ a new dictionary with updated paths containing the canonical path to the ++ kernel and/or initrd ++ ++ :param orig_boot: The original boot parameters passed to the init or update ++ functions. ++ """ ++ saltinst_dir = None ++ new_boot = orig_boot.copy() ++ ++ try: ++ for key in ['kernel', 'initrd']: ++ if check_remote(orig_boot.get(key)): ++ if saltinst_dir is None: ++ os.makedirs(CACHE_DIR) ++ saltinst_dir = CACHE_DIR ++ ++ new_boot[key] = download_remote(orig_boot.get(key), ++ saltinst_dir) ++ ++ return new_boot ++ except Exception as err: ++ raise err ++ ++ + def init(name, + cpu, + mem, +@@ -1136,6 +1173,7 @@ def init(name, + graphics=None, + os_type=None, + arch=None, ++ boot=None, + **kwargs): + ''' + Initialize a new vm +@@ -1266,6 +1304,22 @@ def init(name, + :param password: password to connect with, overriding defaults + + .. versionadded:: 2019.2.0 ++ :param boot: ++ Specifies kernel for the virtual machine, as well as boot parameters ++ for the virtual machine. This is an optionl parameter, and all of the ++ keys are optional within the dictionary. If a remote path is provided ++ to kernel or initrd, salt will handle the downloading of the specified ++ remote fild, and will modify the XML accordingly. ++ ++ .. code-block:: python ++ ++ { ++ 'kernel': '/root/f8-i386-vmlinuz', ++ 'initrd': '/root/f8-i386-initrd', ++ 'cmdline': 'console=ttyS0 ks=http://example.com/f8-i386/os/' ++ } ++ ++ .. versionadded:: neon + + .. _init-nic-def: + +@@ -1513,7 +1567,11 @@ def init(name, + if arch is None: + arch = 'x86_64' if 'x86_64' in arches else arches[0] + +- vm_xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, os_type, arch, graphics, **kwargs) ++ if boot is not None: ++ boot = _handle_remote_boot_params(boot) ++ ++ vm_xml = _gen_xml(name, cpu, mem, diskp, nicp, hypervisor, os_type, arch, ++ graphics, boot, **kwargs) + conn = __get_conn(**kwargs) + try: + conn.defineXML(vm_xml) +@@ -1692,6 +1750,7 @@ def update(name, + interfaces=None, + graphics=None, + live=True, ++ boot=None, + **kwargs): + ''' + Update the definition of an existing domain. +@@ -1727,6 +1786,23 @@ def update(name, + :param username: username to connect with, overriding defaults + :param password: password to connect with, overriding defaults + ++ :param boot: ++ Specifies kernel for the virtual machine, as well as boot parameters ++ for the virtual machine. This is an optionl parameter, and all of the ++ keys are optional within the dictionary. If a remote path is provided ++ to kernel or initrd, salt will handle the downloading of the specified ++ remote fild, and will modify the XML accordingly. ++ ++ .. code-block:: python ++ ++ { ++ 'kernel': '/root/f8-i386-vmlinuz', ++ 'initrd': '/root/f8-i386-initrd', ++ 'cmdline': 'console=ttyS0 ks=http://example.com/f8-i386/os/' ++ } ++ ++ .. versionadded:: neon ++ + :return: + + Returns a dictionary indicating the status of what has been done. It is structured in +@@ -1767,6 +1843,10 @@ def update(name, + # Compute the XML to get the disks, interfaces and graphics + hypervisor = desc.get('type') + all_disks = _disk_profile(disk_profile, hypervisor, disks, name, **kwargs) ++ ++ if boot is not None: ++ boot = _handle_remote_boot_params(boot) ++ + new_desc = ElementTree.fromstring(_gen_xml(name, + cpu, + mem, +@@ -1776,6 +1856,7 @@ def update(name, + domain.OSType(), + desc.find('.//os/type').get('arch'), + graphics, ++ boot, + **kwargs)) + + # Update the cpu +@@ -1785,6 +1866,48 @@ def update(name, + cpu_node.set('current', six.text_type(cpu)) + need_update = True + ++ # Update the kernel boot parameters ++ boot_tags = ['kernel', 'initrd', 'cmdline'] ++ parent_tag = desc.find('os') ++ ++ # We need to search for each possible subelement, and update it. ++ for tag in boot_tags: ++ # The Existing Tag... ++ found_tag = desc.find(tag) ++ ++ # The new value ++ boot_tag_value = boot.get(tag, None) if boot else None ++ ++ # Existing tag is found and values don't match ++ if found_tag and found_tag.text != boot_tag_value: ++ ++ # If the existing tag is found, but the new value is None ++ # remove it. If the existing tag is found, and the new value ++ # doesn't match update it. In either case, mark for update. ++ if boot_tag_value is None \ ++ and boot is not None \ ++ and parent_tag is not None: ++ ElementTree.remove(parent_tag, tag) ++ else: ++ found_tag.text = boot_tag_value ++ ++ need_update = True ++ ++ # Existing tag is not found, but value is not None ++ elif found_tag is None and boot_tag_value is not None: ++ ++ # Need to check for parent tag, and add it if it does not exist. ++ # Add a subelement and set the value to the new value, and then ++ # mark for update. ++ if parent_tag is not None: ++ child_tag = ElementTree.SubElement(parent_tag, tag) ++ else: ++ new_parent_tag = ElementTree.Element('os') ++ child_tag = ElementTree.SubElement(new_parent_tag, tag) ++ ++ child_tag.text = boot_tag_value ++ need_update = True ++ + # Update the memory, note that libvirt outputs all memory sizes in KiB + for mem_node_name in ['memory', 'currentMemory']: + mem_node = desc.find(mem_node_name) +diff --git a/salt/states/virt.py b/salt/states/virt.py +index 68e9ac6fb6..c700cae849 100644 +--- a/salt/states/virt.py ++++ b/salt/states/virt.py +@@ -264,7 +264,8 @@ def running(name, + username=None, + password=None, + os_type=None, +- arch=None): ++ arch=None, ++ boot=None): + ''' + Starts an existing guest, or defines and starts a new VM with specified arguments. + +@@ -349,6 +350,23 @@ def running(name, + + .. versionadded:: Neon + ++ :param boot: ++ Specifies kernel for the virtual machine, as well as boot parameters ++ for the virtual machine. This is an optionl parameter, and all of the ++ keys are optional within the dictionary. If a remote path is provided ++ to kernel or initrd, salt will handle the downloading of the specified ++ remote fild, and will modify the XML accordingly. ++ ++ .. code-block:: python ++ ++ { ++ 'kernel': '/root/f8-i386-vmlinuz', ++ 'initrd': '/root/f8-i386-initrd', ++ 'cmdline': 'console=ttyS0 ks=http://example.com/f8-i386/os/' ++ } ++ ++ .. versionadded:: neon ++ + .. rubric:: Example States + + Make sure an already-defined virtual machine called ``domain_name`` is running: +@@ -413,7 +431,8 @@ def running(name, + live=False, + connection=connection, + username=username, +- password=password) ++ password=password, ++ boot=boot) + if status['definition']: + action_msg = 'updated and started' + __salt__['virt.start'](name) +@@ -431,7 +450,8 @@ def running(name, + graphics=graphics, + connection=connection, + username=username, +- password=password) ++ password=password, ++ boot=boot) + ret['changes'][name] = status + if status.get('errors', None): + ret['comment'] = 'Domain {0} updated, but some live update(s) failed'.format(name) +@@ -466,7 +486,8 @@ def running(name, + priv_key=priv_key, + connection=connection, + username=username, +- password=password) ++ password=password, ++ boot=boot) + ret['changes'][name] = 'Domain defined and started' + ret['comment'] = 'Domain {0} defined and started'.format(name) + except libvirt.libvirtError as err: +diff --git a/salt/templates/virt/libvirt_domain.jinja b/salt/templates/virt/libvirt_domain.jinja +index 0b4c3fc2d6..fdaea168f2 100644 +--- a/salt/templates/virt/libvirt_domain.jinja ++++ b/salt/templates/virt/libvirt_domain.jinja +@@ -5,7 +5,17 @@ + {{ mem }} + + {{ os_type }} +- {% if kernel %}{{ kernel }}{% endif %} ++ {% if boot %} ++ {% if 'kernel' in boot %} ++ {{ boot.kernel }} ++ {% endif %} ++ {% if 'initrd' in boot %} ++ {{ boot.initrd }} ++ {% endif %} ++ {% if 'cmdline' in boot %} ++ {{ boot.cmdline }} ++ {% endif %} ++ {% endif %} + {% for dev in boot_dev %} + + {% endfor %} +diff --git a/salt/utils/virt.py b/salt/utils/virt.py +index 9dad849c0e..b36adba81c 100644 +--- a/salt/utils/virt.py ++++ b/salt/utils/virt.py +@@ -6,16 +6,59 @@ from __future__ import absolute_import, print_function, unicode_literals + + # Import python libs + import os ++import re + import time + import logging ++import hashlib ++ ++# pylint: disable=E0611 ++from salt.ext.six.moves.urllib.parse import urlparse ++from salt.ext.six.moves.urllib import request + + # Import salt libs + import salt.utils.files + +- + log = logging.getLogger(__name__) + + ++def download_remote(url, dir): ++ """ ++ Attempts to download a file specified by 'url' ++ ++ :param url: The full remote path of the file which should be downloaded. ++ :param dir: The path the file should be downloaded to. ++ """ ++ ++ try: ++ rand = hashlib.md5(os.urandom(32)).hexdigest() ++ remote_filename = urlparse(url).path.split('/')[-1] ++ full_directory = \ ++ os.path.join(dir, "{}-{}".format(rand, remote_filename)) ++ with salt.utils.files.fopen(full_directory, 'wb') as file,\ ++ request.urlopen(url) as response: ++ file.write(response.rease()) ++ ++ return full_directory ++ ++ except Exception as err: ++ raise err ++ ++ ++def check_remote(cmdline_path): ++ """ ++ Checks to see if the path provided contains ftp, http, or https. Returns ++ the full path if it is found. ++ ++ :param cmdline_path: The path to the initrd image or the kernel ++ """ ++ regex = re.compile('^(ht|f)tps?\\b') ++ ++ if regex.match(urlparse(cmdline_path).scheme): ++ return True ++ ++ return False ++ ++ + class VirtKey(object): + ''' + Used to manage key signing requests. +diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py +index 6f594a8ff3..4bdb933a2d 100644 +--- a/tests/unit/modules/test_virt.py ++++ b/tests/unit/modules/test_virt.py +@@ -10,6 +10,7 @@ from __future__ import absolute_import, print_function, unicode_literals + import os + import re + import datetime ++import shutil + + # Import Salt Testing libs + from tests.support.mixins import LoaderModuleMockMixin +@@ -23,6 +24,7 @@ import salt.modules.config as config + from salt._compat import ElementTree as ET + import salt.config + import salt.syspaths ++import tempfile + from salt.exceptions import CommandExecutionError + + # Import third party libs +@@ -30,7 +32,6 @@ from salt.ext import six + # pylint: disable=import-error + from salt.ext.six.moves import range # pylint: disable=redefined-builtin + +- + # pylint: disable=invalid-name,protected-access,attribute-defined-outside-init,too-many-public-methods,unused-argument + + +@@ -610,6 +611,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + 'xen', + 'xen', + 'x86_64', ++ boot=None + ) + root = ET.fromstring(xml_data) + self.assertEqual(root.attrib['type'], 'xen') +@@ -1123,6 +1125,67 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + self.assertFalse(' +Date: Tue, 13 Aug 2019 12:26:59 +0200 +Subject: [PATCH] virt.network_define allow adding IP configuration + +If using virt.network_define with nat network type, then libvirt +complains about missing IP configuration. Allow setting it in both the +virt.network_define module and the virt.network_running state. + +Add virt.pool_deleted state + +The new virt.pool_deleted state takes care of removing a virtual storage +pool and optionally the contained volumes. + +Fix virt._gen_pool_xml when all source parameters are None + +When all the source_* parameters of _gen_pool_xml are None, no +element should be generated. Instead we had the following: + + + +Rather than filling a source structure with empty members, directly pass +a None for the source as expected by the jinja template. + +Add virt.pool_update function + +Add a function that allows changing a virtual storage pool. By using the +test=True parameter, this function can be used to check if a change +would be done. + +virt.pool_running state improvements + +With this commit the virt.pool_running state is capable to update an +existing pool before ensuring it is running. + +This also adds support for the test parameter. + +Fix virt.pool_define documentation + +virt.pool_define example are mentioning a uuid property while this one +should be named value. The code was right, but not the doc. + +Add source_initiator parameter in virt.pool_define + +In order to define iscsi-direct virtual storage pools, the use needs to +be able to provide the initiator IQN. Add a parameter for it in: + * virt.pool_define module function + * virt.pool_update module function + * virt.pool_running state + +virt pool secret have no type attribute + +According to libvirt schemas and doc only the volume secrets have a +type. + +virt: create and update pool secret's password for the user + +Libvirt stores RBD and iSCSI passwords in secrets. Add a password field +in the source_auth parameter of virt.pool_define and virt.pool_update to +set the password value rather than reuse an already defined libvirt +secret. + +virt.pool_define: remove potential leading / in CIFS source path + +libvirt needs the CIFS source path not to start with a /. Let's remove +them since this could be a common user mistake. + +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 | 497 +++++++++++++++++++++++++++++- + salt/states/virt.py | 279 +++++++++++++++-- + salt/templates/virt/libvirt_network.jinja | 13 +- + salt/templates/virt/libvirt_pool.jinja | 13 +- + salt/templates/virt/libvirt_secret.jinja | 12 + + tests/unit/modules/test_virt.py | 455 ++++++++++++++++++++++++++- + tests/unit/states/test_virt.py | 400 +++++++++++++++++++----- + 7 files changed, 1543 insertions(+), 126 deletions(-) + create mode 100644 salt/templates/virt/libvirt_secret.jinja + +diff --git a/salt/modules/virt.py b/salt/modules/virt.py +index 0f62856f5c..44c7e78ef0 100644 +--- a/salt/modules/virt.py ++++ b/salt/modules/virt.py +@@ -74,6 +74,7 @@ The calls not using the libvirt connection setup are: + + # Import python libs + from __future__ import absolute_import, print_function, unicode_literals ++import base64 + import copy + import os + import re +@@ -111,6 +112,7 @@ from salt.utils.virt import check_remote, download_remote + from salt.exceptions import CommandExecutionError, SaltInvocationError + from salt.ext import six + from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin ++from salt._compat import ipaddress + + log = logging.getLogger(__name__) + +@@ -662,7 +664,8 @@ def _gen_net_xml(name, + bridge, + forward, + vport, +- tag=None): ++ tag=None, ++ ip_configs=None): + ''' + Generate the XML string to define a libvirt network + ''' +@@ -672,6 +675,10 @@ def _gen_net_xml(name, + 'forward': forward, + 'vport': vport, + 'tag': tag, ++ 'ip_configs': [{ ++ 'address': ipaddress.ip_network(config['cidr']), ++ 'dhcp_ranges': config.get('dhcp_ranges', []), ++ } for config in ip_configs or []], + } + fn_ = 'libvirt_network.jinja' + try: +@@ -692,24 +699,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: +@@ -720,6 +734,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 +@@ -4425,7 +4457,12 @@ def cpu_baseline(full=False, migratable=False, out='libvirt', **kwargs): + return cpu.toxml() + + +-def network_define(name, bridge, forward, **kwargs): ++def network_define(name, ++ bridge, ++ forward, ++ ipv4_config=None, ++ ipv6_config=None, ++ **kwargs): + ''' + Create libvirt network. + +@@ -4436,10 +4473,38 @@ def network_define(name, bridge, forward, **kwargs): + :param tag: Vlan tag + :param autostart: Network autostart (default True) + :param start: Network start (default True) ++ :param ipv4_config: IP v4 configuration ++ Dictionary describing the IP v4 setup like IP range and ++ a possible DHCP configuration. The structure is documented ++ in net-define-ip_. ++ ++ ..versionadded:: Neon ++ :type ipv4_config: dict or None ++ ++ :param ipv6_config: IP v6 configuration ++ Dictionary describing the IP v6 setup like IP range and ++ a possible DHCP configuration. The structure is documented ++ in net-define-ip_. ++ ++ ..versionadded:: Neon ++ :type ipv6_config: dict or None ++ + :param connection: libvirt connection URI, overriding defaults + :param username: username to connect with, overriding defaults + :param password: password to connect with, overriding defaults + ++ .. _net-define-ip: ++ ++ ** IP configuration definition ++ ++ Both the IPv4 and IPv6 configuration dictionaries can contain the following properties: ++ ++ cidr ++ CIDR notation for the network. For example '192.168.124.0/24' ++ ++ dhcp_ranges ++ A list of dictionary with ``'start'`` and ``'end'`` properties. ++ + CLI Example: + + .. code-block:: bash +@@ -4453,12 +4518,14 @@ def network_define(name, bridge, forward, **kwargs): + tag = kwargs.get('tag', None) + autostart = kwargs.get('autostart', True) + starting = kwargs.get('start', True) ++ + net_xml = _gen_net_xml( + name, + bridge, + forward, + vport, +- tag, ++ tag=tag, ++ ip_configs=[config for config in [ipv4_config, ipv6_config] if config], + ) + try: + conn.networkDefineXML(net_xml) +@@ -4669,12 +4736,171 @@ 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, + permissions=None, + source_devices=None, + source_dir=None, ++ source_initiator=None, + source_adapter=None, + source_hosts=None, + source_auth=None, +@@ -4705,6 +4931,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`` +@@ -4736,7 +4966,7 @@ def pool_define(name, + 'username': 'admin', + 'secret': { + 'type': 'uuid', +- 'uuid': '2ec115d7-3a88-3ceb-bc12-0ac909a6fd87' ++ 'value': '2ec115d7-3a88-3ceb-bc12-0ac909a6fd87' + } + } + +@@ -4747,10 +4977,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: +@@ -4803,6 +5037,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, +@@ -4812,9 +5048,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: +@@ -4832,6 +5069,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 c700cae849..d1c9191a29 100644 +--- a/salt/states/virt.py ++++ b/salt/states/virt.py +@@ -658,6 +658,8 @@ def network_running(name, + forward, + vport=None, + tag=None, ++ ipv4_config=None, ++ ipv6_config=None, + autostart=True, + connection=None, + username=None, +@@ -665,6 +667,25 @@ def network_running(name, + ''' + Defines and starts a new network with specified arguments. + ++ :param bridge: Bridge name ++ :param forward: Forward mode(bridge, router, nat) ++ :param vport: Virtualport type (Default: ``'None'``) ++ :param tag: Vlan tag (Default: ``'None'``) ++ :param ipv4_config: ++ IPv4 network configuration. See the :py:func`virt.network_define ++ ` function corresponding parameter documentation ++ for more details on this dictionary. ++ (Default: None). ++ ++ .. versionadded:: neon ++ :param ipv6_config: ++ IPv6 network configuration. See the :py:func`virt.network_define ++ ` function corresponding parameter documentation ++ for more details on this dictionary. ++ (Default: None). ++ ++ .. versionadded:: neon ++ :param autostart: Network autostart (default ``'True'``) + :param connection: libvirt connection URI, overriding defaults + + .. versionadded:: 2019.2.0 +@@ -690,6 +711,21 @@ def network_running(name, + - tag: 180 + - autostart: True + ++ .. code-block:: yaml ++ ++ network_name: ++ virt.network_define: ++ - bridge: natted ++ - forward: nat ++ - ipv4_config: ++ cidr: 192.168.42.0/24 ++ dhcp_ranges: ++ - start: 192.168.42.10 ++ end: 192.168.42.25 ++ - start: 192.168.42.100 ++ end: 192.168.42.150 ++ - autostart: True ++ + ''' + ret = {'name': name, + 'changes': {}, +@@ -712,6 +748,8 @@ def network_running(name, + forward, + vport=vport, + tag=tag, ++ ipv4_config=ipv4_config, ++ ipv6_config=ipv6_config, + autostart=autostart, + start=True, + connection=connection, +@@ -784,57 +822,230 @@ 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) ++ except libvirt.libvirtError as err: ++ ret['comment'] = err.get_error_message() ++ ret['result'] = False ++ ++ return ret ++ + +- __salt__['virt.pool_build'](name, +- connection=connection, +- username=username, +- password=password) ++def pool_deleted(name, ++ purge=False, ++ connection=None, ++ username=None, ++ password=None): ++ ''' ++ Deletes a virtual storage pool. ++ ++ :param name: the name of the pool to delete. ++ :param purge: ++ if ``True``, the volumes contained in the pool will be deleted as well as the pool itself. ++ Note that these will be lost for ever. If ``False`` the pool will simply be undefined. ++ (Default: ``False``) ++ :param connection: libvirt connection URI, overriding defaults ++ :param username: username to connect with, overriding defaults ++ :param password: password to connect with, overriding defaults + +- __salt__['virt.pool_start'](name, +- connection=connection, +- username=username, +- password=password) ++ In order to be purged a storage pool needs to be running to get the list of volumes to delete. ++ ++ Some libvirt storage drivers may not implement deleting, those actions are implemented on a ++ best effort idea. In any case check the result's comment property to see if any of the action ++ was unsupported. ++ ++ .. code-block::yaml ++ ++ pool_name: ++ uyuni_virt.pool_deleted: ++ - purge: True ++ ++ .. versionadded:: Neon ++ ''' ++ ret = {'name': name, 'changes': {}, 'result': True, 'comment': ''} + +- ret['changes'][name] = 'Pool defined and started' +- ret['comment'] = 'Pool {0} defined and started'.format(name) ++ try: ++ info = __salt__['virt.pool_info'](name, connection=connection, username=username, password=password) ++ if info: ++ ret['changes']['stopped'] = False ++ ret['changes']['deleted'] = False ++ ret['changes']['undefined'] = False ++ ret['changes']['deleted_volumes'] = [] ++ unsupported = [] ++ ++ if info[name]['state'] == 'running': ++ if purge: ++ unsupported_volume_delete = ['iscsi', 'iscsi-direct', 'mpath', 'scsi'] ++ if info[name]['type'] not in unsupported_volume_delete: ++ __salt__['virt.pool_refresh'](name, ++ connection=connection, ++ username=username, ++ password=password) ++ volumes = __salt__['virt.pool_list_volumes'](name, ++ connection=connection, ++ username=username, ++ password=password) ++ for volume in volumes: ++ # Not supported for iSCSI and SCSI drivers ++ deleted = __opts__['test'] ++ if not __opts__['test']: ++ deleted = __salt__['virt.volume_delete'](name, ++ volume, ++ connection=connection, ++ username=username, ++ password=password) ++ if deleted: ++ ret['changes']['deleted_volumes'].append(volume) ++ else: ++ unsupported.append('deleting volume') ++ ++ if not __opts__['test']: ++ ret['changes']['stopped'] = __salt__['virt.pool_stop'](name, ++ connection=connection, ++ username=username, ++ password=password) ++ else: ++ ret['changes']['stopped'] = True ++ ++ if purge: ++ supported_pool_delete = ['dir', 'fs', 'netfs', 'logical', 'vstorage', 'zfs'] ++ if info[name]['type'] in supported_pool_delete: ++ if not __opts__['test']: ++ ret['changes']['deleted'] = __salt__['virt.pool_delete'](name, ++ connection=connection, ++ username=username, ++ password=password) ++ else: ++ ret['changes']['deleted'] = True ++ else: ++ unsupported.append('deleting pool') ++ ++ if not __opts__['test']: ++ ret['changes']['undefined'] = __salt__['virt.pool_undefine'](name, ++ connection=connection, ++ username=username, ++ password=password) ++ else: ++ ret['changes']['undefined'] = True ++ ret['result'] = None ++ ++ if unsupported: ++ ret['comment'] = 'Unsupported actions for pool of type "{0}": {1}'.format(info[name]['type'], ++ ', '.join(unsupported)) ++ else: ++ ret['comment'] = 'Storage pool could not be found: {0}'.format(name) + except libvirt.libvirtError as err: +- ret['comment'] = err.get_error_message() ++ ret['comment'] = 'Failed deleting pool: {0}'.format(err.get_error_message()) + ret['result'] = False + + return ret +diff --git a/salt/templates/virt/libvirt_network.jinja b/salt/templates/virt/libvirt_network.jinja +index d0db99cad8..2f11e64559 100644 +--- a/salt/templates/virt/libvirt_network.jinja ++++ b/salt/templates/virt/libvirt_network.jinja +@@ -6,4 +6,15 @@ + + + {% endif %} +- +\ No newline at end of file ++ {% for ip_config in ip_configs %} ++ ++ ++ {% for range in ip_config.dhcp_ranges %} ++ ++ {% endfor %} ++ ++ ++ {% endfor %} ++ +diff --git a/salt/templates/virt/libvirt_pool.jinja b/salt/templates/virt/libvirt_pool.jinja +index 58c82f7177..515702cf46 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 0000000000..41c6dd811a +--- /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 4bdb933a2d..698e1922fc 100644 +--- a/tests/unit/modules/test_virt.py ++++ b/tests/unit/modules/test_virt.py +@@ -2016,6 +2016,32 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + self.assertEqual(root.find('forward').attrib['mode'], 'bridge') + self.assertEqual(root.find('virtualport').attrib['type'], 'openvswitch') + ++ def test_network_nat(self): ++ ''' ++ Test virt._get_net_xml() in a nat setup ++ ''' ++ xml_data = virt._gen_net_xml('network', 'main', 'nat', None, ip_configs=[ ++ { ++ 'cidr': '192.168.2.0/24', ++ 'dhcp_ranges': [ ++ {'start': '192.168.2.10', 'end': '192.168.2.25'}, ++ {'start': '192.168.2.110', 'end': '192.168.2.125'}, ++ ] ++ } ++ ]) ++ root = ET.fromstring(xml_data) ++ self.assertEqual(root.find('name').text, 'network') ++ self.assertEqual(root.find('bridge').attrib['name'], 'main') ++ self.assertEqual(root.find('forward').attrib['mode'], 'nat') ++ self.assertEqual(root.find("./ip[@address='192.168.2.0']").attrib['prefix'], '24') ++ self.assertEqual(root.find("./ip[@address='192.168.2.0']").attrib['family'], 'ipv4') ++ self.assertEqual( ++ root.find("./ip[@address='192.168.2.0']/dhcp/range[@start='192.168.2.10']").attrib['end'], ++ '192.168.2.25') ++ self.assertEqual( ++ root.find("./ip[@address='192.168.2.0']/dhcp/range[@start='192.168.2.110']").attrib['end'], ++ '192.168.2.125') ++ + def test_domain_capabilities(self): + ''' + Test the virt.domain_capabilities parsing +@@ -2468,7 +2494,6 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + 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): +@@ -2506,6 +2531,114 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + 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() +@@ -3135,3 +3268,323 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + + isxen_mock.return_value = True + self.assertEqual('xen', virt.get_hypervisor()) ++ ++ 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') ++ ++ 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']) +diff --git a/tests/unit/states/test_virt.py b/tests/unit/states/test_virt.py +index 109faf5fba..334c33b7d0 100644 +--- a/tests/unit/states/test_virt.py ++++ b/tests/unit/states/test_virt.py +@@ -619,6 +619,19 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + 'bridge', + vport='openvswitch', + tag=180, ++ ipv4_config={ ++ 'cidr': '192.168.2.0/24', ++ 'dhcp_ranges': [ ++ {'start': '192.168.2.10', 'end': '192.168.2.25'}, ++ {'start': '192.168.2.110', 'end': '192.168.2.125'}, ++ ] ++ }, ++ ipv6_config={ ++ 'cidr': '2001:db8:ca2:2::1/64', ++ 'dhcp_ranges': [ ++ {'start': '2001:db8:ca2:1::10', 'end': '2001:db8:ca2::1f'}, ++ ] ++ }, + autostart=False, + connection='myconnection', + username='user', +@@ -630,6 +643,19 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + tag=180, + autostart=False, + start=True, ++ ipv4_config={ ++ 'cidr': '192.168.2.0/24', ++ 'dhcp_ranges': [ ++ {'start': '192.168.2.10', 'end': '192.168.2.25'}, ++ {'start': '192.168.2.110', 'end': '192.168.2.125'}, ++ ] ++ }, ++ ipv6_config={ ++ 'cidr': '2001:db8:ca2:2::1/64', ++ 'dhcp_ranges': [ ++ {'start': '2001:db8:ca2:1::10', 'end': '2001:db8:ca2::1f'}, ++ ] ++ }, + connection='myconnection', + username='user', + password='secret') +@@ -668,92 +694,324 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): + 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', ++ 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) +- +- 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', ++ 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) +- 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')) ++ 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): ++ ''' ++ Test the pool_deleted state ++ ''' ++ # purge=False test case, stopped pool ++ with patch.dict(virt.__salt__, { ++ 'virt.pool_info': MagicMock(return_value={'test01': {'state': 'stopped', 'type': 'dir'}}), ++ 'virt.pool_undefine': MagicMock(return_value=True) + }): +- 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) ++ expected = { ++ 'name': 'test01', ++ 'changes': { ++ 'stopped': False, ++ 'deleted_volumes': [], ++ 'deleted': False, ++ 'undefined': True, ++ }, ++ 'result': True, ++ 'comment': '', ++ } ++ ++ with patch.dict(virt.__opts__, {'test': False}): ++ self.assertDictEqual(expected, virt.pool_deleted('test01')) ++ ++ with patch.dict(virt.__opts__, {'test': True}): ++ expected['result'] = None ++ self.assertDictEqual(expected, virt.pool_deleted('test01')) ++ ++ # purge=False test case ++ with patch.dict(virt.__salt__, { ++ 'virt.pool_info': MagicMock(return_value={'test01': {'state': 'running', 'type': 'dir'}}), ++ 'virt.pool_undefine': MagicMock(return_value=True), ++ 'virt.pool_stop': MagicMock(return_value=True) ++ }): ++ expected = { ++ 'name': 'test01', ++ 'changes': { ++ 'stopped': True, ++ 'deleted_volumes': [], ++ 'deleted': False, ++ 'undefined': True, ++ }, ++ 'result': True, ++ 'comment': '', ++ } ++ ++ with patch.dict(virt.__opts__, {'test': False}): ++ self.assertDictEqual(expected, virt.pool_deleted('test01')) ++ ++ with patch.dict(virt.__opts__, {'test': True}): ++ expected['result'] = None ++ self.assertDictEqual(expected, virt.pool_deleted('test01')) ++ ++ # purge=True test case ++ ++ with patch.dict(virt.__salt__, { ++ 'virt.pool_info': MagicMock(return_value={'test01': {'state': 'running', 'type': 'dir'}}), ++ 'virt.pool_list_volumes': MagicMock(return_value=['vm01.qcow2', 'vm02.qcow2']), ++ 'virt.pool_refresh': MagicMock(return_value=True), ++ 'virt.volume_delete': MagicMock(return_value=True), ++ 'virt.pool_stop': MagicMock(return_value=True), ++ 'virt.pool_delete': MagicMock(return_value=True), ++ 'virt.pool_undefine': MagicMock(return_value=True) ++ }): ++ expected = { ++ 'name': 'test01', ++ 'changes': { ++ 'stopped': True, ++ 'deleted_volumes': ['vm01.qcow2', 'vm02.qcow2'], ++ 'deleted': True, ++ 'undefined': True, ++ }, ++ 'result': True, ++ 'comment': '', ++ } ++ ++ with patch.dict(virt.__opts__, {'test': False}): ++ self.assertDictEqual(expected, virt.pool_deleted('test01', purge=True)) ++ ++ with patch.dict(virt.__opts__, {'test': True}): ++ expected['result'] = None ++ self.assertDictEqual(expected, virt.pool_deleted('test01', purge=True)) ++ ++ # Case of backend not unsupporting delete operations ++ with patch.dict(virt.__salt__, { ++ 'virt.pool_info': MagicMock(return_value={'test01': {'state': 'running', 'type': 'iscsi'}}), ++ 'virt.pool_stop': MagicMock(return_value=True), ++ 'virt.pool_undefine': MagicMock(return_value=True) ++ }): ++ expected = { ++ 'name': 'test01', ++ 'changes': { ++ 'stopped': True, ++ 'deleted_volumes': [], ++ 'deleted': False, ++ 'undefined': True, ++ }, ++ 'result': True, ++ 'comment': 'Unsupported actions for pool of type "iscsi": deleting volume, deleting pool', ++ } ++ ++ with patch.dict(virt.__opts__, {'test': False}): ++ self.assertDictEqual(expected, virt.pool_deleted('test01', purge=True)) ++ ++ with patch.dict(virt.__opts__, {'test': True}): ++ expected['result'] = None ++ self.assertDictEqual(expected, virt.pool_deleted('test01', purge=True)) +-- +2.16.4 + +