From dcf656e899c955195a8cb6560e5c2e1848102471e40d1cbd883b74889e482fb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Tue, 12 Jan 2021 12:57:50 +0000 Subject: [PATCH] osc copypac from project:systemsmanagement:saltstack:testing package:salt revision:377 OBS-URL: https://build.opensuse.org/package/show/systemsmanagement:saltstack/salt?expand=0&rev=181 --- _lastrevision | 2 +- ...t-for-allow-vendor-change-option-wit.patch | 107 + add-pkg.services_need_restart-302.patch | 404 + ...malize_name-when-package-arch-is-all.patch | 42 + ...tringutils.to_str-calls-to-make-it-w.patch | 99 + ...-to-prefer-packages.db-than-packages.patch | 29 + open-suse-3002.2-bigvm-310.patch | 6515 ++++++++++++ open-suse-3002.2-virt-network-311.patch | 8842 +++++++++++++++++ salt.changes | 41 + salt.spec | 21 + 10 files changed, 16101 insertions(+), 1 deletion(-) create mode 100644 add-patch-support-for-allow-vendor-change-option-wit.patch create mode 100644 add-pkg.services_need_restart-302.patch create mode 100644 fix-aptpkg.normalize_name-when-package-arch-is-all.patch create mode 100644 fix-salt.utils.stringutils.to_str-calls-to-make-it-w.patch create mode 100644 force-zyppnotify-to-prefer-packages.db-than-packages.patch create mode 100644 open-suse-3002.2-bigvm-310.patch create mode 100644 open-suse-3002.2-virt-network-311.patch diff --git a/_lastrevision b/_lastrevision index 844a1f8..4f844f6 100644 --- a/_lastrevision +++ b/_lastrevision @@ -1 +1 @@ -2a8598ff24165b69d93090c72aeedb6bf5e1c00f \ No newline at end of file +20d9e2d4f25a30971e2b5294d0f8124ab1205788 \ No newline at end of file diff --git a/add-patch-support-for-allow-vendor-change-option-wit.patch b/add-patch-support-for-allow-vendor-change-option-wit.patch new file mode 100644 index 0000000..ae6333b --- /dev/null +++ b/add-patch-support-for-allow-vendor-change-option-wit.patch @@ -0,0 +1,107 @@ +From cee4cc182b4740c912861c712dea7bc44eb70ffb Mon Sep 17 00:00:00 2001 +From: Martin Seidl +Date: Mon, 7 Dec 2020 01:10:51 +0100 +Subject: [PATCH] add patch support for allow vendor change option with + zypper + +--- + salt/modules/zypperpkg.py | 46 +++++++++++++++++++++++++++------------ + 1 file changed, 32 insertions(+), 14 deletions(-) + +diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py +index 6f22994bf0..4a5cb85e7c 100644 +--- a/salt/modules/zypperpkg.py ++++ b/salt/modules/zypperpkg.py +@@ -35,7 +35,6 @@ import salt.utils.versions + from salt.exceptions import CommandExecutionError, MinionError, SaltInvocationError + + # pylint: disable=import-error,redefined-builtin,no-name-in-module +-from salt.ext import six + from salt.ext.six.moves import configparser + from salt.ext.six.moves.urllib.parse import urlparse as _urlparse + from salt.utils.versions import LooseVersion +@@ -1431,6 +1430,7 @@ def install( + no_recommends=False, + root=None, + inclusion_detection=False, ++ novendorchange=True, + **kwargs + ): + """ +@@ -1478,6 +1478,10 @@ def install( + skip_verify + Skip the GPG verification check (e.g., ``--no-gpg-checks``) + ++ ++ novendorchange ++ Disallow vendor change ++ + version + Can be either a version number, or the combination of a comparison + operator (<, >, <=, >=, =) and a version number (ex. '>1.2.3-4'). +@@ -1638,6 +1642,22 @@ def install( + cmd_install.append( + kwargs.get("resolve_capabilities") and "--capability" or "--name" + ) ++ if novendorchange: ++ if __grains__["osrelease_info"][0] > 11: ++ cmd_install.append("--no-allow-vendor-change") ++ log.info("Disabling vendor changes") ++ else: ++ log.warning( ++ "Enabling/Disabling vendor changes is not supported on this Zypper version" ++ ) ++ else: ++ if __grains__["osrelease_info"][0] > 11: ++ cmd_install.append("--allow-vendor-change") ++ log.info("Enabling vendor changes") ++ else: ++ log.warning( ++ "Enabling/Disabling vendor changes is not supported on this Zypper version" ++ ) + + if not refresh: + cmd_install.insert(0, "--no-refresh") +@@ -1649,7 +1669,6 @@ def install( + cmd_install.extend(fromrepoopt) + if no_recommends: + cmd_install.append("--no-recommends") +- + errors = [] + + # Split the targets into batches of 500 packages each, so that +@@ -1793,19 +1812,18 @@ def upgrade( + cmd_update.extend(["--from" if dist_upgrade else "--repo", repo]) + log.info("Targeting repos: %s", fromrepo) + +- if dist_upgrade: +- # TODO: Grains validation should be moved to Zypper class +- if __grains__["osrelease_info"][0] > 11: +- if novendorchange: +- cmd_update.append("--no-allow-vendor-change") +- log.info("Disabling vendor changes") +- else: +- cmd_update.append("--allow-vendor-change") +- log.info("Enabling vendor changes") ++ # TODO: Grains validation should be moved to Zypper class ++ if __grains__["osrelease_info"][0] > 11: ++ if novendorchange: ++ cmd_update.append("--no-allow-vendor-change") ++ log.info("Disabling vendor changes") + else: +- log.warning( +- "Enabling/Disabling vendor changes is not supported on this Zypper version" +- ) ++ cmd_update.append("--allow-vendor-change") ++ log.info("Enabling vendor changes") ++ else: ++ log.warning( ++ "Enabling/Disabling vendor changes is not supported on this Zypper version" ++ ) + + if no_recommends: + cmd_update.append("--no-recommends") +-- +2.29.2 + + diff --git a/add-pkg.services_need_restart-302.patch b/add-pkg.services_need_restart-302.patch new file mode 100644 index 0000000..08ae31b --- /dev/null +++ b/add-pkg.services_need_restart-302.patch @@ -0,0 +1,404 @@ +From c79f4a8619ff1275b2ec4400c1fb27d24c22a7eb Mon Sep 17 00:00:00 2001 +From: Alexander Graul +Date: Tue, 8 Dec 2020 15:35:49 +0100 +Subject: [PATCH] Add pkg.services_need_restart (#302) + +* Add utils.systemd.pid_to_service function + +This function translates a given PID to the systemd service name in case +the process belongs to a running service. It uses DBUS for the +translation if DBUS is available, falling back to parsing +``systemctl status -o json'' output. + +* Add zypperpkg.services_need_restart + +pkg.services_need_restart returns a list of system services that were +affected by package manager operations such as updates, downgrades or +reinstallations without having been restarted. This might cause issues, +e.g. in the case a shared object was loaded by a process and then +replaced by the package manager. + +(cherry picked from commit b950fcdbd6cc8cb08e1413a0ed05e0ae21717cea) + +* Add aptpkg.services_need_restart + +pkg.services_need_restart returns a list of system services that were +affected by package manager operations such as updates, downgrades or +reinstallations without having been restarted. This might cause issues, +e.g. in the case a shared object was loaded by a process and then +replaced by the package manager. + +Requires checkrestart, which is part of the debian-goodies package and +available from official Ubuntu and Debian repositories. + +(cherry picked from commit b981f6ecb1a551b98c5cebab4975fc09c6a55a22) + +* Add yumpkg.services_need_restart + +pkg.services_need_restart returns a list of system services that were +affected by package manager operations such as updates, downgrades or +reinstallations without having been restarted. This might cause issues, +e.g. in the case a shared object was loaded by a process and then +replaced by the package manager. + +Requires dnf with the needs-restarting plugin, which is part of +dnf-plugins-core and installed by default on RHEL/CentOS/Fedora. +Also requires systemd for the mapping between PIDs and systemd services. + +(cherry picked from commit 5e2be1095729c9f73394e852b82749950957e6fb) + +* Add changelog entry for issue #58261 + +(cherry picked from commit 148877ed8ff7a47132c1186274739e648f7acf1c) + +* Simplify dnf needs-restarting output parsing + +Co-authored-by: Wayne Werner +(cherry picked from commit beb5d60f3cc64b880ec25ca188f8a73f6ec493dd) +--- + changelog/58261.added | 1 + + salt/modules/aptpkg.py | 42 ++++++++++++++++- + salt/modules/yumpkg.py | 36 +++++++++++++++ + salt/modules/zypperpkg.py | 25 ++++++++++ + salt/utils/systemd.py | 69 ++++++++++++++++++++++++++++ + tests/unit/modules/test_aptpkg.py | 22 ++++++++- + tests/unit/modules/test_yumpkg.py | 32 ++++++++++++- + tests/unit/modules/test_zypperpkg.py | 14 ++++++ + 8 files changed, 238 insertions(+), 3 deletions(-) + create mode 100644 changelog/58261.added + +diff --git a/changelog/58261.added b/changelog/58261.added +new file mode 100644 +index 0000000000..537a43e80d +--- /dev/null ++++ b/changelog/58261.added +@@ -0,0 +1 @@ ++Added ``pkg.services_need_restart`` which lists system services that should be restarted after package management operations. +diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py +index 03e99af733..a0e0cc30c1 100644 +--- a/salt/modules/aptpkg.py ++++ b/salt/modules/aptpkg.py +@@ -38,7 +38,12 @@ import salt.utils.stringutils + import salt.utils.systemd + import salt.utils.versions + import salt.utils.yaml +-from salt.exceptions import CommandExecutionError, MinionError, SaltInvocationError ++from salt.exceptions import ( ++ CommandExecutionError, ++ CommandNotFoundError, ++ MinionError, ++ SaltInvocationError, ++) + from salt.modules.cmdmod import _parse_env + + log = logging.getLogger(__name__) +@@ -3029,3 +3034,38 @@ def list_downloaded(root=None, **kwargs): + ).isoformat(), + } + return ret ++ ++ ++def services_need_restart(**kwargs): ++ """ ++ .. versionadded:: NEXT ++ ++ List services that use files which have been changed by the ++ package manager. It might be needed to restart them. ++ ++ Requires checkrestart from the debian-goodies package. ++ ++ CLI Examples: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.services_need_restart ++ """ ++ if not salt.utils.path.which_bin(["checkrestart"]): ++ raise CommandNotFoundError( ++ "'checkrestart' is needed. It is part of the 'debian-goodies' " ++ "package which can be installed from official repositories." ++ ) ++ ++ cmd = ["checkrestart", "--machine"] ++ services = set() ++ ++ cr_output = __salt__["cmd.run_stdout"](cmd, python_shell=False) ++ for line in cr_output.split("\n"): ++ if not line.startswith("SERVICE:"): ++ continue ++ end_of_name = line.find(",") ++ service = line[8:end_of_name] # skip "SERVICE:" ++ services.add(service) ++ ++ return list(services) +diff --git a/salt/modules/yumpkg.py b/salt/modules/yumpkg.py +index dd843f985b..df174e737d 100644 +--- a/salt/modules/yumpkg.py ++++ b/salt/modules/yumpkg.py +@@ -3434,3 +3434,39 @@ def del_repo_key(keyid, root=None, **kwargs): + + """ + return __salt__["lowpkg.remove_gpg_key"](keyid, root) ++ ++ ++def services_need_restart(**kwargs): ++ """ ++ .. versionadded:: NEXT ++ ++ List services that use files which have been changed by the ++ package manager. It might be needed to restart them. ++ ++ Requires systemd. ++ ++ CLI Examples: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.services_need_restart ++ """ ++ if _yum() != "dnf": ++ raise CommandExecutionError("dnf is required to list outdated services.") ++ if not salt.utils.systemd.booted(__context__): ++ raise CommandExecutionError("systemd is required to list outdated services.") ++ ++ cmd = ["dnf", "--quiet", "needs-restarting"] ++ dnf_output = __salt__["cmd.run_stdout"](cmd, python_shell=False) ++ if not dnf_output: ++ return [] ++ ++ services = set() ++ for line in dnf_output.split("\n"): ++ pid, has_delim, _ = line.partition(":") ++ if has_delim: ++ service = salt.utils.systemd.pid_to_service(pid.strip()) ++ if service: ++ services.add(service) ++ ++ return list(services) +diff --git a/salt/modules/zypperpkg.py b/salt/modules/zypperpkg.py +index 5e13c68708..6f22994bf0 100644 +--- a/salt/modules/zypperpkg.py ++++ b/salt/modules/zypperpkg.py +@@ -3092,3 +3092,28 @@ def del_repo_key(keyid, root=None, **kwargs): + + """ + return __salt__["lowpkg.remove_gpg_key"](keyid, root) ++ ++ ++def services_need_restart(root=None, **kwargs): ++ """ ++ .. versionadded:: NEXT ++ ++ List services that use files which have been changed by the ++ package manager. It might be needed to restart them. ++ ++ root ++ operate on a different root directory. ++ ++ CLI Examples: ++ ++ .. code-block:: bash ++ ++ salt '*' pkg.services_need_restart ++ ++ """ ++ cmd = ["ps", "-sss"] ++ ++ zypper_output = __zypper__(root=root).nolock.call(*cmd) ++ services = zypper_output.split() ++ ++ return services +diff --git a/salt/utils/systemd.py b/salt/utils/systemd.py +index 4d902bc920..f42d0421f8 100644 +--- a/salt/utils/systemd.py ++++ b/salt/utils/systemd.py +@@ -11,6 +11,12 @@ import salt.utils.path + import salt.utils.stringutils + from salt.exceptions import SaltInvocationError + ++try: ++ import dbus ++except ImportError: ++ dbus = None ++ ++ + log = logging.getLogger(__name__) + + +@@ -114,3 +120,66 @@ def has_scope(context=None): + if _sd_version is None: + return False + return _sd_version >= 205 ++ ++ ++def pid_to_service(pid): ++ """ ++ Check if a PID belongs to a systemd service and return its name. ++ Return None if the PID does not belong to a service. ++ ++ Uses DBUS if available. ++ """ ++ if dbus: ++ return _pid_to_service_dbus(pid) ++ else: ++ return _pid_to_service_systemctl(pid) ++ ++ ++def _pid_to_service_systemctl(pid): ++ systemd_cmd = ["systemctl", "--output", "json", "status", str(pid)] ++ try: ++ systemd_output = subprocess.run( ++ systemd_cmd, check=True, text=True, capture_output=True ++ ) ++ status_json = salt.utils.json.find_json(systemd_output.stdout) ++ except (ValueError, subprocess.CalledProcessError): ++ return None ++ ++ name = status_json.get("_SYSTEMD_UNIT") ++ if name and name.endswith(".service"): ++ return _strip_suffix(name) ++ else: ++ return None ++ ++ ++def _pid_to_service_dbus(pid): ++ """ ++ Use DBUS to check if a PID belongs to a running systemd service and return the service name if it does. ++ """ ++ bus = dbus.SystemBus() ++ systemd_object = bus.get_object( ++ "org.freedesktop.systemd1", "/org/freedesktop/systemd1" ++ ) ++ systemd = dbus.Interface(systemd_object, "org.freedesktop.systemd1.Manager") ++ try: ++ service_path = systemd.GetUnitByPID(pid) ++ service_object = bus.get_object("org.freedesktop.systemd1", service_path) ++ service_props = dbus.Interface( ++ service_object, "org.freedesktop.DBus.Properties" ++ ) ++ service_name = service_props.Get("org.freedesktop.systemd1.Unit", "Id") ++ name = str(service_name) ++ ++ if name and name.endswith(".service"): ++ return _strip_suffix(name) ++ else: ++ return None ++ except dbus.DBusException: ++ return None ++ ++ ++def _strip_suffix(service_name): ++ """ ++ Strip ".service" suffix from a given service name. ++ """ ++ return service_name[:-8] +diff --git a/tests/unit/modules/test_aptpkg.py b/tests/unit/modules/test_aptpkg.py +index eb3f9e2da7..1d4d2f7fdc 100644 +--- a/tests/unit/modules/test_aptpkg.py ++++ b/tests/unit/modules/test_aptpkg.py +@@ -13,7 +13,6 @@ import textwrap + import pytest + import salt.modules.aptpkg as aptpkg + from salt.exceptions import CommandExecutionError, SaltInvocationError +-from salt.ext import six + from tests.support.mixins import LoaderModuleMockMixin + from tests.support.mock import MagicMock, Mock, call, patch + from tests.support.unit import TestCase, skipIf +@@ -1001,3 +1000,24 @@ class AptUtilsTestCase(TestCase, LoaderModuleMockMixin): + # We should attempt to call the cmd 5 times + self.assertEqual(cmd_mock.call_count, 5) + cmd_mock.has_calls(expected_calls) ++ ++ @patch("salt.utils.path.which_bin", Mock(return_value="/usr/sbin/checkrestart")) ++ def test_services_need_restart(self): ++ """ ++ Test that checkrestart output is parsed correctly ++ """ ++ cr_output = """ ++PROCESSES: 24 ++PROGRAMS: 17 ++PACKAGES: 8 ++SERVICE:rsyslog,385,/usr/sbin/rsyslogd ++SERVICE:cups-daemon,390,/usr/sbin/cupsd ++ """ ++ ++ with patch.dict( ++ aptpkg.__salt__, {"cmd.run_stdout": Mock(return_value=cr_output)} ++ ): ++ assert sorted(aptpkg.services_need_restart()) == [ ++ "cups-daemon", ++ "rsyslog", ++ ] +diff --git a/tests/unit/modules/test_yumpkg.py b/tests/unit/modules/test_yumpkg.py +index e65a1f8b8b..b97e82d307 100644 +--- a/tests/unit/modules/test_yumpkg.py ++++ b/tests/unit/modules/test_yumpkg.py +@@ -7,7 +7,7 @@ import salt.modules.yumpkg as yumpkg + import salt.utils.platform + from salt.exceptions import CommandExecutionError, SaltInvocationError + from tests.support.mixins import LoaderModuleMockMixin +-from tests.support.mock import MagicMock, Mock, mock_open, patch ++from tests.support.mock import MagicMock, Mock, call, mock_open, patch + from tests.support.unit import TestCase, skipIf + + try: +@@ -1745,3 +1745,33 @@ class YumUtilsTestCase(TestCase, LoaderModuleMockMixin): + python_shell=True, + username="Darth Vader", + ) ++ ++ @skipIf(not salt.utils.systemd.booted(), "Requires systemd") ++ @patch("salt.modules.yumpkg._yum", Mock(return_value="dnf")) ++ def test_services_need_restart(self): ++ """ ++ Test that dnf needs-restarting output is parsed and ++ salt.utils.systemd.pid_to_service is called as expected. ++ """ ++ expected = ["firewalld", "salt-minion"] ++ ++ dnf_mock = Mock( ++ return_value="123 : /usr/bin/firewalld\n456 : /usr/bin/salt-minion\n" ++ ) ++ systemd_mock = Mock(side_effect=["firewalld", "salt-minion"]) ++ with patch.dict(yumpkg.__salt__, {"cmd.run_stdout": dnf_mock}), patch( ++ "salt.utils.systemd.pid_to_service", systemd_mock ++ ): ++ assert sorted(yumpkg.services_need_restart()) == expected ++ systemd_mock.assert_has_calls([call("123"), call("456")]) ++ ++ @patch("salt.modules.yumpkg._yum", Mock(return_value="dnf")) ++ def test_services_need_restart_requires_systemd(self): ++ """Test that yumpkg.services_need_restart raises an error if systemd is unavailable.""" ++ with patch("salt.utils.systemd.booted", Mock(return_value=False)): ++ pytest.raises(CommandExecutionError, yumpkg.services_need_restart) ++ ++ @patch("salt.modules.yumpkg._yum", Mock(return_value="yum")) ++ def test_services_need_restart_requires_dnf(self): ++ """Test that yumpkg.services_need_restart raises an error if DNF is unavailable.""" ++ pytest.raises(CommandExecutionError, yumpkg.services_need_restart) +diff --git a/tests/unit/modules/test_zypperpkg.py b/tests/unit/modules/test_zypperpkg.py +index 018c1ffbca..9c4a224c55 100644 +--- a/tests/unit/modules/test_zypperpkg.py ++++ b/tests/unit/modules/test_zypperpkg.py +@@ -2213,3 +2213,17 @@ pattern() = package-c""" + with patch.dict(zypper.__salt__, salt_mock): + self.assertTrue(zypper.del_repo_key(keyid="keyid", root="/mnt")) + salt_mock["lowpkg.remove_gpg_key"].assert_called_once_with("keyid", "/mnt") ++ ++ def test_services_need_restart(self): ++ """ ++ Test that zypper ps is used correctly to list services that need to ++ be restarted. ++ """ ++ expected = ["salt-minion", "firewalld"] ++ zypper_output = "salt-minion\nfirewalld" ++ zypper_mock = Mock() ++ zypper_mock(root=None).nolock.call = Mock(return_value=zypper_output) ++ ++ with patch("salt.modules.zypperpkg.__zypper__", zypper_mock): ++ assert zypper.services_need_restart() == expected ++ zypper_mock(root=None).nolock.call.assert_called_with("ps", "-sss") +-- +2.29.2 + + diff --git a/fix-aptpkg.normalize_name-when-package-arch-is-all.patch b/fix-aptpkg.normalize_name-when-package-arch-is-all.patch new file mode 100644 index 0000000..85d98de --- /dev/null +++ b/fix-aptpkg.normalize_name-when-package-arch-is-all.patch @@ -0,0 +1,42 @@ +From 763d63b72b9a20f22555b665033899e10f091b60 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= + +Date: Mon, 11 Jan 2021 15:45:28 +0000 +Subject: [PATCH] Fix aptpkg.normalize_name when package arch is 'all' + +Add test case of DEB package where arch is 'all' +--- + salt/modules/aptpkg.py | 2 +- + tests/unit/modules/test_aptpkg.py | 2 ++ + 2 files changed, 3 insertions(+), 1 deletion(-) + +diff --git a/salt/modules/aptpkg.py b/salt/modules/aptpkg.py +index e001d2f11c..03e99af733 100644 +--- a/salt/modules/aptpkg.py ++++ b/salt/modules/aptpkg.py +@@ -208,7 +208,7 @@ def normalize_name(name): + pkgname = name + pkgarch = __grains__["osarch"] + +- return pkgname if pkgarch in (__grains__["osarch"], "any") else name ++ return pkgname if pkgarch in (__grains__["osarch"], "all", "any") else name + + + def parse_arch(name): +diff --git a/tests/unit/modules/test_aptpkg.py b/tests/unit/modules/test_aptpkg.py +index 51dfce29eb..eb3f9e2da7 100644 +--- a/tests/unit/modules/test_aptpkg.py ++++ b/tests/unit/modules/test_aptpkg.py +@@ -808,6 +808,8 @@ class AptPkgTestCase(TestCase, LoaderModuleMockMixin): + assert result == "foo", result + result = aptpkg.normalize_name("foo:any") + assert result == "foo", result ++ result = aptpkg.normalize_name("foo:all") ++ assert result == "foo", result + result = aptpkg.normalize_name("foo:i386") + assert result == "foo:i386", result + +-- +2.29.2 + + diff --git a/fix-salt.utils.stringutils.to_str-calls-to-make-it-w.patch b/fix-salt.utils.stringutils.to_str-calls-to-make-it-w.patch new file mode 100644 index 0000000..8bed8a5 --- /dev/null +++ b/fix-salt.utils.stringutils.to_str-calls-to-make-it-w.patch @@ -0,0 +1,99 @@ +From 435d9fbee299b06e1c58cdc0574b6a1975841879 Mon Sep 17 00:00:00 2001 +From: Victor Zhestkov +Date: Wed, 25 Nov 2020 15:09:41 +0300 +Subject: [PATCH] Fix salt.utils.stringutils.to_str calls to make it + working with numeric uid/gid + +--- + salt/modules/file.py | 16 ++++++++++------ + salt/states/file.py | 11 +++++++++-- + 2 files changed, 19 insertions(+), 8 deletions(-) + +diff --git a/salt/modules/file.py b/salt/modules/file.py +index b830b390d3..b9744393d7 100644 +--- a/salt/modules/file.py ++++ b/salt/modules/file.py +@@ -4970,6 +4970,12 @@ def check_perms( + is_dir = os.path.isdir(name) + is_link = os.path.islink(name) + ++ def __safe_to_str(s): ++ try: ++ return salt.utils.stringutils.to_str(s) ++ except: ++ return salt.utils.stringutils.to_str(str(s)) ++ + # user/group changes if needed, then check if it worked + if user: + if isinstance(user, int): +@@ -4979,7 +4985,7 @@ def check_perms( + and user_to_uid(user) != user_to_uid(perms["luser"]) + ) or ( + not salt.utils.platform.is_windows() +- and salt.utils.stringutils.to_str(user) != perms["luser"] ++ and __safe_to_str(user) != perms["luser"] + ): + perms["cuser"] = user + +@@ -4991,7 +4997,7 @@ def check_perms( + and group_to_gid(group) != group_to_gid(perms["lgroup"]) + ) or ( + not salt.utils.platform.is_windows() +- and salt.utils.stringutils.to_str(group) != perms["lgroup"] ++ and __safe_to_str(group) != perms["lgroup"] + ): + perms["cgroup"] = group + +@@ -5023,8 +5029,7 @@ def check_perms( + and user != "" + ) or ( + not salt.utils.platform.is_windows() +- and salt.utils.stringutils.to_str(user) +- != get_user(name, follow_symlinks=follow_symlinks) ++ and __safe_to_str(user) != get_user(name, follow_symlinks=follow_symlinks) + and user != "" + ): + if __opts__["test"] is True: +@@ -5045,8 +5050,7 @@ def check_perms( + and group != "" + ) or ( + not salt.utils.platform.is_windows() +- and salt.utils.stringutils.to_str(group) +- != get_group(name, follow_symlinks=follow_symlinks) ++ and __safe_to_str(group) != get_group(name, follow_symlinks=follow_symlinks) + and group != "" + ): + if __opts__["test"] is True: +diff --git a/salt/states/file.py b/salt/states/file.py +index 89c70eb454..fd8ffde757 100644 +--- a/salt/states/file.py ++++ b/salt/states/file.py +@@ -989,15 +989,22 @@ def _check_dir_meta(name, user, group, mode, follow_symlinks=False): + if not stats: + changes["directory"] = "new" + return changes ++ ++ def __safe_to_str(s): ++ try: ++ return salt.utils.stringutils.to_str(s) ++ except: ++ return salt.utils.stringutils.to_str(str(s)) ++ + if ( + user is not None +- and salt.utils.stringutils.to_str(user) != stats["user"] ++ and __safe_to_str(user) != stats["user"] + and user != stats.get("uid") + ): + changes["user"] = user + if ( + group is not None +- and salt.utils.stringutils.to_str(group) != stats["group"] ++ and __safe_to_str(group) != stats["group"] + and group != stats.get("gid") + ): + changes["group"] = group +-- +2.29.2 + + diff --git a/force-zyppnotify-to-prefer-packages.db-than-packages.patch b/force-zyppnotify-to-prefer-packages.db-than-packages.patch new file mode 100644 index 0000000..6203d03 --- /dev/null +++ b/force-zyppnotify-to-prefer-packages.db-than-packages.patch @@ -0,0 +1,29 @@ +From 36b107fb5108fe4e52e9ef522765d6ada588c50d Mon Sep 17 00:00:00 2001 +From: Victor Zhestkov +Date: Wed, 9 Dec 2020 14:58:55 +0300 +Subject: [PATCH] Force zyppnotify to prefer Packages.db than Packages + if it exists + +--- + scripts/suse/zypper/plugins/commit/zyppnotify | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/scripts/suse/zypper/plugins/commit/zyppnotify b/scripts/suse/zypper/plugins/commit/zyppnotify +index 51ac02254e..d6a1bef42b 100755 +--- a/scripts/suse/zypper/plugins/commit/zyppnotify ++++ b/scripts/suse/zypper/plugins/commit/zyppnotify +@@ -20,7 +20,9 @@ class DriftDetector(Plugin): + def __init__(self): + Plugin.__init__(self) + self.ck_path = "/var/cache/salt/minion/rpmdb.cookie" +- self.rpm_path = "/var/lib/rpm/Packages" ++ self.rpm_path = "/var/lib/rpm/Packages.db" ++ if not os.path.exists(self.rpm_path): ++ self.rpm_path = "/var/lib/rpm/Packages" + + def _get_mtime(self): + """ +-- +2.29.2 + + diff --git a/open-suse-3002.2-bigvm-310.patch b/open-suse-3002.2-bigvm-310.patch new file mode 100644 index 0000000..cf80e59 --- /dev/null +++ b/open-suse-3002.2-bigvm-310.patch @@ -0,0 +1,6515 @@ +From 0d606b481752d1112321046ce78d3a7f9d2a6604 Mon Sep 17 00:00:00 2001 +From: Cedric Bosdonnat +Date: Tue, 12 Jan 2021 10:48:27 +0100 +Subject: [PATCH] Open suse 3002.2 bigvm (#310) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +* revert stop_on_reboot commits to help applying upstream patches + +* libvirt domain template memory config fixes + +Add unit tests for _gen_xml() on the recently added memory parameters. +Also fixes an issue with an optional attribute. + +* virt: support host numa tunning capability + +* fixup! precommit failure fix + +* virt: support cpu model and topology + +* virt: make context preprocessing more reusable in _gen_xml + +Introduce mapping structures in order to help reusing the common patterns +in the virt._gen_xml() context pre processing. + +* xmlutil.change_xml properly handle xpath node number + +In XPath the node numbers are counted from 1 rather than 0. +Thus /foo/bar[0] is invalid and should be /foo/bar[1]. + +Since in the change_xml function we are getting the index from python +lists in these cases, we need to offset these. + +* virt: support memory_backing + +* virt: support cpu tunning and Iothread allocation + +* xmlutil.change_xml: properly handle updated return value for removals + +When deleting an attribute that doesn't exist in the node we should not +report a change was made. + +* virt.update: properly handle nosharepages and locked elements + +When updating we shouldn't set the value as text in those elements. +Libvirt seems happy with it, but it forces modifying the VM definition +even if there was no change. + +* xmlutil: use a comparison function to update XML + +When updating an XML file, we may need to have a more intelligent +comparison of the current and new values. This typically fits for the +case of numeric values that may have a negligible delta. + +* virt.update: handle tiny difference in memory values + +Libvirt may round the memory values when defining or updating a VM. That +is perfectly fine, but then the value are slightly different from the +ones passed to the virt.update() function or the virt.running state. +In those cases the state would be reapplied even though there is no real +difference with the VM. + +In order to handle that case the memory parameters in the virt.update +mapping now have a comparison function that considers the tiny differences +as equal. + +This commit also factorizes the creation of the memory entries in the +virt.update() mapping. + +* virt.update: factorize the mapping value definition + +In the mapping passed to xmlutil.change_xml() in virt.update() there are +a lot of common patterns. Extract these into helper functions. Some of +them are common enough to even be defined in the xmlutil module. + +* virt: add kvm-hint-dedicated feature handling + +* virt: add clock configuration for guests + +* virt: add qemu guest agent channel + +For libvirt to be able to communicate with the QEMU Guest Agent if +installed in the guest, a channel named org.qemu.guest_agent.0 is +needed. + +Add this channel by default on all newly created KVM virtual machines. + +* virt: allow using IO threads on disks + +* Remove unneeded VM XML definition fragments in tests + +* virt: canonicalize cpuset before comparing + +Multiple libvirt cpuset notations can designate the same thing. We need +to expand those notations into an actual cpu list in order to be able to +properly compare. + +For instance if the libvirt definition has '0-5,^4', and we have +'0,1,2,3,5' passed to virt.update(), those should not trigger an update +of the définition since they are defining the same thing. + +* virt: only live update vcpu max if there is a change + +* Add console and serial to update and running status + +* virt: cleanup the consoles and serials support + +* virt: add stop_on_reboot parameter in guest states and definition + +It can be needed to force a VM to stop instead of rebooting. A typical +example of this is when creating a VM using a install CDROM ISO or when +using an autoinstallation profile. Forcing a shutdown allows libvirt to +pick up another XML definition for the new start to remove the +firstboot-only options. + +* virt: expose live parameter in virt.defined state + +Allow updating the definition of a VM without touching the live +instance. This can be helpful since live update may change the device +names in the guest. + +* Ensure virt.update stop_on_reboot is updated with its default value + +While all virt.update properties default values should not be used when +updating the XML definition, the stop_on_reboot default value (False) +needs to be passed still or the user will never be able to update with +this value. + +Co-authored-by: gqlo +Co-authored-by: gqlo +Co-authored-by: marina2209 +--- + changelog/57880.added | 1 + + changelog/58844.added | 1 + + salt/modules/virt.py | 1232 ++++++- + salt/states/virt.py | 341 +- + salt/templates/virt/libvirt_chardevs.jinja | 16 + + salt/templates/virt/libvirt_domain.jinja | 268 +- + salt/utils/xmlutil.py | 79 +- + tests/pytests/unit/modules/virt/conftest.py | 126 + + .../pytests/unit/modules/virt/test_domain.py | 335 ++ + tests/pytests/unit/utils/test_xmlutil.py | 41 + + tests/unit/modules/test_virt.py | 2961 +++++++++++++++-- + tests/unit/states/test_virt.py | 57 + + 12 files changed, 4934 insertions(+), 524 deletions(-) + create mode 100644 changelog/57880.added + create mode 100644 changelog/58844.added + create mode 100644 salt/templates/virt/libvirt_chardevs.jinja + +diff --git a/changelog/57880.added b/changelog/57880.added +new file mode 100644 +index 0000000000..6fff4295fa +--- /dev/null ++++ b/changelog/57880.added +@@ -0,0 +1 @@ ++CPU model, topology and NUMA node tuning +diff --git a/changelog/58844.added b/changelog/58844.added +new file mode 100644 +index 0000000000..c8599125d2 +--- /dev/null ++++ b/changelog/58844.added +@@ -0,0 +1 @@ ++Enhance console and serial support in virt module +diff --git a/salt/modules/virt.py b/salt/modules/virt.py +index 786bfa1e58..b852f8175d 100644 +--- a/salt/modules/virt.py ++++ b/salt/modules/virt.py +@@ -788,11 +788,11 @@ def _handle_unit(s, def_unit="m"): + return int(value) + + +-def nesthash(): ++def nesthash(value=None): + """ + create default dict that allows arbitrary level of nesting + """ +- return collections.defaultdict(nesthash) ++ return collections.defaultdict(nesthash, value or {}) + + + def _gen_xml( +@@ -808,6 +808,11 @@ def _gen_xml( + graphics=None, + boot=None, + boot_dev=None, ++ numatune=None, ++ hypervisor_features=None, ++ clock=None, ++ serials=None, ++ consoles=None, + stop_on_reboot=False, + **kwargs + ): +@@ -817,24 +822,36 @@ def _gen_xml( + context = { + "hypervisor": hypervisor, + "name": name, +- "cpu": str(cpu), ++ "hypervisor_features": hypervisor_features or {}, ++ "clock": clock or {}, + "on_reboot": "destroy" if stop_on_reboot else "restart", + } + ++ context["to_kib"] = lambda v: int(_handle_unit(v) / 1024) ++ context["yesno"] = lambda v: "yes" if v else "no" ++ + context["mem"] = nesthash() + if isinstance(mem, int): +- mem = int(mem) * 1024 # MB +- context["mem"]["boot"] = str(mem) +- context["mem"]["current"] = str(mem) ++ context["mem"]["boot"] = mem ++ context["mem"]["current"] = mem + elif isinstance(mem, dict): +- for tag, val in mem.items(): +- if val: +- if tag == "slots": +- context["mem"]["slots"] = "{}='{}'".format(tag, val) +- else: +- context["mem"][tag] = str(int(_handle_unit(val) / 1024)) ++ context["mem"] = nesthash(mem) ++ ++ context["cpu"] = nesthash() ++ context["cputune"] = nesthash() ++ if isinstance(cpu, int): ++ context["cpu"]["maximum"] = str(cpu) ++ elif isinstance(cpu, dict): ++ context["cpu"] = nesthash(cpu) ++ ++ if clock: ++ offset = "utc" if clock.get("utc", True) else "localtime" ++ if "timezone" in clock: ++ offset = "timezone" ++ context["clock"]["offset"] = offset + + if hypervisor in ["qemu", "kvm"]: ++ context["numatune"] = numatune if numatune else {} + context["controller_model"] = False + elif hypervisor == "vmware": + # TODO: make bus and model parameterized, this works for 64-bit Linux +@@ -873,18 +890,57 @@ def _gen_xml( + 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"] +- if "serial_type" in context and context["serial_type"] == "tcp": +- if "telnet_port" in kwargs: +- context["telnet_port"] = kwargs["telnet_port"] +- else: +- context["telnet_port"] = 23023 # FIXME: use random unused port +- if "serial_type" in context: +- if "console" in kwargs: +- context["console"] = kwargs["console"] +- else: +- context["console"] = True ++ default_port = 23023 ++ default_chardev_type = "tcp" ++ ++ chardev_types = ["serial", "console"] ++ for chardev_type in chardev_types: ++ context[chardev_type + "s"] = [] ++ parameter_value = locals()[chardev_type + "s"] ++ if parameter_value is not None: ++ for chardev in parameter_value: ++ chardev_context = chardev ++ chardev_context["type"] = chardev.get("type", default_chardev_type) ++ ++ if chardev_context["type"] == "tcp": ++ chardev_context["port"] = chardev.get("port", default_port) ++ chardev_context["protocol"] = chardev.get("protocol", "telnet") ++ context[chardev_type + "s"].append(chardev_context) ++ ++ # processing of deprecated parameters ++ old_port = kwargs.get("telnet_port") ++ if old_port: ++ salt.utils.versions.warn_until( ++ "Phosphorus", ++ "'telnet_port' parameter has been deprecated, use the 'serials' and 'consoles' parameters instead. " ++ "'telnet_port' parameter has been deprecated, use the 'serials' parameter with a value " ++ "like ``{{{{'type': 'tcp', 'protocol': 'telnet', 'port': {}}}}}`` instead and a similar `consoles` parameter. " ++ "It will be removed in {{version}}.".format(old_port), ++ ) ++ ++ old_serial_type = kwargs.get("serial_type") ++ if old_serial_type: ++ salt.utils.versions.warn_until( ++ "Phosphorus", ++ "'serial_type' parameter has been deprecated, use the 'serials' parameter with a value " ++ "like ``{{{{'type': '{}', 'protocol': 'telnet' }}}}`` instead and a similar `consoles` parameter. " ++ "It will be removed in {{version}}.".format(old_serial_type), ++ ) ++ serial_context = {"type": old_serial_type} ++ if serial_context["type"] == "tcp": ++ serial_context["port"] = old_port or default_port ++ serial_context["protocol"] = "telnet" ++ context["serials"].append(serial_context) ++ ++ old_console = kwargs.get("console") ++ if old_console: ++ salt.utils.versions.warn_until( ++ "Phosphorus", ++ "'console' parameter has been deprecated, use the 'serials' and 'consoles' parameters instead. " ++ "It will be removed in {version}.", ++ ) ++ if old_console is True: ++ context["consoles"].append(serial_context) + + context["disks"] = [] + disk_bus_map = {"virtio": "vd", "xen": "xvd", "fdc": "fd", "ide": "hd"} +@@ -897,6 +953,7 @@ def _gen_xml( + "disk_bus": disk["model"], + "format": disk.get("format", "raw"), + "index": str(i), ++ "io": "threads" if disk.get("iothreads", False) else "native", + } + targets.append(disk_context["target_dev"]) + if disk.get("source_file"): +@@ -946,7 +1003,6 @@ def _gen_xml( + + context["os_type"] = os_type + context["arch"] = arch +- + fn_ = "libvirt_domain.jinja" + try: + template = JINJA.get_template(fn_) +@@ -1751,6 +1807,11 @@ def init( + arch=None, + boot=None, + boot_dev=None, ++ numatune=None, ++ hypervisor_features=None, ++ clock=None, ++ serials=None, ++ consoles=None, + stop_on_reboot=False, + **kwargs + ): +@@ -1758,13 +1819,126 @@ def init( + Initialize a new vm + + :param name: name of the virtual machine to create +- :param cpu: Number of virtual CPUs to assign to the virtual machine +- :param mem: Amount of memory to allocate to the virtual machine in MiB. Since Magnesium, a dictionary can be used to ++ :param cpu: ++ Number of virtual CPUs to assign to the virtual machine or a dictionary with detailed information to configure ++ cpu model and topology, numa node tuning, cpu tuning and iothreads allocation. The structure of the dictionary is ++ documented in :ref:`init-cpu-def`. ++ ++ .. code-block:: yaml ++ ++ cpu: ++ placement: static ++ cpuset: 0-11 ++ current: 5 ++ maximum: 12 ++ vcpus: ++ 0: ++ enabled: True ++ hotpluggable: False ++ order: 1 ++ 1: ++ enabled: False ++ hotpluggable: True ++ match: minimum ++ mode: custom ++ check: full ++ vendor: Intel ++ model: ++ name: core2duo ++ fallback: allow ++ vendor_id: GenuineIntel ++ topology: ++ sockets: 1 ++ cores: 12 ++ threads: 1 ++ cache: ++ level: 3 ++ mode: emulate ++ features: ++ lahf: optional ++ pcid: require ++ numa: ++ 0: ++ cpus: 0-3 ++ memory: 1g ++ discard: True ++ distances: ++ 0: 10 # sibling id : value ++ 1: 21 ++ 2: 31 ++ 3: 41 ++ 1: ++ cpus: 4-6 ++ memory: 1g ++ memAccess: shared ++ distances: ++ 0: 21 ++ 1: 10 ++ 2: 21 ++ 3: 31 ++ tuning: ++ vcpupin: ++ 0: 1-4,^2 # vcpuid : cpuset ++ 1: 0,1 ++ 2: 2,3 ++ 3: 0,4 ++ emulatorpin: 1-3 ++ iothreadpin: ++ 1: 5,6 # iothread id: cpuset ++ 2: 7,8 ++ shares: 2048 ++ period: 1000000 ++ quota: -1 ++ global_period: 1000000 ++ global_quota: -1 ++ emulator_period: 1000000 ++ emulator_quota: -1 ++ iothread_period: 1000000 ++ iothread_quota: -1 ++ vcpusched: ++ - scheduler: fifo ++ priority: 1 ++ vcpus: 0,3-5 ++ - scheduler: rr ++ priority: 3 ++ iothreadsched: ++ - scheduler: idle ++ - scheduler: batch ++ iothreads: 2,3 ++ emulatorsched: ++ - scheduler: batch ++ cachetune: ++ 0-3: # vcpus set ++ 0: # cache id ++ level: 3 ++ type: both ++ size: 4 ++ 1: ++ level: 3 ++ type: both ++ size: 6 ++ monitor: ++ 1: 3 ++ 0-3: 3 ++ 4-5: ++ monitor: ++ 4: 3 # vcpus: level ++ 5: 3 ++ memorytune: ++ 0-3: # vcpus set ++ 0: 60 # node id: bandwidth ++ 4-5: ++ 0: 60 ++ iothreads: 4 ++ ++ .. versionadded:: Aluminium ++ ++ :param mem: Amount of memory to allocate to the virtual machine in MiB. Since 3002, a dictionary can be used to + contain detailed configuration which support memory allocation or tuning. Supported parameters are ``boot``, +- ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit`` and ``min_guarantee``. The +- structure of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. +- Detail unit specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be +- an integer. ++ ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit``, ``min_guarantee``, ++ ``hugepages`` , ``nosharepages``, ``locked``, ``source``, ``access``, ``allocation`` and ``discard``. The structure ++ of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. Detail unit ++ specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be an integer. + + .. code-block:: python + +@@ -1773,10 +1947,17 @@ def init( + 'current': 1g, + 'max': 1g, + 'slots': 10, +- 'hard_limit': '1024' +- 'soft_limit': '512m' +- 'swap_hard_limit': '1g' +- 'min_guarantee': '512mib' ++ 'hard_limit': '1024', ++ 'soft_limit': '512m', ++ 'swap_hard_limit': '1g', ++ 'min_guarantee': '512mib', ++ 'hugepages': [{'nodeset': '0-3,^2', 'size': '1g'}, {'nodeset': '2', 'size': '2m'}], ++ 'nosharepages': True, ++ 'locked': True, ++ 'source': 'file', ++ 'access': 'shared', ++ 'allocation': 'immediate', ++ 'discard': True + } + + .. versionchanged:: Magnesium +@@ -1872,6 +2053,232 @@ def init( + + By default, the value will ``"hd"``. + ++ :param numatune: ++ The optional numatune element provides details of how to tune the performance of a NUMA host via controlling NUMA ++ policy for domain process. The optional ``memory`` element specifies how to allocate memory for the domain process ++ on a NUMA host. ``memnode`` elements can specify memory allocation policies per each guest NUMA node. The definition ++ used in the dictionary can be found at :ref:`init-cpu-def`. ++ ++ .. versionadded:: Aluminium ++ ++ .. code-block:: python ++ ++ { ++ 'memory': {'mode': 'strict', 'nodeset': '0-11'}, ++ 'memnodes': {0: {'mode': 'strict', 'nodeset': 1}, 1: {'mode': 'preferred', 'nodeset': 2}} ++ } ++ ++ :param hypervisor_features: ++ Enable or disable hypervisor-specific features on the virtual machine. ++ ++ .. versionadded:: Aluminium ++ ++ .. code-block:: yaml ++ ++ hypervisor_features: ++ kvm-hint-dedicated: True ++ ++ :param clock: ++ Configure the guest clock. ++ The value is a dictionary with the following keys: ++ ++ adjustment ++ time adjustment in seconds or ``reset`` ++ ++ utc ++ set to ``False`` to use the host local time as the guest clock. Defaults to ``True``. ++ ++ timezone ++ synchronize the guest to the correspding timezone ++ ++ timers ++ a dictionary associating the timer name with its configuration. ++ This configuration is a dictionary with the properties ``track``, ``tickpolicy``, ++ ``catchup``, ``frequency``, ``mode``, ``present``, ``slew``, ``threshold`` and ``limit``. ++ See `libvirt time keeping documentation `_ for the possible values. ++ ++ .. versionadded:: Aluminium ++ ++ Set the clock to local time using an offset in seconds ++ .. code-block:: yaml ++ ++ clock: ++ adjustment: 3600 ++ utc: False ++ ++ Set the clock to a specific time zone: ++ ++ .. code-block:: yaml ++ ++ clock: ++ timezone: CEST ++ ++ Tweak guest timers: ++ ++ .. code-block:: yaml ++ ++ clock: ++ timers: ++ tsc: ++ frequency: 3504000000 ++ mode: native ++ rtc: ++ track: wall ++ tickpolicy: catchup ++ slew: 4636 ++ threshold: 123 ++ limit: 2342 ++ hpet: ++ present: False ++ ++ :param serials: ++ Dictionary providing details on the serials connection to create. (Default: ``None``) ++ See :ref:`init-chardevs-def` for more details on the possible values. ++ ++ .. versionadded:: Aluminium ++ ++ :param consoles: ++ Dictionary providing details on the consoles device to create. (Default: ``None``) ++ See :ref:`init-chardevs-def` for more details on the possible values. ++ ++ .. versionadded:: Aluminium ++ ++ .. _init-cpu-def: ++ ++ .. rubric:: cpu parameters definition ++ ++ The cpu parameters dictionary can contain the following properties: ++ ++ cpuset ++ a comma-separated list of physical CPU numbers that domain process and virtual CPUs can be pinned to by default. ++ eg. ``1-4,^3`` cpuset 3 is excluded. ++ ++ current ++ the number of virtual cpus available at startup ++ ++ placement ++ indicate the CPU placement mode for domain process. the value can be either ``static`` or ``auto`` ++ ++ vcpus ++ specify the state of individual vcpu. Possible attribute for each individual vcpu include: ``id``, ``enabled``, ++ ``hotpluggable`` and ``order``. Valid ``ids`` are from 0 to the maximum vCPU count minus 1. ``enabled`` takes ++ boolean values which controls the state of the vcpu. ``hotpluggable`` take boolean value which controls whether ++ given vCPU can be hotplugged and hotunplugged. ``order`` takes an integer value which specifies the order to add ++ the online vCPUs. ++ ++ match ++ The cpu attribute ``match`` attribute specifies how strictly the virtual CPU provided to the guest matches the CPU ++ requirements, possible values are ``minimum``, ``exact`` or ``strict``. ++ ++ check ++ Optional cpu attribute ``check`` attribute can be used to request a specific way of checking whether the virtual ++ CPU matches the specification, possible values are ``none``, ``partial`` and ``full``. ++ ++ mode ++ Optional cpu attribute ``mode`` attribute may be used to make it easier to configure a guest CPU to be as close ++ to host CPU as possible, possible values are ``custom``, ``host-model`` and ``host-passthrough``. ++ ++ model ++ specifies CPU model requested by the guest. An optional ``fallback`` attribute can be used to forbid libvirt falls ++ back to the closest model supported by the hypervisor, possible values are ``allow`` or ``forbid``. ``vendor_id`` ++ attribute can be used to set the vendor id seen by the guest, the length must be exactly 12 characters long. ++ ++ vendor ++ specifies CPU vendor requested by the guest. ++ ++ topology ++ specifies requested topology of virtual CPU provided to the guest. Four possible attributes , ``sockets``, ``dies``, ++ ``cores``, and ``threads``, accept non-zero positive integer values. They refer to the number of CPU sockets per ++ NUMA node, number of dies per socket, number of cores per die, and number of threads per core, respectively. ++ ++ features ++ A dictionary conains a set of cpu features to fine-tune features provided by the selected CPU model. Use cpu ++ feature ``name`` as the key and the ``policy`` as the value. ``policy`` Attribute takes ``force``, ``require``, ++ ``optional``, ``disable`` or ``forbid``. ++ ++ cache ++ describes the virtual CPU cache. Optional attribute ``level`` takes an integer value which describes cache level ++ ``mode`` attribute supported three possible values: ``emulate``, ``passthrough``, ``disable`` ++ ++ numa ++ specify the guest numa topology. ``cell`` element specifies a NUMA cell or a NUMA node, ``cpus`` specifies the ++ CPU or range of CPUs that are part of the node, ``memory`` specifies the size of the node memory. All cells ++ should have ``id`` attribute in case referring to some cell is necessary in the code. optional attribute ++ ``memAccess`` control whether the memory is to be mapped as ``shared`` or ``private``, ``discard`` attribute which ++ fine tunes the discard feature for given numa node, possible values are ``True`` or ``False``. ``distances`` ++ element define the distance between NUMA cells and ``sibling`` sub-element is used to specify the distance value ++ between sibling NUMA cells. ++ ++ vcpupin ++ The optional vcpupin element specifies which of host's physical CPUs the domain vCPU will be pinned to. ++ ++ emulatorpin ++ The optional emulatorpin element specifies which of host physical CPUs the "emulator", a subset of a domain not ++ including vCPU or iothreads will be pinned to. ++ ++ iothreadpin ++ The optional iothreadpin element specifies which of host physical CPUs the IOThreads will be pinned to. ++ ++ shares ++ The optional shares element specifies the proportional weighted share for the domain. ++ ++ period ++ The optional period element specifies the enforcement interval (unit: microseconds). ++ ++ quota ++ The optional quota element specifies the maximum allowed bandwidth (unit: microseconds). ++ ++ global_period ++ The optional global_period element specifies the enforcement CFS scheduler interval (unit: microseconds) for the ++ whole domain in contrast with period which enforces the interval per vCPU. ++ ++ global_quota ++ The optional global_quota element specifies the maximum allowed bandwidth (unit: microseconds) within a period ++ for the whole domain. ++ ++ emulator_period ++ The optional emulator_period element specifies the enforcement interval (unit: microseconds). ++ ++ emulator_quota ++ The optional emulator_quota element specifies the maximum allowed bandwidth (unit: microseconds) for domain's ++ emulator threads (those excluding vCPUs). ++ ++ iothread_period ++ The optional iothread_period element specifies the enforcement interval (unit: microseconds) for IOThreads. ++ ++ iothread_quota ++ The optional iothread_quota element specifies the maximum allowed bandwidth (unit: microseconds) for IOThreads. ++ ++ vcpusched ++ specify the scheduler type for vCPUs. ++ The value is a list of dictionaries with the ``scheduler`` key (values ``batch``, ``idle``, ``fifo``, ``rr``) ++ and the optional ``priority`` and ``vcpus`` keys. The ``priority`` value usually is a positive integer and the ++ ``vcpus`` value is a cpu set like ``1-4,^3,6`` or simply the vcpu id. ++ ++ iothreadsched ++ specify the scheduler type for IO threads. ++ The value is a list of dictionaries with the ``scheduler`` key (values ``batch``, ``idle``, ``fifo``, ``rr``) ++ and the optional ``priority`` and ``vcpus`` keys. The ``priority`` value usually is a positive integer and the ++ ``vcpus`` value is a cpu set like ``1-4,^3,6`` or simply the vcpu id. ++ ++ emulatorsched ++ specify the scheduler type (values batch, idle, fifo, rr) for particular the emulator. ++ The value is a dictionary with the ``scheduler`` key (values ``batch``, ``idle``, ``fifo``, ``rr``) ++ and the optional ``priority`` and ``vcpus`` keys. The ``priority`` value usually is a positive integer. ++ ++ cachetune ++ Optional cachetune element can control allocations for CPU caches using the resctrl on the host. ++ ++ monitor ++ The optional element monitor creates the cache monitor(s) for current cache allocation. ++ ++ memorytune ++ Optional memorytune element can control allocations for memory bandwidth using the resctrl on the host. ++ ++ iothreads ++ Number of threads for supported disk devices to perform I/O requests. iothread id will be numbered from 1 to ++ the provided number (Default: None). ++ + .. _init-boot-def: + + .. rubric:: Boot parameters definition +@@ -1932,6 +2339,33 @@ def init( + min_guarantee + the guaranteed minimum memory allocation for the guest + ++ hugepages ++ memory allocated using ``hugepages`` instead of the normal native page size. It takes a list of ++ dictionaries with ``nodeset`` and ``size`` keys. ++ For example ``"hugepages": [{"nodeset": "1-4,^3", "size": "2m"}, {"nodeset": "3", "size": "1g"}]``. ++ ++ nosharepages ++ boolean value to instruct hypervisor to disable shared pages (memory merge, KSM) for this domain ++ ++ locked ++ boolean value that allows memory pages belonging to the domain will be locked in host's memory and the host will ++ not be allowed to swap them out, which might be required for some workloads such as real-time. ++ ++ source ++ possible values are ``file`` which utilizes file memorybacking, ``anonymous`` by default and ``memfd`` backing. ++ (QEMU/KVM only) ++ ++ access ++ specify if the memory is to be ``shared`` or ``private``. This can be overridden per numa node by memAccess. ++ ++ allocation ++ specify when to allocate the memory by supplying either ``immediate`` or ``ondemand``. ++ ++ discard ++ boolean value to ensure the memory content is discarded just before guest shuts down (or when DIMM module is ++ unplugged). Please note that this is just an optimization and is not guaranteed to work in all cases ++ (e.g. when hypervisor crashes). (QEMU/KVM only) ++ + .. _init-nic-def: + + .. rubric:: Network Interfaces Definitions +@@ -2051,6 +2485,10 @@ def init( + hostname_property: virt:hostname + sparse_volume: True + ++ iothreads ++ When ``True`` dedicated threads will be used for the I/O of the disk. ++ (Default: ``False``) ++ + .. _init-graphics-def: + + .. rubric:: Graphics Definition +@@ -2077,6 +2515,42 @@ def init( + By default, not setting the ``listen`` part of the dictionary will default to + listen on all addresses. + ++ .. _init-chardevs-def: ++ ++ .. rubric:: Serials and Consoles Definitions ++ ++ Serial dictionaries can contain the following properties: ++ ++ type ++ Type of the serial connection, like ``'tcp'``, ``'pty'``, ``'file'``, ``'udp'``, ``'dev'``, ++ ``'pipe'``, ``'unix'``. ++ ++ path ++ Path to the source device. Can be a log file, a host character device to pass through, ++ a unix socket, a named pipe path. ++ ++ host ++ The serial UDP or TCP host name. ++ (Default: 23023) ++ ++ port ++ The serial UDP or TCP port number. ++ (Default: 23023) ++ ++ protocol ++ Name of the TCP connection protocol. ++ (Default: telnet) ++ ++ tls ++ Boolean value indicating whether to use hypervisor TLS certificates environment for TCP devices. ++ ++ target_port ++ The guest device port number starting from 0 ++ ++ target_type ++ The guest device type. Common values are ``serial``, ``virtio`` or ``usb-serial``, but more are documented in ++ `the libvirt documentation `_. ++ + .. rubric:: CLI Example + + .. code-block:: bash +@@ -2226,6 +2700,11 @@ def init( + graphics, + boot, + boot_dev, ++ numatune, ++ hypervisor_features, ++ clock, ++ serials, ++ consoles, + stop_on_reboot, + **kwargs + ) +@@ -2249,19 +2728,15 @@ def _disks_equal(disk1, disk2): + """ + target1 = disk1.find("target") + target2 = disk2.find("target") +- source1 = ( +- disk1.find("source") +- if disk1.find("source") is not None +- else ElementTree.Element("source") +- ) +- source2 = ( +- disk2.find("source") +- if disk2.find("source") is not None +- else ElementTree.Element("source") +- ) + +- source1_dict = xmlutil.to_dict(source1, True) +- source2_dict = xmlutil.to_dict(source2, True) ++ disk1_dict = xmlutil.to_dict(disk1, True) ++ disk2_dict = xmlutil.to_dict(disk2, True) ++ ++ source1_dict = disk1_dict.get("source", {}) ++ source2_dict = disk2_dict.get("source", {}) ++ ++ io1 = disk1_dict.get("driver", {}).get("io", "native") ++ io2 = disk2_dict.get("driver", {}).get("io", "native") + + # Remove the index added by libvirt in the source for backing chain + if source1_dict: +@@ -2276,6 +2751,7 @@ def _disks_equal(disk1, disk2): + and target1.get("bus") == target2.get("bus") + and disk1.get("device", "disk") == disk2.get("device", "disk") + and target1.get("dev") == target2.get("dev") ++ and io1 == io2 + ) + + +@@ -2443,6 +2919,101 @@ def _diff_graphics_lists(old, new): + return _diff_lists(old, new, _graphics_equal) + + ++def _expand_cpuset(cpuset): ++ """ ++ Expand the libvirt cpuset and nodeset values into a list of cpu/node IDs ++ """ ++ if cpuset is None: ++ return None ++ ++ if isinstance(cpuset, int): ++ return str(cpuset) ++ ++ result = set() ++ toremove = set() ++ for part in cpuset.split(","): ++ m = re.match("([0-9]+)-([0-9]+)", part) ++ if m: ++ result |= set(range(int(m.group(1)), int(m.group(2)) + 1)) ++ elif part.startswith("^"): ++ toremove.add(int(part[1:])) ++ else: ++ result.add(int(part)) ++ cpus = list(result - toremove) ++ cpus.sort() ++ cpus = [str(cpu) for cpu in cpus] ++ return ",".join(cpus) ++ ++ ++def _normalize_cpusets(desc, data): ++ """ ++ Expand the cpusets that can't be expanded by the change_xml() function, ++ namely the ones that are used as keys and in the middle of the XPath expressions. ++ """ ++ # Normalize the cpusets keys in the XML ++ xpaths = ["cputune/cachetune", "cputune/cachetune/monitor", "cputune/memorytune"] ++ for xpath in xpaths: ++ nodes = desc.findall(xpath) ++ for node in nodes: ++ node.set("vcpus", _expand_cpuset(node.get("vcpus"))) ++ ++ # data paths to change: ++ # - cpu:tuning:cachetune:{id}:monitor:{sid} ++ # - cpu:tuning:memorytune:{id} ++ if not isinstance(data.get("cpu"), dict): ++ return ++ tuning = data["cpu"].get("tuning", {}) ++ for child in ["cachetune", "memorytune"]: ++ if tuning.get(child): ++ new_item = dict() ++ for cpuset, value in tuning[child].items(): ++ if child == "cachetune" and value.get("monitor"): ++ value["monitor"] = { ++ _expand_cpuset(monitor_cpus): monitor ++ for monitor_cpus, monitor in value["monitor"].items() ++ } ++ new_item[_expand_cpuset(cpuset)] = value ++ tuning[child] = new_item ++ ++ ++def _serial_or_concole_equal(old, new): ++ def _filter_serial_or_concole(item): ++ """ ++ Filter out elements to ignore when comparing items ++ """ ++ return { ++ "type": item.attrib["type"], ++ "port": item.find("source").attrib["service"] ++ if item.find("source") is not None ++ else None, ++ "protocol": item.find("protocol").attrib["type"] ++ if item.find("protocol") is not None ++ else None, ++ } ++ ++ return _filter_serial_or_concole(old) == _filter_serial_or_concole(new) ++ ++ ++def _diff_serial_list(old, new): ++ """ ++ Compare serial definitions to extract the changes ++ ++ :param old: list of ElementTree nodes representing the old serials ++ :param new: list of ElementTree nodes representing the new serials ++ """ ++ return _diff_lists(old, new, _serial_or_concole_equal) ++ ++ ++def _diff_console_list(old, new): ++ """ ++ Compare console definitions to extract the changes ++ ++ :param old: list of ElementTree nodes representing the old consoles ++ :param new: list of ElementTree nodes representing the new consoles ++ """ ++ return _diff_lists(old, new, _serial_or_concole_equal) ++ ++ + def update( + name, + cpu=0, +@@ -2454,8 +3025,13 @@ def update( + graphics=None, + live=True, + boot=None, ++ numatune=None, + test=False, + boot_dev=None, ++ hypervisor_features=None, ++ clock=None, ++ serials=None, ++ consoles=None, + stop_on_reboot=False, + **kwargs + ): +@@ -2463,13 +3039,20 @@ def update( + Update the definition of an existing domain. + + :param name: Name of the domain to update +- :param cpu: Number of virtual CPUs to assign to the virtual machine +- :param mem: Amount of memory to allocate to the virtual machine in MiB. Since Magnesium, a dictionary can be used to ++ :param cpu: ++ Number of virtual CPUs to assign to the virtual machine or a dictionary with detailed information to configure ++ cpu model and topology, numa node tuning, cpu tuning and iothreads allocation. The structure of the dictionary is ++ documented in :ref:`init-cpu-def`. ++ ++ To update any cpu parameters specify the new values to the corresponding tag. To remove any element or attribute, ++ specify ``None`` object. Please note that ``None`` object is mapped to ``null`` in yaml, use ``null`` in sls file ++ instead. ++ :param mem: Amount of memory to allocate to the virtual machine in MiB. Since 3002, a dictionary can be used to + contain detailed configuration which support memory allocation or tuning. Supported parameters are ``boot``, +- ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit`` and ``min_guarantee``. The +- structure of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. +- Detail unit specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be +- an integer. ++ ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit``, ``min_guarantee``, ++ ``hugepages`` , ``nosharepages``, ``locked``, ``source``, ``access``, ``allocation`` and ``discard``. The structure ++ of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. Detail unit ++ specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be an integer. + + To remove any parameters, pass a None object, for instance: 'soft_limit': ``None``. Please note that ``None`` + is mapped to ``null`` in sls file, pass ``null`` in sls file instead. +@@ -2538,6 +3121,30 @@ def update( + + .. versionadded:: Magnesium + ++ :param numatune: ++ The optional numatune element provides details of how to tune the performance of a NUMA host via controlling NUMA ++ policy for domain process. The optional ``memory`` element specifies how to allocate memory for the domain process ++ on a NUMA host. ``memnode`` elements can specify memory allocation policies per each guest NUMA node. The definition ++ used in the dictionary can be found at :ref:`init-cpu-def`. ++ ++ To update any numatune parameters, specify the new value. To remove any ``numatune`` parameters, pass a None object, ++ for instance: 'numatune': ``None``. Please note that ``None`` is mapped to ``null`` in sls file, pass ``null`` in ++ sls file instead. ++ ++ .. versionadded:: Aluminium ++ ++ :param serials: ++ Dictionary providing details on the serials connection to create. (Default: ``None``) ++ See :ref:`init-chardevs-def` for more details on the possible values. ++ ++ .. versionadded:: Aluminium ++ ++ :param consoles: ++ Dictionary providing details on the consoles device to create. (Default: ``None``) ++ See :ref:`init-chardevs-def` for more details on the possible values. ++ ++ .. versionadded:: Aluminium ++ + :param stop_on_reboot: + If set to ``True`` the guest will stop instead of rebooting. + This is specially useful when creating a virtual machine with an installation cdrom or +@@ -2550,6 +3157,69 @@ def update( + + .. versionadded:: sodium + ++ :param hypervisor_features: ++ Enable or disable hypervisor-specific features on the virtual machine. ++ ++ .. versionadded:: Aluminium ++ ++ .. code-block:: yaml ++ ++ hypervisor_features: ++ kvm-hint-dedicated: True ++ ++ :param clock: ++ Configure the guest clock. ++ The value is a dictionary with the following keys: ++ ++ adjustment ++ time adjustment in seconds or ``reset`` ++ ++ utc ++ set to ``False`` to use the host local time as the guest clock. Defaults to ``True``. ++ ++ timezone ++ synchronize the guest to the correspding timezone ++ ++ timers ++ a dictionary associating the timer name with its configuration. ++ This configuration is a dictionary with the properties ``track``, ``tickpolicy``, ++ ``catchup``, ``frequency``, ``mode``, ``present``, ``slew``, ``threshold`` and ``limit``. ++ See `libvirt time keeping documentation `_ for the possible values. ++ ++ .. versionadded:: Aluminium ++ ++ Set the clock to local time using an offset in seconds ++ .. code-block:: yaml ++ ++ clock: ++ adjustment: 3600 ++ utc: False ++ ++ Set the clock to a specific time zone: ++ ++ .. code-block:: yaml ++ ++ clock: ++ timezone: CEST ++ ++ Tweak guest timers: ++ ++ .. code-block:: yaml ++ ++ clock: ++ timers: ++ tsc: ++ frequency: 3504000000 ++ mode: native ++ rtc: ++ track: wall ++ tickpolicy: catchup ++ slew: 4636 ++ threshold: 123 ++ limit: 2342 ++ hpet: ++ present: False ++ + :return: + + Returns a dictionary indicating the status of what has been done. It is structured in +@@ -2595,12 +3265,11 @@ def update( + boot = _handle_remote_boot_params(boot) + if boot.get("efi", None) is not None: + need_update = _handle_efi_param(boot, desc) +- + new_desc = ElementTree.fromstring( + _gen_xml( + conn, + name, +- cpu or 0, ++ cpu, + mem or 0, + all_disks, + _get_merged_nics(hypervisor, nic_profile, interfaces), +@@ -2610,17 +3279,19 @@ def update( + graphics, + boot, + boot_dev, +- stop_on_reboot, ++ numatune, ++ serial=serials, ++ consoles=consoles, ++ stop_on_reboot=stop_on_reboot, + **kwargs + ) + ) + +- # Update the cpu +- cpu_node = desc.find("vcpu") +- if cpu and int(cpu_node.text) != cpu: +- cpu_node.text = str(cpu) +- cpu_node.set("current", str(cpu)) +- need_update = True ++ if clock: ++ offset = "utc" if clock.get("utc", True) else "localtime" ++ if "timezone" in clock: ++ offset = "timezone" ++ clock["offset"] = offset + + def _set_loader(node, value): + salt.utils.xmlutil.set_node_text(node, value) +@@ -2631,20 +3302,110 @@ def update( + def _set_nvram(node, value): + node.set("template", value) + +- def _set_with_byte_unit(node, value): +- node.text = str(value) +- node.set("unit", "bytes") ++ def _set_with_byte_unit(attr_name=None): ++ def _setter(node, value): ++ if attr_name: ++ node.set(attr_name, str(value)) ++ else: ++ node.text = str(value) ++ node.set("unit", "bytes") ++ ++ return _setter + + def _get_with_unit(node): + unit = node.get("unit", "KiB") + # _handle_unit treats bytes as invalid unit for the purpose of consistency + unit = unit if unit != "bytes" else "b" +- value = node.get("memory") or node.text ++ value = node.get("memory") or node.get("size") or node.text + return _handle_unit("{}{}".format(value, unit)) if value else None + ++ def _set_vcpu(node, value): ++ node.text = str(value) ++ node.set("current", str(value)) ++ + old_mem = int(_get_with_unit(desc.find("memory")) / 1024) ++ old_cpu = int(desc.find("./vcpu").text) ++ ++ def _almost_equal(current, new): ++ if current is None or new is None: ++ return False ++ return abs(current - new) / current < 1e-03 ++ ++ def _yesno_attribute(path, xpath, attr_name, ignored=None): ++ return xmlutil.attribute( ++ path, xpath, attr_name, ignored, lambda v: "yes" if v else "no" ++ ) ++ ++ def _memory_parameter(path, xpath, attr_name=None, ignored=None): ++ entry = { ++ "path": path, ++ "xpath": xpath, ++ "convert": _handle_unit, ++ "get": _get_with_unit, ++ "set": _set_with_byte_unit(attr_name), ++ "equals": _almost_equal, ++ } ++ if attr_name: ++ entry["del"] = salt.utils.xmlutil.del_attribute(attr_name, ignored) ++ return entry ++ ++ def _cpuset_parameter(path, xpath, attr_name=None, ignored=None): ++ def _set_cpuset(node, value): ++ if attr_name: ++ node.set(attr_name, value) ++ else: ++ node.text = value ++ ++ entry = { ++ "path": path, ++ "xpath": xpath, ++ "convert": _expand_cpuset, ++ "get": lambda n: _expand_cpuset(n.get(attr_name) if attr_name else n.text), ++ "set": _set_cpuset, ++ } ++ if attr_name: ++ entry["del"] = salt.utils.xmlutil.del_attribute(attr_name, ignored) ++ return entry + + # Update the kernel boot parameters ++ data = {k: v for k, v in locals().items() if bool(v)} ++ data["stop_on_reboot"] = stop_on_reboot ++ if boot_dev: ++ data["boot_dev"] = boot_dev.split() ++ ++ # Set the missing optional attributes and timers to None in timers to help cleaning up ++ timer_names = [ ++ "platform", ++ "hpet", ++ "kvmclock", ++ "pit", ++ "rtc", ++ "tsc", ++ "hypervclock", ++ "armvtimer", ++ ] ++ if data.get("clock", {}).get("timers"): ++ attributes = [ ++ "track", ++ "tickpolicy", ++ "frequency", ++ "mode", ++ "present", ++ "slew", ++ "threshold", ++ "limit", ++ ] ++ for timer in data["clock"]["timers"].values(): ++ for attribute in attributes: ++ if attribute not in timer: ++ timer[attribute] = None ++ ++ for timer_name in timer_names: ++ if timer_name not in data["clock"]["timers"]: ++ data["clock"]["timers"][timer_name] = None ++ ++ _normalize_cpusets(desc, data) ++ + params_mapping = [ + { + "path": "stop_on_reboot", +@@ -2657,89 +3418,251 @@ def update( + {"path": "boot:loader", "xpath": "os/loader", "set": _set_loader}, + {"path": "boot:nvram", "xpath": "os/nvram", "set": _set_nvram}, + # Update the memory, note that libvirt outputs all memory sizes in KiB ++ _memory_parameter("mem", "memory"), ++ _memory_parameter("mem", "currentMemory"), ++ _memory_parameter("mem:max", "maxMemory"), ++ _memory_parameter("mem:boot", "memory"), ++ _memory_parameter("mem:current", "currentMemory"), ++ xmlutil.attribute("mem:slots", "maxMemory", "slots", ["unit"]), ++ _memory_parameter("mem:hard_limit", "memtune/hard_limit"), ++ _memory_parameter("mem:soft_limit", "memtune/soft_limit"), ++ _memory_parameter("mem:swap_hard_limit", "memtune/swap_hard_limit"), ++ _memory_parameter("mem:min_guarantee", "memtune/min_guarantee"), ++ xmlutil.attribute("boot_dev:{dev}", "os/boot[$dev]", "dev"), ++ _memory_parameter( ++ "mem:hugepages:{id}:size", ++ "memoryBacking/hugepages/page[$id]", ++ "size", ++ ["unit", "nodeset"], ++ ), ++ _cpuset_parameter( ++ "mem:hugepages:{id}:nodeset", "memoryBacking/hugepages/page[$id]", "nodeset" ++ ), + { +- "path": "mem", +- "xpath": "memory", +- "convert": _handle_unit, +- "get": _get_with_unit, +- "set": _set_with_byte_unit, +- }, +- { +- "path": "mem", +- "xpath": "currentMemory", +- "convert": _handle_unit, +- "get": _get_with_unit, +- "set": _set_with_byte_unit, +- }, +- { +- "path": "mem:max", +- "convert": _handle_unit, +- "xpath": "maxMemory", +- "get": _get_with_unit, +- "set": _set_with_byte_unit, ++ "path": "mem:nosharepages", ++ "xpath": "memoryBacking/nosharepages", ++ "get": lambda n: n is not None, ++ "set": lambda n, v: None, + }, + { +- "path": "mem:boot", +- "convert": _handle_unit, +- "xpath": "memory", +- "get": _get_with_unit, +- "set": _set_with_byte_unit, +- }, +- { +- "path": "mem:current", +- "convert": _handle_unit, +- "xpath": "currentMemory", +- "get": _get_with_unit, +- "set": _set_with_byte_unit, ++ "path": "mem:locked", ++ "xpath": "memoryBacking/locked", ++ "get": lambda n: n is not None, ++ "set": lambda n, v: None, + }, ++ xmlutil.attribute("mem:source", "memoryBacking/source", "type"), ++ xmlutil.attribute("mem:access", "memoryBacking/access", "mode"), ++ xmlutil.attribute("mem:allocation", "memoryBacking/allocation", "mode"), ++ {"path": "mem:discard", "xpath": "memoryBacking/discard"}, + { +- "path": "mem:slots", +- "xpath": "maxMemory", +- "get": lambda n: n.get("slots"), +- "set": lambda n, v: n.set("slots", str(v)), +- "del": salt.utils.xmlutil.del_attribute("slots", ["unit"]), +- }, +- { +- "path": "mem:hard_limit", +- "convert": _handle_unit, +- "xpath": "memtune/hard_limit", +- "get": _get_with_unit, +- "set": _set_with_byte_unit, +- }, +- { +- "path": "mem:soft_limit", +- "convert": _handle_unit, +- "xpath": "memtune/soft_limit", +- "get": _get_with_unit, +- "set": _set_with_byte_unit, +- }, +- { +- "path": "mem:swap_hard_limit", +- "convert": _handle_unit, +- "xpath": "memtune/swap_hard_limit", +- "get": _get_with_unit, +- "set": _set_with_byte_unit, +- }, +- { +- "path": "mem:min_guarantee", +- "convert": _handle_unit, +- "xpath": "memtune/min_guarantee", +- "get": _get_with_unit, +- "set": _set_with_byte_unit, +- }, +- { +- "path": "boot_dev:{dev}", +- "xpath": "os/boot[$dev]", +- "get": lambda n: n.get("dev"), +- "set": lambda n, v: n.set("dev", v), +- "del": salt.utils.xmlutil.del_attribute("dev"), ++ "path": "cpu", ++ "xpath": "vcpu", ++ "get": lambda n: int(n.text), ++ "set": _set_vcpu, + }, ++ {"path": "cpu:maximum", "xpath": "vcpu", "get": lambda n: int(n.text)}, ++ xmlutil.attribute("cpu:placement", "vcpu", "placement"), ++ _cpuset_parameter("cpu:cpuset", "vcpu", "cpuset"), ++ xmlutil.attribute("cpu:current", "vcpu", "current"), ++ xmlutil.attribute("cpu:match", "cpu", "match"), ++ xmlutil.attribute("cpu:mode", "cpu", "mode"), ++ xmlutil.attribute("cpu:check", "cpu", "check"), ++ {"path": "cpu:model:name", "xpath": "cpu/model"}, ++ xmlutil.attribute("cpu:model:fallback", "cpu/model", "fallback"), ++ xmlutil.attribute("cpu:model:vendor_id", "cpu/model", "vendor_id"), ++ {"path": "cpu:vendor", "xpath": "cpu/vendor"}, ++ xmlutil.attribute("cpu:topology:sockets", "cpu/topology", "sockets"), ++ xmlutil.attribute("cpu:topology:cores", "cpu/topology", "cores"), ++ xmlutil.attribute("cpu:topology:threads", "cpu/topology", "threads"), ++ xmlutil.attribute("cpu:cache:level", "cpu/cache", "level"), ++ xmlutil.attribute("cpu:cache:mode", "cpu/cache", "mode"), ++ xmlutil.attribute( ++ "cpu:features:{id}", "cpu/feature[@name='$id']", "policy", ["name"] ++ ), ++ _yesno_attribute( ++ "cpu:vcpus:{id}:enabled", "vcpus/vcpu[@id='$id']", "enabled", ["id"] ++ ), ++ _yesno_attribute( ++ "cpu:vcpus:{id}:hotpluggable", ++ "vcpus/vcpu[@id='$id']", ++ "hotpluggable", ++ ["id"], ++ ), ++ xmlutil.int_attribute( ++ "cpu:vcpus:{id}:order", "vcpus/vcpu[@id='$id']", "order", ["id"] ++ ), ++ _cpuset_parameter( ++ "cpu:numa:{id}:cpus", "cpu/numa/cell[@id='$id']", "cpus", ["id"] ++ ), ++ _memory_parameter( ++ "cpu:numa:{id}:memory", "cpu/numa/cell[@id='$id']", "memory", ["id"] ++ ), ++ _yesno_attribute( ++ "cpu:numa:{id}:discard", "cpu/numa/cell[@id='$id']", "discard", ["id"] ++ ), ++ xmlutil.attribute( ++ "cpu:numa:{id}:memAccess", "cpu/numa/cell[@id='$id']", "memAccess", ["id"] ++ ), ++ xmlutil.attribute( ++ "cpu:numa:{id}:distances:{sid}", ++ "cpu/numa/cell[@id='$id']/distances/sibling[@id='$sid']", ++ "value", ++ ["id"], ++ ), ++ {"path": "cpu:iothreads", "xpath": "iothreads"}, ++ {"path": "cpu:tuning:shares", "xpath": "cputune/shares"}, ++ {"path": "cpu:tuning:period", "xpath": "cputune/period"}, ++ {"path": "cpu:tuning:quota", "xpath": "cputune/quota"}, ++ {"path": "cpu:tuning:global_period", "xpath": "cputune/global_period"}, ++ {"path": "cpu:tuning:global_quota", "xpath": "cputune/global_quota"}, ++ {"path": "cpu:tuning:emulator_period", "xpath": "cputune/emulator_period"}, ++ {"path": "cpu:tuning:emulator_quota", "xpath": "cputune/emulator_quota"}, ++ {"path": "cpu:tuning:iothread_period", "xpath": "cputune/iothread_period"}, ++ {"path": "cpu:tuning:iothread_quota", "xpath": "cputune/iothread_quota"}, ++ _cpuset_parameter( ++ "cpu:tuning:vcpupin:{id}", ++ "cputune/vcpupin[@vcpu='$id']", ++ "cpuset", ++ ["vcpu"], ++ ), ++ _cpuset_parameter("cpu:tuning:emulatorpin", "cputune/emulatorpin", "cpuset"), ++ _cpuset_parameter( ++ "cpu:tuning:iothreadpin:{id}", ++ "cputune/iothreadpin[@iothread='$id']", ++ "cpuset", ++ ["iothread"], ++ ), ++ xmlutil.attribute( ++ "cpu:tuning:vcpusched:{id}:scheduler", ++ "cputune/vcpusched[$id]", ++ "scheduler", ++ ["priority", "vcpus"], ++ ), ++ xmlutil.attribute( ++ "cpu:tuning:vcpusched:{id}:priority", "cputune/vcpusched[$id]", "priority" ++ ), ++ _cpuset_parameter( ++ "cpu:tuning:vcpusched:{id}:vcpus", "cputune/vcpusched[$id]", "vcpus" ++ ), ++ xmlutil.attribute( ++ "cpu:tuning:iothreadsched:{id}:scheduler", ++ "cputune/iothreadsched[$id]", ++ "scheduler", ++ ["priority", "iothreads"], ++ ), ++ xmlutil.attribute( ++ "cpu:tuning:iothreadsched:{id}:priority", ++ "cputune/iothreadsched[$id]", ++ "priority", ++ ), ++ _cpuset_parameter( ++ "cpu:tuning:iothreadsched:{id}:iothreads", ++ "cputune/iothreadsched[$id]", ++ "iothreads", ++ ), ++ xmlutil.attribute( ++ "cpu:tuning:emulatorsched:scheduler", ++ "cputune/emulatorsched", ++ "scheduler", ++ ["priority"], ++ ), ++ xmlutil.attribute( ++ "cpu:tuning:emulatorsched:priority", "cputune/emulatorsched", "priority" ++ ), ++ xmlutil.attribute( ++ "cpu:tuning:cachetune:{id}:monitor:{sid}", ++ "cputune/cachetune[@vcpus='$id']/monitor[@vcpus='$sid']", ++ "level", ++ ["vcpus"], ++ ), ++ xmlutil.attribute( ++ "cpu:tuning:memorytune:{id}:{sid}", ++ "cputune/memorytune[@vcpus='$id']/node[@id='$sid']", ++ "bandwidth", ++ ["id", "vcpus"], ++ ), ++ xmlutil.attribute("clock:offset", "clock", "offset"), ++ xmlutil.attribute("clock:adjustment", "clock", "adjustment", convert=str), ++ xmlutil.attribute("clock:timezone", "clock", "timezone"), + ] + +- data = {k: v for k, v in locals().items() if bool(v)} +- data["stop_on_reboot"] = stop_on_reboot +- if boot_dev: +- data["boot_dev"] = {i + 1: dev for i, dev in enumerate(boot_dev.split())} ++ for timer in timer_names: ++ params_mapping += [ ++ xmlutil.attribute( ++ "clock:timers:{}:track".format(timer), ++ "clock/timer[@name='{}']".format(timer), ++ "track", ++ ["name"], ++ ), ++ xmlutil.attribute( ++ "clock:timers:{}:tickpolicy".format(timer), ++ "clock/timer[@name='{}']".format(timer), ++ "tickpolicy", ++ ["name"], ++ ), ++ xmlutil.int_attribute( ++ "clock:timers:{}:frequency".format(timer), ++ "clock/timer[@name='{}']".format(timer), ++ "frequency", ++ ["name"], ++ ), ++ xmlutil.attribute( ++ "clock:timers:{}:mode".format(timer), ++ "clock/timer[@name='{}']".format(timer), ++ "mode", ++ ["name"], ++ ), ++ _yesno_attribute( ++ "clock:timers:{}:present".format(timer), ++ "clock/timer[@name='{}']".format(timer), ++ "present", ++ ["name"], ++ ), ++ ] ++ for attr in ["slew", "threshold", "limit"]: ++ params_mapping.append( ++ xmlutil.int_attribute( ++ "clock:timers:{}:{}".format(timer, attr), ++ "clock/timer[@name='{}']/catchup".format(timer), ++ attr, ++ ) ++ ) ++ ++ for attr in ["level", "type", "size"]: ++ params_mapping.append( ++ xmlutil.attribute( ++ "cpu:tuning:cachetune:{id}:{sid}:" + attr, ++ "cputune/cachetune[@vcpus='$id']/cache[@id='$sid']", ++ attr, ++ ["id", "unit", "vcpus"], ++ ) ++ ) ++ ++ # update NUMA host policy ++ if hypervisor in ["qemu", "kvm"]: ++ params_mapping += [ ++ xmlutil.attribute("numatune:memory:mode", "numatune/memory", "mode"), ++ _cpuset_parameter("numatune:memory:nodeset", "numatune/memory", "nodeset"), ++ xmlutil.attribute( ++ "numatune:memnodes:{id}:mode", ++ "numatune/memnode[@cellid='$id']", ++ "mode", ++ ["cellid"], ++ ), ++ _cpuset_parameter( ++ "numatune:memnodes:{id}:nodeset", ++ "numatune/memnode[@cellid='$id']", ++ "nodeset", ++ ["cellid"], ++ ), ++ xmlutil.attribute( ++ "hypervisor_features:kvm-hint-dedicated", ++ "features/kvm/hint-dedicated", ++ "state", ++ convert=lambda v: "on" if v else "off", ++ ), ++ ] ++ + need_update = ( + salt.utils.xmlutil.change_xml(desc, data, params_mapping) or need_update + ) +@@ -2750,6 +3673,8 @@ def update( + "disk": ["disks", "disk_profile"], + "interface": ["interfaces", "nic_profile"], + "graphics": ["graphics"], ++ "serial": ["serial"], ++ "console": ["console"], + } + changes = {} + for dev_type in parameters: +@@ -2787,7 +3712,6 @@ def update( + _qemu_image_create(all_disks[idx]) + elif item in changes["disk"]["new"] and not source_file: + _disk_volume_create(conn, all_disks[idx]) +- + if not test: + xml_desc = ElementTree.tostring(desc) + log.debug("Update virtual machine definition: %s", xml_desc) +@@ -2803,14 +3727,18 @@ def update( + commands = [] + removable_changes = [] + if domain.isActive() and live: +- if cpu: +- commands.append( +- { +- "device": "cpu", +- "cmd": "setVcpusFlags", +- "args": [cpu, libvirt.VIR_DOMAIN_AFFECT_LIVE], +- } +- ) ++ if cpu and ( ++ isinstance(cpu, int) or isinstance(cpu, dict) and cpu.get("maximum") ++ ): ++ new_cpu = cpu.get("maximum") if isinstance(cpu, dict) else cpu ++ if old_cpu != new_cpu and new_cpu is not None: ++ commands.append( ++ { ++ "device": "cpu", ++ "cmd": "setVcpusFlags", ++ "args": [new_cpu, libvirt.VIR_DOMAIN_AFFECT_LIVE], ++ } ++ ) + if mem: + if isinstance(mem, dict): + # setMemoryFlags takes memory amount in KiB +@@ -2822,7 +3750,7 @@ def update( + elif isinstance(mem, int): + new_mem = int(mem * 1024) + +- if old_mem != new_mem and new_mem is not None: ++ if not _almost_equal(old_mem, new_mem) and new_mem is not None: + commands.append( + { + "device": "mem", +@@ -4402,7 +5330,7 @@ def purge(vm_, dirs=False, removables=False, **kwargs): + directories.add(os.path.dirname(disks[disk]["file"])) + else: + # We may have a volume to delete here +- matcher = re.match("^(?P[^/]+)/(?P.*)$", disks[disk]["file"],) ++ matcher = re.match("^(?P[^/]+)/(?P.*)$", disks[disk]["file"]) + if matcher: + pool_name = matcher.group("pool") + pool = None +diff --git a/salt/states/virt.py b/salt/states/virt.py +index 20ea1c25f1..784cdca73c 100644 +--- a/salt/states/virt.py ++++ b/salt/states/virt.py +@@ -287,8 +287,13 @@ def defined( + os_type=None, + arch=None, + boot=None, ++ numatune=None, + update=True, + boot_dev=None, ++ hypervisor_features=None, ++ clock=None, ++ serials=None, ++ consoles=None, + stop_on_reboot=False, + live=True, + ): +@@ -298,26 +303,151 @@ def defined( + .. versionadded:: sodium + + :param name: name of the virtual machine to run +- :param cpu: number of CPUs for the virtual machine to create +- :param mem: Amount of memory to allocate to the virtual machine in MiB. Since Magnesium, a dictionary can be used to ++ :param cpu: ++ Number of virtual CPUs to assign to the virtual machine or a dictionary with detailed information to configure ++ cpu model and topology, numa node tuning, cpu tuning and iothreads allocation. The structure of the dictionary is ++ documented in :ref:`init-cpu-def`. ++ ++ .. code-block:: yaml ++ ++ cpu: ++ placement: static ++ cpuset: 0-11 ++ current: 5 ++ maximum: 12 ++ vcpus: ++ 0: ++ enabled: 'yes' ++ hotpluggable: 'no' ++ order: 1 ++ 1: ++ enabled: 'no' ++ hotpluggable: 'yes' ++ match: minimum ++ mode: custom ++ check: full ++ vendor: Intel ++ model: ++ name: core2duo ++ fallback: allow ++ vendor_id: GenuineIntel ++ topology: ++ sockets: 1 ++ cores: 12 ++ threads: 1 ++ cache: ++ level: 3 ++ mode: emulate ++ feature: ++ policy: optional ++ name: lahf_lm ++ numa: ++ 0: ++ cpus: 0-3 ++ memory: 1g ++ discard: 'yes' ++ distances: ++ 0: 10 # sibling id : value ++ 1: 21 ++ 2: 31 ++ 3: 41 ++ 1: ++ cpus: 4-6 ++ memory: 1g ++ memAccess: shared ++ distances: ++ 0: 21 ++ 1: 10 ++ 2: 21 ++ 3: 31 ++ tuning: ++ vcpupin: ++ 0: 1-4,^2 # vcpuid : cpuset ++ 1: 0,1 ++ 2: 2,3 ++ 3: 0,4 ++ emulatorpin: 1-3 ++ iothreadpin: ++ 1: 5,6 # iothread id: cpuset ++ 2: 7,8 ++ shares: 2048 ++ period: 1000000 ++ quota: -1 ++ global_period: 1000000 ++ global_quota: -1 ++ emulator_period: 1000000 ++ emulator_quota: -1 ++ iothread_period: 1000000 ++ iothread_quota: -1 ++ vcpusched: ++ - scheduler: fifo ++ priority: 1 ++ - scheduler: fifo ++ priority: 2 ++ vcpus: 1-3 ++ - scheduler: rr ++ priority: 3 ++ vcpus: 4 ++ iothreadsched: ++ - scheduler: batch ++ iothreads: 2 ++ emulatorsched: ++ scheduler: idle ++ cachetune: ++ 0-3: # vcpus set ++ 0: # cache id ++ level: 3 ++ type: both ++ size: 4 ++ 1: ++ level: 3 ++ type: both ++ size: 6 ++ monitor: ++ 1: 3 ++ 0-3: 3 ++ 4-5: ++ monitor: ++ 4: 3 # vcpus: level ++ 5: 3 ++ memorytune: ++ 0-3: # vcpus set ++ 0: 60 # node id: bandwidth ++ 4-5: ++ 0: 60 ++ iothreads: 4 ++ ++ .. versionadded:: Aluminium ++ ++ :param mem: Amount of memory to allocate to the virtual machine in MiB. Since 3002, a dictionary can be used to + contain detailed configuration which support memory allocation or tuning. Supported parameters are ``boot``, +- ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit`` and ``min_guarantee``. The +- structure of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. +- Detail unit specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be +- an integer. ++ ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit``, ``min_guarantee``, ++ ``hugepages`` , ``nosharepages``, ``locked``, ``source``, ``access``, ``allocation`` and ``discard``. The structure ++ of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. Detail unit ++ specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be an integer. + +- .. code-block:: python ++ .. code-block:: yaml + +- { +- 'boot': 1g, +- 'current': 1g, +- 'max': 1g, +- 'slots': 10, +- 'hard_limit': '1024' +- 'soft_limit': '512m' +- 'swap_hard_limit': '1g' +- 'min_guarantee': '512mib' +- } ++ boot: 1g ++ current: 1g ++ max: 1g ++ slots: 10 ++ hard_limit: 1024 ++ soft_limit: 512m ++ swap_hard_limit: 1g ++ min_guarantee: 512mib ++ hugepages: ++ - size: 2m ++ - nodeset: 0-2 ++ size: 1g ++ - nodeset: 3 ++ size: 2g ++ nosharepages: True ++ locked: True ++ source: file ++ access: shared ++ allocation: immediate ++ discard: True + + .. versionchanged:: Magnesium + +@@ -380,6 +510,77 @@ def defined( + + .. versionadded:: Magnesium + ++ :param numatune: ++ The optional numatune element provides details of how to tune the performance of a NUMA host via controlling NUMA ++ policy for domain process. The optional ``memory`` element specifies how to allocate memory for the domain process ++ on a NUMA host. ``memnode`` elements can specify memory allocation policies per each guest NUMA node. The definition ++ used in the dictionary can be found at :ref:`init-cpu-def`. ++ ++ .. versionadded:: Aluminium ++ ++ .. code-block:: python ++ ++ { ++ 'memory': {'mode': 'strict', 'nodeset': '0-11'}, ++ 'memnodes': {0: {'mode': 'strict', 'nodeset': 1}, 1: {'mode': 'preferred', 'nodeset': 2}} ++ } ++ ++ :param hypervisor_features: ++ Enable or disable hypervisor-specific features on the virtual machine. ++ ++ .. versionadded:: Aluminium ++ ++ .. code-block:: yaml ++ ++ hypervisor_features: ++ kvm-hint-dedicated: True ++ ++ :param clock: ++ Configure the guest clock. ++ The value is a dictionary with the following keys: ++ ++ adjustment ++ time adjustment in seconds or ``reset`` ++ ++ utc ++ set to ``False`` to use the host local time as the guest clock. Defaults to ``True``. ++ ++ timezone ++ synchronize the guest to the correspding timezone ++ ++ timers ++ a dictionary associating the timer name with its configuration. ++ This configuration is a dictionary with the properties ``track``, ``tickpolicy``, ++ ``catchup``, ``frequency``, ``mode``, ``present``, ``slew``, ``threshold`` and ``limit``. ++ See `libvirt time keeping documentation `_ for the possible values. ++ ++ .. versionadded:: Aluminium ++ ++ Set the clock to local time using an offset in seconds ++ .. code-block:: yaml ++ ++ clock: ++ adjustment: 3600 ++ utc: False ++ ++ Set the clock to a specific time zone: ++ ++ .. code-block:: yaml ++ ++ clock: ++ timezone: CEST ++ ++ :param serials: ++ Dictionary providing details on the serials connection to create. (Default: ``None``) ++ See :ref:`init-chardevs-def` for more details on the possible values. ++ ++ .. versionadded:: Aluminium ++ :param consoles: ++ Dictionary providing details on the consoles device to create. (Default: ``None``) ++ See :ref:`init-chardevs-def` for more details on the possible values. ++ ++ .. versionadded:: Aluminium ++ + :param stop_on_reboot: + If set to ``True`` the guest will stop instead of rebooting. + This is specially useful when creating a virtual machine with an installation cdrom or +@@ -456,8 +657,13 @@ def defined( + username=username, + password=password, + boot=boot, ++ numatune=numatune, ++ serials=serials, ++ consoles=consoles, + test=__opts__["test"], + boot_dev=boot_dev, ++ hypervisor_features=hypervisor_features, ++ clock=clock, + stop_on_reboot=stop_on_reboot, + ) + ret["changes"][name] = status +@@ -492,8 +698,13 @@ def defined( + username=username, + password=password, + boot=boot, ++ numatune=numatune, ++ serials=serials, ++ consoles=consoles, + start=False, + boot_dev=boot_dev, ++ hypervisor_features=hypervisor_features, ++ clock=clock, + stop_on_reboot=stop_on_reboot, + ) + ret["changes"][name] = {"definition": True} +@@ -528,6 +739,11 @@ def running( + arch=None, + boot=None, + boot_dev=None, ++ numatune=None, ++ hypervisor_features=None, ++ clock=None, ++ serials=None, ++ consoles=None, + stop_on_reboot=False, + ): + """ +@@ -536,13 +752,20 @@ def running( + .. versionadded:: 2016.3.0 + + :param name: name of the virtual machine to run +- :param cpu: number of CPUs for the virtual machine to create +- :param mem: Amount of memory to allocate to the virtual machine in MiB. Since Magnesium, a dictionary can be used to ++ :param cpu: ++ Number of virtual CPUs to assign to the virtual machine or a dictionary with detailed information to configure ++ cpu model and topology, numa node tuning, cpu tuning and iothreads allocation. The structure of the dictionary is ++ documented in :ref:`init-cpu-def`. ++ ++ To update any cpu parameters specify the new values to the corresponding tag. To remove any element or attribute, ++ specify ``None`` object. Please note that ``None`` object is mapped to ``null`` in yaml, use ``null`` in sls file ++ instead. ++ :param mem: Amount of memory to allocate to the virtual machine in MiB. Since 3002, a dictionary can be used to + contain detailed configuration which support memory allocation or tuning. Supported parameters are ``boot``, +- ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit`` and ``min_guarantee``. The +- structure of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. +- Detail unit specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be +- an integer. ++ ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit``, ``min_guarantee``, ++ ``hugepages`` , ``nosharepages``, ``locked``, ``source``, ``access``, ``allocation`` and ``discard``. The structure ++ of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. Detail unit ++ specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be an integer. + + To remove any parameters, pass a None object, for instance: 'soft_limit': ``None``. Please note that ``None`` + is mapped to ``null`` in sls file, pass ``null`` in sls file instead. +@@ -638,6 +861,16 @@ def running( + pass a None object, for instance: 'kernel': ``None``. + + .. versionadded:: 3000 ++ :param serials: ++ Dictionary providing details on the serials connection to create. (Default: ``None``) ++ See :ref:`init-chardevs-def` for more details on the possible values. ++ ++ .. versionadded:: Aluminium ++ :param consoles: ++ Dictionary providing details on the consoles device to create. (Default: ``None``) ++ See :ref:`init-chardevs-def` for more details on the possible values. ++ ++ .. versionadded:: Aluminium + + :param boot: + Specifies kernel for the virtual machine, as well as boot parameters +@@ -664,6 +897,18 @@ def running( + + .. versionadded:: Magnesium + ++ :param numatune: ++ The optional numatune element provides details of how to tune the performance of a NUMA host via controlling NUMA ++ policy for domain process. The optional ``memory`` element specifies how to allocate memory for the domain process ++ on a NUMA host. ``memnode`` elements can specify memory allocation policies per each guest NUMA node. The definition ++ used in the dictionary can be found at :ref:`init-cpu-def`. ++ ++ To update any numatune parameters, specify the new value. To remove any ``numatune`` parameters, pass a None object, ++ for instance: 'numatune': ``None``. Please note that ``None`` is mapped to ``null`` in sls file, pass ``null`` in ++ sls file instead. ++ ++ .. versionadded:: Aluminium ++ + :param stop_on_reboot: + If set to ``True`` the guest will stop instead of rebooting. + This is specially useful when creating a virtual machine with an installation cdrom or +@@ -672,6 +917,51 @@ def running( + + .. versionadded:: Aluminium + ++ :param hypervisor_features: ++ Enable or disable hypervisor-specific features on the virtual machine. ++ ++ .. versionadded:: Aluminium ++ ++ .. code-block:: yaml ++ ++ hypervisor_features: ++ kvm-hint-dedicated: True ++ ++ :param clock: ++ Configure the guest clock. ++ The value is a dictionary with the following keys: ++ ++ adjustment ++ time adjustment in seconds or ``reset`` ++ ++ utc ++ set to ``False`` to use the host local time as the guest clock. Defaults to ``True``. ++ ++ timezone ++ synchronize the guest to the correspding timezone ++ ++ timers ++ a dictionary associating the timer name with its configuration. ++ This configuration is a dictionary with the properties ``track``, ``tickpolicy``, ++ ``catchup``, ``frequency``, ``mode``, ``present``, ``slew``, ``threshold`` and ``limit``. ++ See `libvirt time keeping documentation `_ for the possible values. ++ ++ .. versionadded:: Aluminium ++ ++ Set the clock to local time using an offset in seconds ++ .. code-block:: yaml ++ ++ clock: ++ adjustment: 3600 ++ utc: False ++ ++ Set the clock to a specific time zone: ++ ++ .. code-block:: yaml ++ ++ clock: ++ timezone: CEST ++ + .. rubric:: Example States + + Make sure an already-defined virtual machine called ``domain_name`` is running: +@@ -740,10 +1030,15 @@ def running( + boot=boot, + update=update, + boot_dev=boot_dev, ++ numatune=numatune, ++ hypervisor_features=hypervisor_features, ++ clock=clock, + stop_on_reboot=stop_on_reboot, + connection=connection, + username=username, + password=password, ++ serials=serials, ++ consoles=consoles, + ) + + result = True if not __opts__["test"] else None +diff --git a/salt/templates/virt/libvirt_chardevs.jinja b/salt/templates/virt/libvirt_chardevs.jinja +new file mode 100644 +index 0000000000..1795277180 +--- /dev/null ++++ b/salt/templates/virt/libvirt_chardevs.jinja +@@ -0,0 +1,16 @@ ++{% macro chardev(dev) -%} ++ {% if dev.type == "unix" -%} ++ ++ {% elif dev.type in ["udp", "tcp"] -%} ++ ++ {% elif dev.type in ["pipe", "dev", "pty", "file"] and dev.path -%} ++ ++ {%- endif %} ++ {% if dev.type == "tcp" -%} ++ ++ {%- endif %} ++ {% if "target_port" in dev or "target_type" in dev -%} ++ ++ {%- endif %} ++{%- endmacro %} +diff --git a/salt/templates/virt/libvirt_domain.jinja b/salt/templates/virt/libvirt_domain.jinja +index fb4c9f40d0..6ac3e867b9 100644 +--- a/salt/templates/virt/libvirt_domain.jinja ++++ b/salt/templates/virt/libvirt_domain.jinja +@@ -1,32 +1,220 @@ + {%- import 'libvirt_disks.jinja' as libvirt_disks -%} ++{%- macro opt_attribute(obj, name, conv=none) %} ++{%- if obj.get(name) is not none %} {{ name }}='{{ obj[name] if conv is none else conv(obj[name]) }}'{% endif -%} ++{%- endmacro %} ++{%- import 'libvirt_chardevs.jinja' as libvirt_chardevs -%} + + {{ name }} +- {{ cpu }} ++ {%- if cpu %} ++ {{ cpu.get('maximum', '') }} ++ {%- endif %} ++ {%- if cpu.get('vcpus') %} ++ ++ {%- for vcpu_id in cpu["vcpus"].keys() %} ++ ++ {%- endfor %} ++ ++ {%- endif %} ++ {%- if cpu %} ++ ++ {%- if cpu.model %} ++ {{ cpu.model.get('name', '') }} ++ {%- endif %} ++ {%- if cpu.vendor %} ++ {{ cpu.get('vendor', '') }} ++ {%- endif %} ++ {%- if cpu.topology %} ++ ++ {%- endif %} ++ {%- if cpu.cache %} ++ ++ {%- endif %} ++ {%- if cpu.features %} ++ {%- for k, v in cpu.features.items() %} ++ ++ {%- endfor %} ++ {%- endif %} ++ {%- if cpu.numa %} ++ ++ {%- for numa_id in cpu.numa.keys() %} ++ {%- if cpu.numa.get(numa_id) %} ++ ++ {%- if cpu.numa[numa_id].distances %} ++ ++ {%- for sibling_id in cpu.numa[numa_id].distances %} ++ ++ {%- endfor %} ++ ++ {%- endif %} ++ ++ {%- endif %} ++ {%- endfor %} ++ ++ {%- endif %} ++ ++ {%- if cpu.iothreads %} ++ {{ cpu.iothreads }} ++ {%- endif %} ++ {%- endif %} ++ {%- if cpu.tuning %} ++ ++ {%- if cpu.tuning.vcpupin %} ++ {%- for vcpu_id, cpuset in cpu.tuning.vcpupin.items() %} ++ ++ {%- endfor %} ++ {%- endif %} ++ {%- if cpu.tuning.emulatorpin %} ++ ++ {%- endif %} ++ {%- if cpu.tuning.iothreadpin %} ++ {%- for thread_id, cpuset in cpu.tuning.iothreadpin.items() %} ++ ++ {%- endfor %} ++ {%- endif %} ++ {%- if cpu.tuning.shares %} ++ {{ cpu.tuning.shares }} ++ {%- endif %} ++ {%- if cpu.tuning.period %} ++ {{ cpu.tuning.period }} ++ {%- endif %} ++ {%- if cpu.tuning.quota %} ++ {{ cpu.tuning.quota }} ++ {%- endif %} ++ {%- if cpu.tuning.global_period %} ++ {{ cpu.tuning.global_period }} ++ {%- endif %} ++ {%- if cpu.tuning.global_quota %} ++ {{ cpu.tuning.global_quota }} ++ {%- endif %} ++ {%- if cpu.tuning.emulator_period %} ++ {{ cpu.tuning.emulator_period }} ++ {%- endif %} ++ {%- if cpu.tuning.emulator_quota %} ++ {{ cpu.tuning.emulator_quota }} ++ {%- endif %} ++ {%- if cpu.tuning.iothread_period %} ++ {{ cpu.tuning.iothread_period }} ++ {%- endif %} ++ {%- if cpu.tuning.iothread_quota %} ++ {{ cpu.tuning.iothread_quota }} ++ {%- endif %} ++ {%- if cpu.tuning.vcpusched %} ++ {%- for sched in cpu.tuning.vcpusched %} ++ ++ {%- endfor %} ++ {%- endif %} ++ {%- if cpu.tuning.iothreadsched %} ++ {%- for sched in cpu.tuning.iothreadsched %} ++ ++ {%- endfor %} ++ {%- endif %} ++ {%- if cpu.tuning.emulatorsched %} ++ ++ {%- endif %} ++ {%- if cpu.tuning.cachetune %} ++ {%- for k, v in cpu.tuning.cachetune.items() %} ++ ++ {%- for e, atrs in v.items() %} ++ {%- if e is number and atrs %} ++ ++ {%- elif e is not number %} ++ {%- for atr, val in atrs.items() %} ++ ++ {%- endfor %} ++ {%- endif %} ++ {%- endfor %} ++ ++ {%- endfor %} ++ {%- endif %} ++ {%- if cpu.tuning.memorytune %} ++ {%- for vcpus, nodes in cpu.tuning.memorytune.items() %} ++ ++ {%- for id, bandwidth in nodes.items() %} ++ ++ {%- endfor %} ++ ++ {%- endfor %} ++ {%- endif %} ++ ++ {%- endif %} + {%- if mem.max %} +- {{ mem.max }} ++ {{ to_kib(mem.max) }} + {%- endif %} + {%- if mem.boot %} +- {{ mem.boot }} ++ {{ to_kib(mem.boot) }} + {%- endif %} + {%- if mem.current %} +- {{ mem.current }} ++ {{ to_kib(mem.current) }} + {%- endif %} + {%- if mem %} + + {%- if 'hard_limit' in mem and mem.hard_limit %} +- {{ mem.hard_limit }} ++ {{ to_kib(mem.hard_limit) }} + {%- endif %} + {%- if 'soft_limit' in mem and mem.soft_limit %} +- {{ mem.soft_limit }} ++ {{ to_kib(mem.soft_limit) }} + {%- endif %} + {%- if 'swap_hard_limit' in mem and mem.swap_hard_limit %} +- {{ mem.swap_hard_limit }} ++ {{ to_kib(mem.swap_hard_limit) }} + {%- endif %} + {%- if 'min_guarantee' in mem and mem.min_guarantee %} +- {{ mem.min_guarantee }} ++ {{ to_kib(mem.min_guarantee) }} + {%- endif %} + + {%- endif %} ++ {%- if numatune %} ++ ++ {%- if 'memory' in numatune and numatune.memory %} ++ ++ {%- endif %} ++ {%- if 'memnodes' in numatune and numatune.memnodes %} ++ {%- for cell_id in numatune['memnodes'] %} ++ ++ {%- endfor %} ++ {%- endif %} ++ ++ {%- endif %} ++ {%- if mem %} ++ ++ {%- if mem.hugepages %} ++ ++ {%- for page in mem.hugepages %} ++ ++ {%- endfor %} ++ ++ {%- if mem.nosharepages %} ++ ++ {%- endif %} ++ {%- if mem.locked %} ++ ++ {%- endif %} ++ {%- if mem.source %} ++ ++ {%- endif %} ++ {%- if mem.access %} ++ ++ {%- endif %} ++ {%- if mem.allocation %} ++ ++ {%- endif %} ++ {%- if mem.discard %} ++ ++ {%- endif %} ++ {%- endif %} ++ ++ {%- endif %} + + {{ os_type }} + {% if boot %} +@@ -50,6 +238,18 @@ + + {% endfor %} + ++{%- if clock %} ++ ++ {%- for timer_name in clock.timers %} ++ {%- set timer = clock.timers[timer_name] %} ++ ++ {%- if "threshold" in timer or "slew" in timer or "limit" in timer %} ++ ++ {%- endif %} ++ ++ {%- endfor %} ++ ++{%- endif %} + {{ on_reboot }} + + {% for disk in disks %} +@@ -69,7 +269,7 @@ +
+ {% endif %} + {% if disk.driver -%} +- ++ + {% endif %} + + {% endfor %} +@@ -104,35 +304,39 @@ + address='{{ graphics.listen.address }}' + {% endif %}/> + +- {% endif %} +- {% if serial_type == 'pty' %} +- +- +- +- {% if console %} +- +- +- +- {% endif %} ++ ++ {% if graphics.type == "spice" -%} ++ ++ ++ ++ {%- endif %} + {% endif %} + +- {% if serial_type == 'tcp' %} +- +- +- +- ++ {%- for serial in serials %} ++ ++ {{ libvirt_chardevs.chardev(serial) }} + +- {% if console %} +- +- +- +- +- +- {% endif %} +- {% endif %} ++ {%- endfor %} + ++ {%- for console in consoles %} ++ ++ {{ libvirt_chardevs.chardev(console) }} ++ ++ {% endfor %} ++{%- if hypervisor in ["qemu", "kvm"] %} ++ ++ ++ ++{%- endif %} + + + ++ ++ ++{%- if hypervisor_features.get("kvm-hint-dedicated") %} ++ ++ ++ ++{%- endif %} + + +diff --git a/salt/utils/xmlutil.py b/salt/utils/xmlutil.py +index d25f5c8da5..5c187ca7e5 100644 +--- a/salt/utils/xmlutil.py ++++ b/salt/utils/xmlutil.py +@@ -157,18 +157,24 @@ def clean_node(parent_map, node, ignored=None): + :param parent_map: dictionary mapping each node to its parent + :param node: the node to clean + :param ignored: a list of ignored attributes. ++ :return: True if anything has been removed, False otherwise + """ + has_text = node.text is not None and node.text.strip() + parent = parent_map.get(node) ++ removed = False + if ( + len(set(node.attrib.keys()) - set(ignored or [])) == 0 + and not list(node) + and not has_text ++ and parent + ): + parent.remove(node) ++ removed = True + # Clean parent nodes if needed + if parent is not None: +- clean_node(parent_map, parent, ignored) ++ parent_cleaned = clean_node(parent_map, parent, ignored) ++ removed = removed or parent_cleaned ++ return removed + + + def del_text(parent_map, node): +@@ -180,6 +186,7 @@ def del_text(parent_map, node): + parent = parent_map[node] + parent.remove(node) + clean_node(parent, node) ++ return True + + + def del_attribute(attribute, ignored=None): +@@ -197,13 +204,54 @@ def del_attribute(attribute, ignored=None): + + def _do_delete(parent_map, node): + if attribute not in node.keys(): +- return ++ return False + node.attrib.pop(attribute) + clean_node(parent_map, node, ignored) ++ return True + + return _do_delete + + ++def attribute(path, xpath, attr_name, ignored=None, convert=None): ++ """ ++ Helper function creating a change_xml mapping entry for a text XML attribute. ++ ++ :param path: the path to the value in the data ++ :param xpath: the xpath to the node holding the attribute ++ :param attr_name: the attribute name ++ :param ignored: the list of attributes to ignore when cleaning up the node ++ :param convert: a function used to convert the value ++ """ ++ entry = { ++ "path": path, ++ "xpath": xpath, ++ "get": lambda n: n.get(attr_name), ++ "set": lambda n, v: n.set(attr_name, str(v)), ++ "del": salt.utils.xmlutil.del_attribute(attr_name, ignored), ++ } ++ if convert: ++ entry["convert"] = convert ++ return entry ++ ++ ++def int_attribute(path, xpath, attr_name, ignored=None): ++ """ ++ Helper function creating a change_xml mapping entry for a text XML integer attribute. ++ ++ :param path: the path to the value in the data ++ :param xpath: the xpath to the node holding the attribute ++ :param attr_name: the attribute name ++ :param ignored: the list of attributes to ignore when cleaning up the node ++ """ ++ return { ++ "path": path, ++ "xpath": xpath, ++ "get": lambda n: int(n.get(attr_name)) if n.get(attr_name) else None, ++ "set": lambda n, v: n.set(attr_name, str(v)), ++ "del": salt.utils.xmlutil.del_attribute(attr_name, ignored), ++ } ++ ++ + def change_xml(doc, data, mapping): + """ + Change an XML ElementTree document according. +@@ -237,6 +285,7 @@ def change_xml(doc, data, mapping): + del + function deleting the value in the XML. + Takes two parameters for the parent node and the node matched by the XPath. ++ Returns True if anything was removed, False otherwise. + Default is to remove the text value. + More cleanup may be performed, see the :py:func:`clean_node` function for details. + +@@ -281,8 +330,17 @@ def change_xml(doc, data, mapping): + continue + + if new_value is not None: ++ # We need to increment ids from arrays since xpath starts at 1 ++ converters = { ++ p: (lambda n: n + 1) ++ if "[${}]".format(p) in xpath ++ else (lambda n: n) ++ for p in placeholders ++ } + ctx = { +- placeholder: value_item.get(placeholder, "") ++ placeholder: converters[placeholder]( ++ value_item.get(placeholder, "") ++ ) + for placeholder in placeholders + } + node_xpath = string.Template(xpath).substitute(ctx) +@@ -299,7 +357,9 @@ def change_xml(doc, data, mapping): + if convert_fn: + new_value = convert_fn(new_value) + +- if str(current_value) != str(new_value): ++ # Allow custom comparison. Can be useful for almost equal numeric values ++ compare_fn = param.get("equals", lambda o, n: str(o) == str(n)) ++ if not compare_fn(current_value, new_value): + set_fn(node, new_value) + need_update = True + else: +@@ -307,17 +367,16 @@ def change_xml(doc, data, mapping): + del_fn = param.get("del", del_text) + parent_map = {c: p for p in doc.iter() for c in p} + for node in nodes: +- del_fn(parent_map, node) +- need_update = True ++ deleted = del_fn(parent_map, node) ++ need_update = need_update or deleted + + # Clean the left over XML elements if there were placeholders +- if placeholders and values[0].get("value") != []: ++ if placeholders and [v for v in values if v.get("value") != []]: + all_nodes = set(doc.findall(all_nodes_xpath)) + to_remove = all_nodes - kept_nodes + del_fn = param.get("del", del_text) + parent_map = {c: p for p in doc.iter() for c in p} + for node in to_remove: +- del_fn(parent_map, node) +- need_update = True +- ++ deleted = del_fn(parent_map, node) ++ need_update = need_update or deleted + return need_update +diff --git a/tests/pytests/unit/modules/virt/conftest.py b/tests/pytests/unit/modules/virt/conftest.py +index 1c32ae12eb..ec56bdff24 100644 +--- a/tests/pytests/unit/modules/virt/conftest.py ++++ b/tests/pytests/unit/modules/virt/conftest.py +@@ -189,3 +189,129 @@ def make_mock_storage_pool(): + return mocked_pool + + return _make_mock_storage_pool ++ ++ ++@pytest.fixture ++def make_capabilities(): ++ def _make_capabilities(): ++ mocked_conn = virt.libvirt.openAuth.return_value ++ mocked_conn.getCapabilities.return_value = """ ++ ++ ++ 44454c4c-3400-105a-8033-b3c04f4b344a ++ ++ x86_64 ++ Nehalem ++ Intel ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ tcp ++ rdma ++ ++ ++ ++ ++ ++ 12367120 ++ 3091780 ++ 0 ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ apparmor ++ 0 ++ ++ ++ dac ++ 0 ++ +487:+486 ++ +487:+486 ++ ++ ++ ++ ++ hvm ++ ++ 32 ++ /usr/bin/qemu-system-i386 ++ pc-i440fx-2.6 ++ pc ++ pc-0.12 ++ ++ ++ /usr/bin/qemu-kvm ++ pc-i440fx-2.6 ++ pc ++ pc-0.12 ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ hvm ++ ++ 64 ++ /usr/bin/qemu-system-x86_64 ++ pc-i440fx-2.6 ++ pc ++ pc-0.12 ++ ++ ++ /usr/bin/qemu-kvm ++ pc-i440fx-2.6 ++ pc ++ pc-0.12 ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++""" ++ ++ return _make_capabilities +diff --git a/tests/pytests/unit/modules/virt/test_domain.py b/tests/pytests/unit/modules/virt/test_domain.py +index 5f9b45ec9a..347c3bcd88 100644 +--- a/tests/pytests/unit/modules/virt/test_domain.py ++++ b/tests/pytests/unit/modules/virt/test_domain.py +@@ -254,3 +254,338 @@ def test_get_disk_convert_volumes(make_mock_vm, make_mock_storage_pool): + "virtual size": 214748364800, + }, + } == virt.get_disks("srv01") ++ ++ ++def test_update_approx_mem(make_mock_vm): ++ """ ++ test virt.update with memory parameter unchanged thought not exactly equals to the current value. ++ This may happen since libvirt sometimes rounds the memory value. ++ """ ++ xml_def = """ ++ ++ my_vm ++ 3177680 ++ 3177680 ++ 1 ++ ++ hvm ++ ++ restart ++ ++ """ ++ domain_mock = make_mock_vm(xml_def) ++ ++ ret = virt.update("my_vm", mem={"boot": "3253941043B", "current": "3253941043B"}) ++ assert not ret["definition"] ++ ++ ++def test_gen_hypervisor_features(): ++ """ ++ Test the virt._gen_xml hypervisor_features handling ++ """ ++ xml_data = virt._gen_xml( ++ virt.libvirt.openAuth.return_value, ++ "hello", ++ 1, ++ 512, ++ {}, ++ {}, ++ "kvm", ++ "hvm", ++ "x86_64", ++ hypervisor_features={"kvm-hint-dedicated": True}, ++ ) ++ root = ET.fromstring(xml_data) ++ assert "on" == root.find("features/kvm/hint-dedicated").attrib["state"] ++ ++ ++def test_update_hypervisor_features(make_mock_vm): ++ """ ++ Test changing the hypervisor features of a guest ++ """ ++ xml_def = """ ++ ++ my_vm ++ 524288 ++ 524288 ++ 1 ++ ++ linux ++ /usr/lib/grub2/x86_64-xen/grub.xen ++ ++ ++ ++ ++ ++ ++ restart ++ ++ """ ++ domain_mock = make_mock_vm(xml_def) ++ ++ # Update with no change to the features ++ ret = virt.update("my_vm", hypervisor_features={"kvm-hint-dedicated": True}) ++ assert not ret["definition"] ++ ++ # Alter the features ++ ret = virt.update("my_vm", hypervisor_features={"kvm-hint-dedicated": False}) ++ assert ret["definition"] ++ setxml = ET.fromstring(virt.libvirt.openAuth().defineXML.call_args[0][0]) ++ assert "off" == setxml.find("features/kvm/hint-dedicated").get("state") ++ ++ # Add the features ++ xml_def = """ ++ ++ my_vm ++ 524288 ++ 524288 ++ 1 ++ ++ linux ++ /usr/lib/grub2/x86_64-xen/grub.xen ++ ++ ++ """ ++ domain_mock = make_mock_vm(xml_def) ++ ret = virt.update("my_vm", hypervisor_features={"kvm-hint-dedicated": True}) ++ assert ret["definition"] ++ setxml = ET.fromstring(virt.libvirt.openAuth().defineXML.call_args[0][0]) ++ assert "on" == setxml.find("features/kvm/hint-dedicated").get("state") ++ ++ ++def test_gen_clock(): ++ """ ++ Test the virt._gen_xml clock property ++ """ ++ # Localtime with adjustment ++ xml_data = virt._gen_xml( ++ virt.libvirt.openAuth.return_value, ++ "hello", ++ 1, ++ 512, ++ {}, ++ {}, ++ "kvm", ++ "hvm", ++ "x86_64", ++ clock={"adjustment": 3600, "utc": False}, ++ ) ++ root = ET.fromstring(xml_data) ++ assert "localtime" == root.find("clock").get("offset") ++ assert "3600" == root.find("clock").get("adjustment") ++ ++ # Specific timezone ++ xml_data = virt._gen_xml( ++ virt.libvirt.openAuth.return_value, ++ "hello", ++ 1, ++ 512, ++ {}, ++ {}, ++ "kvm", ++ "hvm", ++ "x86_64", ++ clock={"timezone": "CEST"}, ++ ) ++ root = ET.fromstring(xml_data) ++ assert "timezone" == root.find("clock").get("offset") ++ assert "CEST" == root.find("clock").get("timezone") ++ ++ # UTC ++ xml_data = virt._gen_xml( ++ virt.libvirt.openAuth.return_value, ++ "hello", ++ 1, ++ 512, ++ {}, ++ {}, ++ "kvm", ++ "hvm", ++ "x86_64", ++ clock={"utc": True}, ++ ) ++ root = ET.fromstring(xml_data) ++ assert "utc" == root.find("clock").get("offset") ++ ++ # Timers ++ xml_data = virt._gen_xml( ++ virt.libvirt.openAuth.return_value, ++ "hello", ++ 1, ++ 512, ++ {}, ++ {}, ++ "kvm", ++ "hvm", ++ "x86_64", ++ clock={ ++ "timers": { ++ "tsc": {"frequency": 3504000000, "mode": "native"}, ++ "rtc": { ++ "tickpolicy": "catchup", ++ "slew": 4636, ++ "threshold": 123, ++ "limit": 2342, ++ }, ++ "hpet": {"present": False}, ++ }, ++ }, ++ ) ++ root = ET.fromstring(xml_data) ++ assert "utc" == root.find("clock").get("offset") ++ assert "3504000000" == root.find("clock/timer[@name='tsc']").get("frequency") ++ assert "native" == root.find("clock/timer[@name='tsc']").get("mode") ++ assert "catchup" == root.find("clock/timer[@name='rtc']").get("tickpolicy") ++ assert {"slew": "4636", "threshold": "123", "limit": "2342"} == root.find( ++ "clock/timer[@name='rtc']/catchup" ++ ).attrib ++ assert "no" == root.find("clock/timer[@name='hpet']").get("present") ++ ++ ++def test_update_clock(make_mock_vm): ++ """ ++ test virt.update with clock parameter ++ """ ++ xml_def = """ ++ ++ my_vm ++ 524288 ++ 524288 ++ 1 ++ ++ linux ++ /usr/lib/grub2/x86_64-xen/grub.xen ++ ++ ++ ++ ++ ++ restart ++ ++ """ ++ domain_mock = make_mock_vm(xml_def) ++ ++ # Update with no change to the features ++ ret = virt.update( ++ "my_vm", ++ clock={ ++ "utc": False, ++ "adjustment": -3600, ++ "timers": { ++ "tsc": {"frequency": 3504000000, "mode": "native"}, ++ "kvmclock": {"present": False}, ++ }, ++ }, ++ ) ++ assert not ret["definition"] ++ ++ # Update ++ ret = virt.update( ++ "my_vm", ++ clock={ ++ "timezone": "CEST", ++ "timers": { ++ "rtc": { ++ "track": "wall", ++ "tickpolicy": "catchup", ++ "slew": 4636, ++ "threshold": 123, ++ "limit": 2342, ++ }, ++ "hpet": {"present": True}, ++ }, ++ }, ++ ) ++ assert ret["definition"] ++ setxml = ET.fromstring(virt.libvirt.openAuth().defineXML.call_args[0][0]) ++ assert "timezone" == setxml.find("clock").get("offset") ++ assert "CEST" == setxml.find("clock").get("timezone") ++ assert {"rtc", "hpet"} == {t.get("name") for t in setxml.findall("clock/timer")} ++ assert "catchup" == setxml.find("clock/timer[@name='rtc']").get("tickpolicy") ++ assert "wall" == setxml.find("clock/timer[@name='rtc']").get("track") ++ assert {"slew": "4636", "threshold": "123", "limit": "2342"} == setxml.find( ++ "clock/timer[@name='rtc']/catchup" ++ ).attrib ++ assert "yes" == setxml.find("clock/timer[@name='hpet']").get("present") ++ ++ # Revert to UTC ++ ret = virt.update("my_vm", clock={"utc": True, "adjustment": None, "timers": None}) ++ assert ret["definition"] ++ setxml = ET.fromstring(virt.libvirt.openAuth().defineXML.call_args[0][0]) ++ assert {"offset": "utc"} == setxml.find("clock").attrib ++ assert setxml.find("clock/timer") is None ++ ++ ++def test_update_stop_on_reboot_reset(make_mock_vm): ++ """ ++ Test virt.update to remove the on_reboot=destroy flag ++ """ ++ xml_def = """ ++ ++ my_vm ++ 524288 ++ 524288 ++ 1 ++ destroy ++ ++ hvm ++ ++ """ ++ domain_mock = make_mock_vm(xml_def) ++ ++ ret = virt.update("my_vm") ++ ++ assert ret["definition"] ++ define_mock = virt.libvirt.openAuth().defineXML ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ assert "restart" == setxml.find("./on_reboot").text ++ ++ ++def test_update_stop_on_reboot(make_mock_vm): ++ """ ++ Test virt.update to add the on_reboot=destroy flag ++ """ ++ xml_def = """ ++ ++ my_vm ++ 524288 ++ 524288 ++ 1 ++ ++ hvm ++ ++ """ ++ domain_mock = make_mock_vm(xml_def) ++ ++ ret = virt.update("my_vm", stop_on_reboot=True) ++ ++ assert ret["definition"] ++ define_mock = virt.libvirt.openAuth().defineXML ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ assert "destroy" == setxml.find("./on_reboot").text ++ ++ ++def test_init_no_stop_on_reboot(make_capabilities): ++ """ ++ Test virt.init to add the on_reboot=restart flag ++ """ ++ make_capabilities() ++ with patch.dict(virt.os.__dict__, {"chmod": MagicMock(), "makedirs": MagicMock()}): ++ with patch.dict(virt.__salt__, {"cmd.run": MagicMock()}): ++ virt.init("test_vm", 2, 2048, start=False) ++ define_mock = virt.libvirt.openAuth().defineXML ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ assert "restart" == setxml.find("./on_reboot").text ++ ++ ++def test_init_stop_on_reboot(make_capabilities): ++ """ ++ Test virt.init to add the on_reboot=destroy flag ++ """ ++ make_capabilities() ++ with patch.dict(virt.os.__dict__, {"chmod": MagicMock(), "makedirs": MagicMock()}): ++ with patch.dict(virt.__salt__, {"cmd.run": MagicMock()}): ++ virt.init("test_vm", 2, 2048, stop_on_reboot=True, start=False) ++ define_mock = virt.libvirt.openAuth().defineXML ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ assert "destroy" == setxml.find("./on_reboot").text +diff --git a/tests/pytests/unit/utils/test_xmlutil.py b/tests/pytests/unit/utils/test_xmlutil.py +index 081cc64193..2bcaff3a17 100644 +--- a/tests/pytests/unit/utils/test_xmlutil.py ++++ b/tests/pytests/unit/utils/test_xmlutil.py +@@ -16,6 +16,11 @@ def xml_doc(): + + + ++ ++ ++ ++ ++ + + """ + ) +@@ -36,6 +41,22 @@ def test_change_xml_text_nochange(xml_doc): + assert not ret + + ++def test_change_xml_equals_nochange(xml_doc): ++ ret = xml.change_xml( ++ xml_doc, ++ {"mem": 1023}, ++ [ ++ { ++ "path": "mem", ++ "xpath": "memory", ++ "get": lambda n: int(n.text), ++ "equals": lambda o, n: abs(o - n) <= 1, ++ } ++ ], ++ ) ++ assert not ret ++ ++ + def test_change_xml_text_notdefined(xml_doc): + ret = xml.change_xml(xml_doc, {}, [{"path": "name", "xpath": "name"}]) + assert not ret +@@ -167,3 +188,23 @@ def test_change_xml_template_remove(xml_doc): + ) + assert ret + assert xml_doc.find("vcpus") is None ++ ++ ++def test_change_xml_template_list(xml_doc): ++ ret = xml.change_xml( ++ xml_doc, ++ {"memtune": {"hugepages": [{"size": "1024"}, {"size": "512"}]}}, ++ [ ++ { ++ "path": "memtune:hugepages:{id}:size", ++ "xpath": "memtune/hugepages/page[$id]", ++ "get": lambda n: n.get("size"), ++ "set": lambda n, v: n.set("size", v), ++ "del": xml.del_attribute("size"), ++ }, ++ ], ++ ) ++ assert ret ++ assert ["1024", "512"] == [ ++ n.get("size") for n in xml_doc.findall("memtune/hugepages/page") ++ ] +diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py +index 83152eda6e..91dee2098d 100644 +--- a/tests/unit/modules/test_virt.py ++++ b/tests/unit/modules/test_virt.py +@@ -106,6 +106,10 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + mock_domain.name.return_value = name + return mock_domain + ++ def assertEqualUnit(self, actual, expected, unit="KiB"): ++ self.assertEqual(actual.get("unit"), unit) ++ self.assertEqual(actual.text, str(expected)) ++ + def test_disk_profile_merge(self): + """ + Test virt._disk_profile() when merging with user-defined disks +@@ -215,16 +219,14 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + "kvm", + "hvm", + "x86_64", +- serial_type="pty", +- console=True, ++ serials=[{"type": "pty"}], + ) + root = ET.fromstring(xml_data) + self.assertEqual(root.find("devices/serial").attrib["type"], "pty") +- self.assertEqual(root.find("devices/console").attrib["type"], "pty") + +- def test_gen_xml_for_serial_console(self): ++ def test_gen_xml_for_telnet_serial(self): + """ +- Test virt._gen_xml() serial console ++ Test virt._gen_xml() telnet serial + """ + diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") + nicp = virt._nic_profile("default", "kvm") +@@ -238,11 +240,134 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + "kvm", + "hvm", + "x86_64", +- serial_type="pty", +- console=True, ++ serials=[{"type": "tcp", "port": 22223, "protocol": "telnet"}], ++ ) ++ root = ET.fromstring(xml_data) ++ self.assertEqual(root.find("devices/serial").attrib["type"], "tcp") ++ self.assertEqual(root.find("devices/serial/source").attrib["service"], "22223") ++ self.assertEqual(root.find("devices/serial/protocol").attrib["type"], "telnet") ++ ++ def test_gen_xml_for_telnet_serial_unspecified_port(self): ++ """ ++ Test virt._gen_xml() telnet serial without any specified port ++ """ ++ diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") ++ nicp = virt._nic_profile("default", "kvm") ++ xml_data = virt._gen_xml( ++ self.mock_conn, ++ "hello", ++ 1, ++ 512, ++ diskp, ++ nicp, ++ "kvm", ++ "hvm", ++ "x86_64", ++ serials=[{"type": "tcp"}], ++ ) ++ root = ET.fromstring(xml_data) ++ self.assertEqual(root.find("devices/serial").attrib["type"], "tcp") ++ self.assertEqual(root.find("devices/serial/source").attrib["service"], "23023") ++ self.assertFalse("tls" in root.find("devices/serial/source").keys()) ++ self.assertEqual(root.find("devices/serial/protocol").attrib["type"], "telnet") ++ ++ def test_gen_xml_for_chardev_types(self): ++ """ ++ Test virt._gen_xml() consoles and serials of various types ++ """ ++ diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") ++ nicp = virt._nic_profile("default", "kvm") ++ xml_data = virt._gen_xml( ++ self.mock_conn, ++ "hello", ++ 1, ++ 512, ++ diskp, ++ nicp, ++ "kvm", ++ "hvm", ++ "x86_64", ++ consoles=[ ++ {"type": "pty", "path": "/dev/pts/2", "target_port": 2}, ++ {"type": "pty", "target_type": "usb-serial"}, ++ {"type": "stdio"}, ++ {"type": "file", "path": "/path/to/serial.log"}, ++ ], ++ serials=[ ++ {"type": "pipe", "path": "/tmp/mypipe"}, ++ {"type": "udp", "host": "127.0.0.1", "port": 1234}, ++ {"type": "tcp", "port": 22223, "protocol": "raw", "tls": True}, ++ {"type": "unix", "path": "/path/to/socket"}, ++ ], ++ ) ++ root = ET.fromstring(xml_data) ++ ++ self.assertEqual(root.find("devices/console[1]").attrib["type"], "pty") ++ self.assertEqual( ++ root.find("devices/console[1]/source").attrib["path"], "/dev/pts/2" ++ ) ++ self.assertEqual(root.find("devices/console[1]/target").attrib["port"], "2") ++ ++ self.assertEqual(root.find("devices/console[2]").attrib["type"], "pty") ++ self.assertIsNone(root.find("devices/console[2]/source")) ++ self.assertEqual( ++ root.find("devices/console[2]/target").attrib["type"], "usb-serial" ++ ) ++ ++ self.assertEqual(root.find("devices/console[3]").attrib["type"], "stdio") ++ self.assertIsNone(root.find("devices/console[3]/source")) ++ ++ self.assertEqual(root.find("devices/console[4]").attrib["type"], "file") ++ self.assertEqual( ++ root.find("devices/console[4]/source").attrib["path"], "/path/to/serial.log" ++ ) ++ ++ self.assertEqual(root.find("devices/serial[1]").attrib["type"], "pipe") ++ self.assertEqual( ++ root.find("devices/serial[1]/source").attrib["path"], "/tmp/mypipe" ++ ) ++ ++ self.assertEqual(root.find("devices/serial[2]").attrib["type"], "udp") ++ self.assertEqual(root.find("devices/serial[2]/source").attrib["mode"], "bind") ++ self.assertEqual( ++ root.find("devices/serial[2]/source").attrib["service"], "1234" ++ ) ++ self.assertEqual( ++ root.find("devices/serial[2]/source").attrib["host"], "127.0.0.1" ++ ) ++ ++ self.assertEqual(root.find("devices/serial[3]").attrib["type"], "tcp") ++ self.assertEqual(root.find("devices/serial[3]/source").attrib["mode"], "bind") ++ self.assertEqual( ++ root.find("devices/serial[3]/source").attrib["service"], "22223" ++ ) ++ self.assertEqual(root.find("devices/serial[3]/source").attrib["tls"], "yes") ++ self.assertEqual(root.find("devices/serial[3]/protocol").attrib["type"], "raw") ++ ++ self.assertEqual(root.find("devices/serial[4]").attrib["type"], "unix") ++ self.assertEqual( ++ root.find("devices/serial[4]/source").attrib["path"], "/path/to/socket" ++ ) ++ ++ def test_gen_xml_no_nic_console(self): ++ """ ++ Test virt._gen_xml() console ++ """ ++ diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") ++ nicp = virt._nic_profile("default", "kvm") ++ xml_data = virt._gen_xml( ++ self.mock_conn, ++ "hello", ++ 1, ++ 512, ++ diskp, ++ nicp, ++ "kvm", ++ "hvm", ++ "x86_64", ++ consoles=[{"type": "pty"}], + ) + root = ET.fromstring(xml_data) +- self.assertEqual(root.find("devices/serial").attrib["type"], "pty") + self.assertEqual(root.find("devices/console").attrib["type"], "pty") + + def test_gen_xml_for_telnet_console(self): +@@ -261,14 +386,12 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + "kvm", + "hvm", + "x86_64", +- serial_type="tcp", +- console=True, +- telnet_port=22223, ++ consoles=[{"type": "tcp", "port": 22223, "protocol": "telnet"}], + ) + root = ET.fromstring(xml_data) +- self.assertEqual(root.find("devices/serial").attrib["type"], "tcp") + self.assertEqual(root.find("devices/console").attrib["type"], "tcp") + self.assertEqual(root.find("devices/console/source").attrib["service"], "22223") ++ self.assertEqual(root.find("devices/console/protocol").attrib["type"], "telnet") + + def test_gen_xml_for_telnet_console_unspecified_port(self): + """ +@@ -286,15 +409,12 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + "kvm", + "hvm", + "x86_64", +- serial_type="tcp", +- console=True, ++ consoles=[{"type": "tcp"}], + ) + root = ET.fromstring(xml_data) +- self.assertEqual(root.find("devices/serial").attrib["type"], "tcp") + self.assertEqual(root.find("devices/console").attrib["type"], "tcp") +- self.assertIsInstance( +- int(root.find("devices/console/source").attrib["service"]), int +- ) ++ self.assertEqual(root.find("devices/console/source").attrib["service"], "23023") ++ self.assertEqual(root.find("devices/console/protocol").attrib["type"], "telnet") + + def test_gen_xml_for_serial_no_console(self): + """ +@@ -312,8 +432,8 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + "kvm", + "hvm", + "x86_64", +- serial_type="pty", +- console=False, ++ serials=[{"type": "pty"}], ++ consoles=[], + ) + root = ET.fromstring(xml_data) + self.assertEqual(root.find("devices/serial").attrib["type"], "pty") +@@ -335,8 +455,8 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + "kvm", + "hvm", + "x86_64", +- serial_type="tcp", +- console=False, ++ serials=[{"type": "tcp", "port": 22223, "protocol": "telnet"}], ++ consoles=[], + ) + root = ET.fromstring(xml_data) + self.assertEqual(root.find("devices/serial").attrib["type"], "tcp") +@@ -459,109 +579,493 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + self.assertEqual(root.find("devices/graphics/listen").attrib["type"], "none") + self.assertFalse("address" in root.find("devices/graphics/listen").attrib) + +- def test_default_disk_profile_hypervisor_esxi(self): ++ def test_gen_xml_memory(self): + """ +- Test virt._disk_profile() default ESXi profile ++ Test virt._gen_xml() with advanced memory settings + """ +- mock = MagicMock(return_value={}) +- with patch.dict( +- virt.__salt__, {"config.get": mock} # pylint: disable=no-member +- ): +- ret = virt._disk_profile( +- self.mock_conn, "nonexistent", "vmware", None, "test-vm" +- ) +- self.assertTrue(len(ret) == 1) +- found = [disk for disk in ret if disk["name"] == "system"] +- self.assertTrue(bool(found)) +- system = found[0] +- self.assertEqual(system["format"], "vmdk") +- self.assertEqual(system["model"], "scsi") +- self.assertTrue(int(system["size"]) >= 1) ++ diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") ++ nicp = virt._nic_profile("default", "kvm") ++ xml_data = virt._gen_xml( ++ self.mock_conn, ++ "hello", ++ 1, ++ { ++ "boot": "512m", ++ "current": "256m", ++ "max": "1g", ++ "hard_limit": "1024", ++ "soft_limit": "512m", ++ "swap_hard_limit": "1g", ++ "min_guarantee": "256m", ++ "hugepages": [ ++ {"nodeset": "", "size": "128m"}, ++ {"nodeset": "0", "size": "256m"}, ++ {"nodeset": "1", "size": "512m"}, ++ ], ++ "nosharepages": True, ++ "locked": True, ++ "source": "file", ++ "access": "shared", ++ "allocation": "immediate", ++ "discard": True, ++ }, ++ diskp, ++ nicp, ++ "kvm", ++ "hvm", ++ "x86_64", ++ ) ++ root = ET.fromstring(xml_data) ++ self.assertEqualUnit(root.find("memory"), 512 * 1024) ++ self.assertEqualUnit(root.find("currentMemory"), 256 * 1024) ++ self.assertEqualUnit(root.find("maxMemory"), 1024 * 1024) ++ self.assertFalse("slots" in root.find("maxMemory").keys()) ++ self.assertEqualUnit(root.find("memtune/hard_limit"), 1024 * 1024) ++ self.assertEqualUnit(root.find("memtune/soft_limit"), 512 * 1024) ++ self.assertEqualUnit(root.find("memtune/swap_hard_limit"), 1024 ** 2) ++ self.assertEqualUnit(root.find("memtune/min_guarantee"), 256 * 1024) ++ self.assertEqual( ++ [ ++ {"nodeset": page.get("nodeset"), "size": page.get("size")} ++ for page in root.findall("memoryBacking/hugepages/page") ++ ], ++ [ ++ {"nodeset": None, "size": str(128 * 1024)}, ++ {"nodeset": "0", "size": str(256 * 1024)}, ++ {"nodeset": "1", "size": str(512 * 1024)}, ++ ], ++ ) ++ self.assertIsNotNone(root.find("memoryBacking/nosharepages")) ++ self.assertIsNotNone(root.find("memoryBacking/locked")) ++ self.assertIsNotNone(root.find("memoryBacking/discard")) ++ self.assertEqual(root.find("memoryBacking/source").get("type"), "file") ++ self.assertEqual(root.find("memoryBacking/access").get("mode"), "shared") ++ self.assertEqual(root.find("memoryBacking/allocation").get("mode"), "immediate") + +- def test_default_disk_profile_hypervisor_kvm(self): ++ def test_gen_xml_cpu(self): + """ +- Test virt._disk_profile() default KVM profile ++ Test virt._gen_xml() with CPU advanced properties + """ +- mock = MagicMock(side_effect=[{}, "/images/dir"]) +- with patch.dict( +- virt.__salt__, {"config.get": mock} # pylint: disable=no-member +- ): +- ret = virt._disk_profile( +- self.mock_conn, "nonexistent", "kvm", None, "test-vm" +- ) +- self.assertTrue(len(ret) == 1) +- found = [disk for disk in ret if disk["name"] == "system"] +- self.assertTrue(bool(found)) +- system = found[0] +- self.assertEqual(system["format"], "qcow2") +- self.assertEqual(system["model"], "virtio") +- self.assertTrue(int(system["size"]) >= 1) ++ diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") ++ nicp = virt._nic_profile("default", "kvm") ++ xml_data = virt._gen_xml( ++ self.mock_conn, ++ "hello", ++ { ++ "maximum": 12, ++ "placement": "static", ++ "cpuset": "0-11", ++ "current": 5, ++ "mode": "custom", ++ "match": "minimum", ++ "check": "full", ++ "vendor": "Intel", ++ "model": { ++ "name": "core2duo", ++ "fallback": "allow", ++ "vendor_id": "GenuineIntel", ++ }, ++ "cache": {"level": 3, "mode": "emulate"}, ++ "features": {"lahf": "optional", "vmx": "require"}, ++ "vcpus": { ++ 0: {"enabled": True, "hotpluggable": True}, ++ 1: {"enabled": False}, ++ }, ++ }, ++ 512, ++ diskp, ++ nicp, ++ "kvm", ++ "hvm", ++ "x86_64", ++ ) ++ root = ET.fromstring(xml_data) ++ self.assertEqual(root.find("vcpu").get("current"), "5") ++ self.assertEqual(root.find("vcpu").get("placement"), "static") ++ self.assertEqual(root.find("vcpu").get("cpuset"), "0-11") ++ self.assertEqual(root.find("vcpu").text, "12") ++ self.assertEqual(root.find("cpu").get("match"), "minimum") ++ self.assertEqual(root.find("cpu").get("mode"), "custom") ++ self.assertEqual(root.find("cpu").get("check"), "full") ++ self.assertEqual(root.find("cpu/vendor").text, "Intel") ++ self.assertEqual(root.find("cpu/model").text, "core2duo") ++ self.assertEqual(root.find("cpu/model").get("fallback"), "allow") ++ self.assertEqual(root.find("cpu/model").get("vendor_id"), "GenuineIntel") ++ self.assertEqual(root.find("cpu/cache").get("level"), "3") ++ self.assertEqual(root.find("cpu/cache").get("mode"), "emulate") ++ self.assertEqual( ++ {f.get("name"): f.get("policy") for f in root.findall("cpu/feature")}, ++ {"lahf": "optional", "vmx": "require"}, ++ ) ++ self.assertEqual( ++ { ++ v.get("id"): { ++ "enabled": v.get("enabled"), ++ "hotpluggable": v.get("hotpluggable"), ++ } ++ for v in root.findall("vcpus/vcpu") ++ }, ++ { ++ "0": {"enabled": "yes", "hotpluggable": "yes"}, ++ "1": {"enabled": "no", "hotpluggable": None}, ++ }, ++ ) + +- def test_default_disk_profile_hypervisor_xen(self): ++ def test_gen_xml_cpu_topology(self): + """ +- Test virt._disk_profile() default XEN profile ++ Test virt._gen_xml() with CPU topology + """ +- mock = MagicMock(side_effect=[{}, "/images/dir"]) +- with patch.dict( +- virt.__salt__, {"config.get": mock} # pylint: disable=no-member +- ): +- ret = virt._disk_profile( +- self.mock_conn, "nonexistent", "xen", None, "test-vm" +- ) +- self.assertTrue(len(ret) == 1) +- found = [disk for disk in ret if disk["name"] == "system"] +- self.assertTrue(bool(found)) +- system = found[0] +- self.assertEqual(system["format"], "qcow2") +- self.assertEqual(system["model"], "xen") +- self.assertTrue(int(system["size"]) >= 1) ++ diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") ++ nicp = virt._nic_profile("default", "kvm") ++ xml_data = virt._gen_xml( ++ self.mock_conn, ++ "hello", ++ {"maximum": 1, "topology": {"sockets": 4, "cores": 16, "threads": 2}}, ++ 512, ++ diskp, ++ nicp, ++ "kvm", ++ "hvm", ++ "x86_64", ++ ) ++ root = ET.fromstring(xml_data) ++ self.assertEqual(root.find("cpu/topology").get("sockets"), "4") ++ self.assertEqual(root.find("cpu/topology").get("cores"), "16") ++ self.assertEqual(root.find("cpu/topology").get("threads"), "2") + +- def test_default_nic_profile_hypervisor_esxi(self): ++ def test_gen_xml_cpu_numa(self): + """ +- Test virt._nic_profile() default ESXi profile ++ Test virt._gen_xml() with CPU numa settings + """ +- mock = MagicMock(return_value={}) +- with patch.dict( +- virt.__salt__, {"config.get": mock} # pylint: disable=no-member +- ): +- ret = virt._nic_profile("nonexistent", "vmware") +- self.assertTrue(len(ret) == 1) +- eth0 = ret[0] +- self.assertEqual(eth0["name"], "eth0") +- self.assertEqual(eth0["type"], "bridge") +- self.assertEqual(eth0["source"], "DEFAULT") +- self.assertEqual(eth0["model"], "e1000") ++ diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") ++ nicp = virt._nic_profile("default", "kvm") ++ xml_data = virt._gen_xml( ++ self.mock_conn, ++ "hello", ++ { ++ "maximum": 1, ++ "numa": { ++ 0: { ++ "cpus": "0-3", ++ "memory": "1g", ++ "discard": True, ++ "distances": {0: 10, 1: 20}, ++ }, ++ 1: {"cpus": "4-7", "memory": "2g", "distances": {0: 20, 1: 10}}, ++ }, ++ }, ++ 512, ++ diskp, ++ nicp, ++ "kvm", ++ "hvm", ++ "x86_64", ++ ) ++ root = ET.fromstring(xml_data) ++ cell0 = root.find("cpu/numa/cell[@id='0']") ++ self.assertEqual(cell0.get("cpus"), "0-3") ++ self.assertIsNone(cell0.get("unit")) ++ self.assertEqual(cell0.get("memory"), str(1024 ** 2)) ++ self.assertEqual(cell0.get("discard"), "yes") ++ self.assertEqual( ++ {d.get("id"): d.get("value") for d in cell0.findall("distances/sibling")}, ++ {"0": "10", "1": "20"}, ++ ) + +- def test_default_nic_profile_hypervisor_kvm(self): +- """ +- Test virt._nic_profile() default KVM profile +- """ +- mock = MagicMock(return_value={}) +- with patch.dict( +- virt.__salt__, {"config.get": mock} # pylint: disable=no-member +- ): +- ret = virt._nic_profile("nonexistent", "kvm") +- self.assertTrue(len(ret) == 1) +- eth0 = ret[0] +- self.assertEqual(eth0["name"], "eth0") +- self.assertEqual(eth0["type"], "bridge") +- self.assertEqual(eth0["source"], "br0") +- self.assertEqual(eth0["model"], "virtio") ++ cell1 = root.find("cpu/numa/cell[@id='1']") ++ self.assertEqual(cell1.get("cpus"), "4-7") ++ self.assertIsNone(cell0.get("unit")) ++ self.assertEqual(cell1.get("memory"), str(2 * 1024 ** 2)) ++ self.assertFalse("discard" in cell1.keys()) ++ self.assertEqual( ++ {d.get("id"): d.get("value") for d in cell1.findall("distances/sibling")}, ++ {"0": "20", "1": "10"}, ++ ) + +- def test_default_nic_profile_hypervisor_xen(self): ++ def test_gen_xml_cputune(self): + """ +- Test virt._nic_profile() default XEN profile ++ Test virt._gen_xml() with CPU tuning + """ +- mock = MagicMock(return_value={}) +- with patch.dict( +- virt.__salt__, {"config.get": mock} # pylint: disable=no-member +- ): +- ret = virt._nic_profile("nonexistent", "xen") +- self.assertTrue(len(ret) == 1) +- eth0 = ret[0] +- self.assertEqual(eth0["name"], "eth0") +- self.assertEqual(eth0["type"], "bridge") +- self.assertEqual(eth0["source"], "br0") ++ diskp = virt._disk_profile(self.mock_conn, "default", "kvm", [], "hello") ++ nicp = virt._nic_profile("default", "kvm") ++ cputune = { ++ "shares": 2048, ++ "period": 122000, ++ "quota": -1, ++ "global_period": 1000000, ++ "global_quota": -3, ++ "emulator_period": 1200000, ++ "emulator_quota": -10, ++ "iothread_period": 133000, ++ "iothread_quota": -1, ++ "vcpupin": {0: "1-4,^2", 1: "0,1", 2: "2,3", 3: "0,4"}, ++ "emulatorpin": "1-3", ++ "iothreadpin": {1: "5-6", 2: "7-8"}, ++ "vcpusched": [ ++ {"scheduler": "fifo", "priority": 1, "vcpus": "0"}, ++ {"scheduler": "fifo", "priority": 2, "vcpus": "1"}, ++ {"scheduler": "idle", "priority": 3, "vcpus": "2"}, ++ ], ++ "iothreadsched": [ ++ {"scheduler": "idle"}, ++ {"scheduler": "batch", "iothreads": "5-7", "priority": 1}, ++ ], ++ "emulatorsched": {"scheduler": "rr", "priority": 2}, ++ "cachetune": { ++ "0-3": { ++ 0: {"level": 3, "type": "both", "size": 3}, ++ 1: {"level": 3, "type": "both", "size": 3}, ++ "monitor": {1: 3, "0-3": 3}, ++ }, ++ "4-5": {"monitor": {4: 3, 5: 2}}, ++ }, ++ "memorytune": {"0-2": {0: 60}, "3-4": {0: 50, 1: 70}}, ++ } ++ xml_data = virt._gen_xml( ++ self.mock_conn, ++ "hello", ++ {"maximum": 1, "tuning": cputune, "iothreads": 2}, ++ 512, ++ diskp, ++ nicp, ++ "kvm", ++ "hvm", ++ "x86_64", ++ ) ++ root = ET.fromstring(xml_data) ++ self.assertEqual(root.find("cputune").find("shares").text, "2048") ++ self.assertEqual(root.find("cputune").find("period").text, "122000") ++ self.assertEqual(root.find("cputune").find("quota").text, "-1") ++ self.assertEqual(root.find("cputune").find("global_period").text, "1000000") ++ self.assertEqual(root.find("cputune").find("global_quota").text, "-3") ++ self.assertEqual(root.find("cputune").find("emulator_period").text, "1200000") ++ self.assertEqual(root.find("cputune").find("emulator_quota").text, "-10") ++ self.assertEqual(root.find("cputune").find("iothread_period").text, "133000") ++ self.assertEqual(root.find("cputune").find("iothread_quota").text, "-1") ++ self.assertEqual( ++ root.find("cputune").find("vcpupin[@vcpu='0']").attrib.get("cpuset"), ++ "1-4,^2", ++ ) ++ self.assertEqual( ++ root.find("cputune").find("vcpupin[@vcpu='1']").attrib.get("cpuset"), "0,1", ++ ) ++ self.assertEqual( ++ root.find("cputune").find("vcpupin[@vcpu='2']").attrib.get("cpuset"), "2,3", ++ ) ++ self.assertEqual( ++ root.find("cputune").find("vcpupin[@vcpu='3']").attrib.get("cpuset"), "0,4", ++ ) ++ self.assertEqual( ++ root.find("cputune").find("emulatorpin").attrib.get("cpuset"), "1-3" ++ ) ++ self.assertEqual( ++ root.find("cputune") ++ .find("iothreadpin[@iothread='1']") ++ .attrib.get("cpuset"), ++ "5-6", ++ ) ++ self.assertEqual( ++ root.find("cputune") ++ .find("iothreadpin[@iothread='2']") ++ .attrib.get("cpuset"), ++ "7-8", ++ ) ++ self.assertDictEqual( ++ { ++ s.get("vcpus"): { ++ "scheduler": s.get("scheduler"), ++ "priority": s.get("priority"), ++ } ++ for s in root.findall("cputune/vcpusched") ++ }, ++ { ++ "0": {"scheduler": "fifo", "priority": "1"}, ++ "1": {"scheduler": "fifo", "priority": "2"}, ++ "2": {"scheduler": "idle", "priority": "3"}, ++ }, ++ ) ++ self.assertDictEqual( ++ { ++ s.get("iothreads"): { ++ "scheduler": s.get("scheduler"), ++ "priority": s.get("priority"), ++ } ++ for s in root.findall("cputune/iothreadsched") ++ }, ++ { ++ None: {"scheduler": "idle", "priority": None}, ++ "5-7": {"scheduler": "batch", "priority": "1"}, ++ }, ++ ) ++ self.assertEqual(root.find("cputune/emulatorsched").get("scheduler"), "rr") ++ self.assertEqual(root.find("cputune/emulatorsched").get("priority"), "2") ++ self.assertEqual( ++ root.find("./cputune/cachetune[@vcpus='0-3']").attrib.get("vcpus"), "0-3" ++ ) ++ self.assertEqual( ++ root.find("./cputune/cachetune[@vcpus='0-3']/cache[@id='0']").attrib.get( ++ "level" ++ ), ++ "3", ++ ) ++ self.assertEqual( ++ root.find("./cputune/cachetune[@vcpus='0-3']/cache[@id='0']").attrib.get( ++ "type" ++ ), ++ "both", ++ ) ++ self.assertEqual( ++ root.find( ++ "./cputune/cachetune[@vcpus='0-3']/monitor[@vcpus='1']" ++ ).attrib.get("level"), ++ "3", ++ ) ++ self.assertNotEqual( ++ root.find("./cputune/cachetune[@vcpus='0-3']/monitor[@vcpus='1']"), None ++ ) ++ self.assertNotEqual( ++ root.find("./cputune/cachetune[@vcpus='4-5']").attrib.get("vcpus"), None ++ ) ++ self.assertEqual( ++ root.find("./cputune/cachetune[@vcpus='4-5']/cache[@id='0']"), None ++ ) ++ self.assertEqual( ++ root.find( ++ "./cputune/cachetune[@vcpus='4-5']/monitor[@vcpus='4']" ++ ).attrib.get("level"), ++ "3", ++ ) ++ self.assertEqual( ++ root.find( ++ "./cputune/cachetune[@vcpus='4-5']/monitor[@vcpus='5']" ++ ).attrib.get("level"), ++ "2", ++ ) ++ self.assertNotEqual(root.find("./cputune/memorytune[@vcpus='0-2']"), None) ++ self.assertEqual( ++ root.find("./cputune/memorytune[@vcpus='0-2']/node[@id='0']").attrib.get( ++ "bandwidth" ++ ), ++ "60", ++ ) ++ self.assertNotEqual(root.find("./cputune/memorytune[@vcpus='3-4']"), None) ++ self.assertEqual( ++ root.find("./cputune/memorytune[@vcpus='3-4']/node[@id='0']").attrib.get( ++ "bandwidth" ++ ), ++ "50", ++ ) ++ self.assertEqual( ++ root.find("./cputune/memorytune[@vcpus='3-4']/node[@id='1']").attrib.get( ++ "bandwidth" ++ ), ++ "70", ++ ) ++ self.assertEqual(root.find("iothreads").text, "2") ++ ++ def test_default_disk_profile_hypervisor_esxi(self): ++ """ ++ Test virt._disk_profile() default ESXi profile ++ """ ++ mock = MagicMock(return_value={}) ++ with patch.dict( ++ virt.__salt__, {"config.get": mock} # pylint: disable=no-member ++ ): ++ ret = virt._disk_profile( ++ self.mock_conn, "nonexistent", "vmware", None, "test-vm" ++ ) ++ self.assertTrue(len(ret) == 1) ++ found = [disk for disk in ret if disk["name"] == "system"] ++ self.assertTrue(bool(found)) ++ system = found[0] ++ self.assertEqual(system["format"], "vmdk") ++ self.assertEqual(system["model"], "scsi") ++ self.assertTrue(int(system["size"]) >= 1) ++ ++ def test_default_disk_profile_hypervisor_kvm(self): ++ """ ++ Test virt._disk_profile() default KVM profile ++ """ ++ mock = MagicMock(side_effect=[{}, "/images/dir"]) ++ with patch.dict( ++ virt.__salt__, {"config.get": mock} # pylint: disable=no-member ++ ): ++ ret = virt._disk_profile( ++ self.mock_conn, "nonexistent", "kvm", None, "test-vm" ++ ) ++ self.assertTrue(len(ret) == 1) ++ found = [disk for disk in ret if disk["name"] == "system"] ++ self.assertTrue(bool(found)) ++ system = found[0] ++ self.assertEqual(system["format"], "qcow2") ++ self.assertEqual(system["model"], "virtio") ++ self.assertTrue(int(system["size"]) >= 1) ++ ++ def test_default_disk_profile_hypervisor_xen(self): ++ """ ++ Test virt._disk_profile() default XEN profile ++ """ ++ mock = MagicMock(side_effect=[{}, "/images/dir"]) ++ with patch.dict( ++ virt.__salt__, {"config.get": mock} # pylint: disable=no-member ++ ): ++ ret = virt._disk_profile( ++ self.mock_conn, "nonexistent", "xen", None, "test-vm" ++ ) ++ self.assertTrue(len(ret) == 1) ++ found = [disk for disk in ret if disk["name"] == "system"] ++ self.assertTrue(bool(found)) ++ system = found[0] ++ self.assertEqual(system["format"], "qcow2") ++ self.assertEqual(system["model"], "xen") ++ self.assertTrue(int(system["size"]) >= 1) ++ ++ def test_default_nic_profile_hypervisor_esxi(self): ++ """ ++ Test virt._nic_profile() default ESXi profile ++ """ ++ mock = MagicMock(return_value={}) ++ with patch.dict( ++ virt.__salt__, {"config.get": mock} # pylint: disable=no-member ++ ): ++ ret = virt._nic_profile("nonexistent", "vmware") ++ self.assertTrue(len(ret) == 1) ++ eth0 = ret[0] ++ self.assertEqual(eth0["name"], "eth0") ++ self.assertEqual(eth0["type"], "bridge") ++ self.assertEqual(eth0["source"], "DEFAULT") ++ self.assertEqual(eth0["model"], "e1000") ++ ++ def test_default_nic_profile_hypervisor_kvm(self): ++ """ ++ Test virt._nic_profile() default KVM profile ++ """ ++ mock = MagicMock(return_value={}) ++ with patch.dict( ++ virt.__salt__, {"config.get": mock} # pylint: disable=no-member ++ ): ++ ret = virt._nic_profile("nonexistent", "kvm") ++ self.assertTrue(len(ret) == 1) ++ eth0 = ret[0] ++ self.assertEqual(eth0["name"], "eth0") ++ self.assertEqual(eth0["type"], "bridge") ++ self.assertEqual(eth0["source"], "br0") ++ self.assertEqual(eth0["model"], "virtio") ++ ++ def test_default_nic_profile_hypervisor_xen(self): ++ """ ++ Test virt._nic_profile() default XEN profile ++ """ ++ mock = MagicMock(return_value={}) ++ with patch.dict( ++ virt.__salt__, {"config.get": mock} # pylint: disable=no-member ++ ): ++ ret = virt._nic_profile("nonexistent", "xen") ++ self.assertTrue(len(ret) == 1) ++ eth0 = ret[0] ++ self.assertEqual(eth0["name"], "eth0") ++ self.assertEqual(eth0["type"], "bridge") ++ self.assertEqual(eth0["source"], "br0") + self.assertFalse(eth0["model"]) + + def test_gen_vol_xml_esx(self): +@@ -1836,6 +2340,8 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + +
+ ++ ++ + + + """.format( +@@ -1896,10 +2402,11 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + username=None, + password=None, + boot=None, ++ numatune=None, + ), + ) + +- # Update vcpus case ++ # test cpu passed as an integer case + setvcpus_mock = MagicMock(return_value=0) + domain_mock.setVcpusFlags = setvcpus_mock + self.assertEqual( +@@ -1914,142 +2421,400 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("vcpu").text, "2") + self.assertEqual(setvcpus_mock.call_args[0][0], 2) ++ define_mock.reset_mock() + +- boot = { +- "kernel": "/root/f8-i386-vmlinuz", +- "initrd": "/root/f8-i386-initrd", +- "cmdline": "console=ttyS0 ks=http://example.com/f8-i386/os/", ++ # test updating vcpu attribute ++ vcpu = { ++ "placement": "static", ++ "cpuset": "0-11", ++ "current": 5, ++ "maximum": 12, + } +- +- # Update boot devices case +- define_mock.reset_mock() + self.assertEqual( + { + "definition": True, ++ "cpu": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, +- virt.update("my_vm", boot_dev="cdrom network hd"), ++ virt.update("my_vm", cpu=vcpu), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("vcpu").text, "12") ++ self.assertEqual(setxml.find("vcpu").attrib["placement"], "static") + self.assertEqual( +- ["cdrom", "network", "hd"], +- [node.get("dev") for node in setxml.findall("os/boot")], ++ setxml.find("vcpu").attrib["cpuset"], "0,1,2,3,4,5,6,7,8,9,10,11" + ) ++ self.assertEqual(setxml.find("vcpu").attrib["current"], "5") + +- # Update unchanged boot devices case +- define_mock.reset_mock() ++ # test adding vcpus elements ++ vcpus = { ++ "vcpus": { ++ "0": {"enabled": True, "hotpluggable": False, "order": 1}, ++ "1": {"enabled": False, "hotpluggable": True}, ++ } ++ } + self.assertEqual( + { +- "definition": False, ++ "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, +- virt.update("my_vm", boot_dev="hd"), ++ virt.update("my_vm", cpu=vcpus), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("./vcpus/vcpu/[@id='0']").attrib["id"], "0") ++ self.assertEqual(setxml.find("./vcpus/vcpu/[@id='0']").attrib["enabled"], "yes") ++ self.assertEqual( ++ setxml.find("./vcpus/vcpu/[@id='0']").attrib["hotpluggable"], "no" ++ ) ++ self.assertEqual(setxml.find("./vcpus/vcpu/[@id='0']").attrib["order"], "1") ++ self.assertEqual(setxml.find("./vcpus/vcpu/[@id='1']").attrib["id"], "1") ++ self.assertEqual(setxml.find("./vcpus/vcpu/[@id='1']").attrib["enabled"], "no") ++ self.assertEqual( ++ setxml.find("./vcpus/vcpu/[@id='1']").attrib["hotpluggable"], "yes" ++ ) ++ self.assertEqual( ++ setxml.find("./vcpus/vcpu/[@id='1']").attrib.get("order"), None + ) +- define_mock.assert_not_called() + +- # Update with boot parameter case +- define_mock.reset_mock() ++ # test adding cpu attribute ++ cpu_atr = {"mode": "custom", "match": "exact", "check": "full"} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, +- virt.update("my_vm", boot=boot), +- ) +- setxml = ET.fromstring(define_mock.call_args[0][0]) +- self.assertEqual(setxml.find("os").find("kernel").text, "/root/f8-i386-vmlinuz") +- self.assertEqual(setxml.find("os").find("initrd").text, "/root/f8-i386-initrd") +- self.assertEqual( +- setxml.find("os").find("cmdline").text, +- "console=ttyS0 ks=http://example.com/f8-i386/os/", ++ virt.update("my_vm", cpu=cpu_atr), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) +- self.assertEqual(setxml.find("os").find("kernel").text, "/root/f8-i386-vmlinuz") +- self.assertEqual(setxml.find("os").find("initrd").text, "/root/f8-i386-initrd") +- self.assertEqual( +- setxml.find("os").find("cmdline").text, +- "console=ttyS0 ks=http://example.com/f8-i386/os/", +- ) +- +- boot_uefi = { +- "loader": "/usr/share/OVMF/OVMF_CODE.fd", +- "nvram": "/usr/share/OVMF/OVMF_VARS.ms.fd", ++ self.assertEqual(setxml.find("cpu").attrib["mode"], "custom") ++ self.assertEqual(setxml.find("cpu").attrib["match"], "exact") ++ self.assertEqual(setxml.find("cpu").attrib["check"], "full") ++ ++ # test adding cpu model ++ cpu_model = { ++ "model": { ++ "name": "coreduo", ++ "fallback": "allow", ++ "vendor_id": "Genuine20201", ++ } + } +- + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, +- virt.update("my_vm", boot=boot_uefi), ++ virt.update("my_vm", cpu=cpu_model), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( +- setxml.find("os").find("loader").text, "/usr/share/OVMF/OVMF_CODE.fd" ++ setxml.find("cpu").find("model").attrib.get("vendor_id"), "Genuine20201" + ) +- self.assertEqual(setxml.find("os").find("loader").attrib.get("readonly"), "yes") +- self.assertEqual(setxml.find("os").find("loader").attrib["type"], "pflash") + self.assertEqual( +- setxml.find("os").find("nvram").attrib["template"], +- "/usr/share/OVMF/OVMF_VARS.ms.fd", ++ setxml.find("cpu").find("model").attrib.get("fallback"), "allow" + ) ++ self.assertEqual(setxml.find("cpu").find("model").text, "coreduo") + ++ # test adding cpu vendor ++ cpu_vendor = {"vendor": "Intel"} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, +- virt.update("my_vm", boot={"efi": True}), ++ virt.update("my_vm", cpu=cpu_vendor), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) +- self.assertEqual(setxml.find("os").attrib.get("firmware"), "efi") +- +- invalid_boot = { +- "loader": "/usr/share/OVMF/OVMF_CODE.fd", +- "initrd": "/root/f8-i386-initrd", +- } +- +- with self.assertRaises(SaltInvocationError): +- virt.update("my_vm", boot=invalid_boot) +- +- with self.assertRaises(SaltInvocationError): +- virt.update("my_vm", boot={"efi": "Not a boolean value"}) +- +- # Update memtune parameter case +- memtune = { +- "soft_limit": "0.5g", +- "hard_limit": "1024", +- "swap_hard_limit": "2048m", +- "min_guarantee": "1 g", +- } ++ self.assertEqual(setxml.find("cpu").find("vendor").text, "Intel") + ++ # test adding cpu topology ++ cpu_topology = {"topology": {"sockets": 1, "cores": 12, "threads": 1}} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, +- virt.update("my_vm", mem=memtune), ++ virt.update("my_vm", cpu=cpu_topology), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("cpu").find("topology").attrib.get("sockets"), "1") ++ self.assertEqual(setxml.find("cpu").find("topology").attrib.get("cores"), "12") ++ self.assertEqual(setxml.find("cpu").find("topology").attrib.get("threads"), "1") ++ ++ # test adding cache ++ cpu_cache = {"cache": {"mode": "emulate", "level": 3}} ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", cpu=cpu_cache), + ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("cpu").find("cache").attrib.get("level"), "3") ++ self.assertEqual(setxml.find("cpu").find("cache").attrib.get("mode"), "emulate") + ++ # test adding feature ++ cpu_feature = {"features": {"lahf": "optional", "pcid": "disable"}} ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", cpu=cpu_feature), ++ ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( +- setxml.find("memtune").find("soft_limit").text, str(int(0.5 * 1024 ** 3)) ++ setxml.find("./cpu/feature[@name='pcid']").attrib.get("policy"), "disable" ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/feature[@name='lahf']").attrib.get("policy"), "optional" + ) +- self.assertEqual(setxml.find("memtune").find("soft_limit").get("unit"), "bytes") ++ ++ # test adding numa cell ++ numa_cell = { ++ "numa": { ++ "0": { ++ "cpus": "0-3", ++ "memory": "1g", ++ "discard": True, ++ "distances": {0: 10, 1: 21, 2: 31, 3: 41}, ++ }, ++ "1": { ++ "cpus": "4-6", ++ "memory": "0.5g", ++ "discard": False, ++ "memAccess": "shared", ++ "distances": {0: 21, 1: 10, 2: 15, 3: 30}, ++ }, ++ } ++ } + self.assertEqual( +- setxml.find("memtune").find("hard_limit").text, str(1024 * 1024 ** 2) ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", cpu=numa_cell), + ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( +- setxml.find("memtune").find("swap_hard_limit").text, str(2048 * 1024 ** 2) ++ setxml.find("./cpu/numa/cell/[@id='0']").attrib["cpus"], "0,1,2,3" + ) + self.assertEqual( +- setxml.find("memtune").find("min_guarantee").text, str(1 * 1024 ** 3) ++ setxml.find("./cpu/numa/cell/[@id='0']").attrib["memory"], str(1024 ** 3) ++ ) ++ self.assertEqual(setxml.find("./cpu/numa/cell/[@id='0']").get("unit"), "bytes") ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='0']").attrib["discard"], "yes" ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='0']").attrib[ ++ "value" ++ ], ++ "10", ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='1']").attrib[ ++ "value" ++ ], ++ "21", ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='2']").attrib[ ++ "value" ++ ], ++ "31", ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='3']").attrib[ ++ "value" ++ ], ++ "41", ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']").attrib["cpus"], "4,5,6" ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']").attrib["memory"], ++ str(int(1024 ** 3 / 2)), ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']").get("unit"), "bytes", ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']").attrib["discard"], "no" ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']").attrib["memAccess"], "shared" ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='0']").attrib[ ++ "value" ++ ], ++ "21", ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='1']").attrib[ ++ "value" ++ ], ++ "10", ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='2']").attrib[ ++ "value" ++ ], ++ "15", ++ ) ++ self.assertEqual( ++ setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='3']").attrib[ ++ "value" ++ ], ++ "30", ++ ) ++ ++ # Update boot parameter case ++ boot = { ++ "kernel": "/root/f8-i386-vmlinuz", ++ "initrd": "/root/f8-i386-initrd", ++ "cmdline": "console=ttyS0 ks=http://example.com/f8-i386/os/", ++ } ++ ++ # Update boot devices case ++ define_mock.reset_mock() ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", boot_dev="cdrom network hd"), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual( ++ ["cdrom", "network", "hd"], ++ [node.get("dev") for node in setxml.findall("os/boot")], ++ ) ++ ++ # Update unchanged boot devices case ++ define_mock.reset_mock() ++ self.assertEqual( ++ { ++ "definition": False, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", boot_dev="hd"), ++ ) ++ define_mock.assert_not_called() ++ ++ # Update with boot parameter case ++ define_mock.reset_mock() ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", boot=boot), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("os").find("kernel").text, "/root/f8-i386-vmlinuz") ++ self.assertEqual(setxml.find("os").find("initrd").text, "/root/f8-i386-initrd") ++ self.assertEqual( ++ setxml.find("os").find("cmdline").text, ++ "console=ttyS0 ks=http://example.com/f8-i386/os/", ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("os").find("kernel").text, "/root/f8-i386-vmlinuz") ++ self.assertEqual(setxml.find("os").find("initrd").text, "/root/f8-i386-initrd") ++ self.assertEqual( ++ setxml.find("os").find("cmdline").text, ++ "console=ttyS0 ks=http://example.com/f8-i386/os/", ++ ) ++ ++ boot_uefi = { ++ "loader": "/usr/share/OVMF/OVMF_CODE.fd", ++ "nvram": "/usr/share/OVMF/OVMF_VARS.ms.fd", ++ } ++ ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", boot=boot_uefi), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual( ++ setxml.find("os").find("loader").text, "/usr/share/OVMF/OVMF_CODE.fd" ++ ) ++ self.assertEqual(setxml.find("os").find("loader").attrib.get("readonly"), "yes") ++ self.assertEqual(setxml.find("os").find("loader").attrib["type"], "pflash") ++ self.assertEqual( ++ setxml.find("os").find("nvram").attrib["template"], ++ "/usr/share/OVMF/OVMF_VARS.ms.fd", ++ ) ++ ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", boot={"efi": True}), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("os").attrib.get("firmware"), "efi") ++ ++ invalid_boot = { ++ "loader": "/usr/share/OVMF/OVMF_CODE.fd", ++ "initrd": "/root/f8-i386-initrd", ++ } ++ ++ with self.assertRaises(SaltInvocationError): ++ virt.update("my_vm", boot=invalid_boot) ++ ++ with self.assertRaises(SaltInvocationError): ++ virt.update("my_vm", boot={"efi": "Not a boolean value"}) ++ ++ # Update memtune parameter case ++ memtune = { ++ "soft_limit": "0.5g", ++ "hard_limit": "1024", ++ "swap_hard_limit": "2048m", ++ "min_guarantee": "1 g", ++ } ++ ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", mem=memtune), ++ ) ++ ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqualUnit( ++ setxml.find("memtune").find("soft_limit"), int(0.5 * 1024 ** 3), "bytes" ++ ) ++ self.assertEqualUnit( ++ setxml.find("memtune").find("hard_limit"), 1024 * 1024 ** 2, "bytes" ++ ) ++ self.assertEqualUnit( ++ setxml.find("memtune").find("swap_hard_limit"), 2048 * 1024 ** 2, "bytes" ++ ) ++ self.assertEqualUnit( ++ setxml.find("memtune").find("min_guarantee"), 1 * 1024 ** 3, "bytes" + ) + + invalid_unit = {"soft_limit": "2HB"} +@@ -2064,6 +2829,50 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + with self.assertRaises(SaltInvocationError): + virt.update("my_vm", mem=invalid_number) + ++ # Update numatune case ++ numatune = { ++ "memory": {"mode": "strict", "nodeset": 1}, ++ "memnodes": { ++ 0: {"mode": "strict", "nodeset": 1}, ++ 1: {"mode": "preferred", "nodeset": 2}, ++ }, ++ } ++ ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", numatune=numatune), ++ ) ++ ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual( ++ setxml.find("numatune").find("memory").attrib.get("mode"), "strict" ++ ) ++ ++ self.assertEqual( ++ setxml.find("numatune").find("memory").attrib.get("nodeset"), "1" ++ ) ++ ++ self.assertEqual( ++ setxml.find("./numatune/memnode/[@cellid='0']").attrib.get("mode"), "strict" ++ ) ++ ++ self.assertEqual( ++ setxml.find("./numatune/memnode/[@cellid='0']").attrib.get("nodeset"), "1" ++ ) ++ ++ self.assertEqual( ++ setxml.find("./numatune/memnode/[@cellid='1']").attrib.get("mode"), ++ "preferred", ++ ) ++ ++ self.assertEqual( ++ setxml.find("./numatune/memnode/[@cellid='1']").attrib.get("nodeset"), "2" ++ ) ++ + # Update memory case + setmem_mock = MagicMock(return_value=0) + domain_mock.setMemoryFlags = setmem_mock +@@ -2115,37 +2924,250 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + self.assertEqual(setxml.find("maxMemory").text, str(3096 * 1024 ** 2)) + self.assertEqual(setxml.find("maxMemory").attrib.get("slots"), "10") + +- # Update disks case +- devattach_mock = MagicMock(return_value=0) +- devdetach_mock = MagicMock(return_value=0) +- domain_mock.attachDevice = devattach_mock +- domain_mock.detachDevice = devdetach_mock +- mock_chmod = MagicMock() +- mock_run = MagicMock() +- with patch.dict( +- os.__dict__, {"chmod": mock_chmod, "makedirs": MagicMock()} +- ): # pylint: disable=no-member +- with patch.dict( +- virt.__salt__, {"cmd.run": mock_run} +- ): # pylint: disable=no-member +- ret = virt.update( +- "my_vm", +- disk_profile="default", +- disks=[ +- { +- "name": "cddrive", +- "device": "cdrom", +- "source_file": None, +- "model": "ide", +- }, +- {"name": "added", "size": 2048}, +- ], +- ) +- added_disk_path = os.path.join( +- virt.__salt__["config.get"]("virt:images"), "my_vm_added.qcow2" +- ) # pylint: disable=no-member +- self.assertEqual( +- mock_run.call_args[0][0], ++ # update memory backing case ++ mem_back = { ++ "hugepages": [ ++ {"nodeset": "1-5,^4", "size": "1g"}, ++ {"nodeset": "4", "size": "2g"}, ++ ], ++ "nosharepages": True, ++ "locked": True, ++ "source": "file", ++ "access": "shared", ++ "allocation": "immediate", ++ "discard": True, ++ } ++ ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", mem=mem_back), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertDictEqual( ++ { ++ p.get("nodeset"): {"size": p.get("size"), "unit": p.get("unit")} ++ for p in setxml.findall("memoryBacking/hugepages/page") ++ }, ++ { ++ "1,2,3,5": {"size": str(1024 ** 3), "unit": "bytes"}, ++ "4": {"size": str(2 * 1024 ** 3), "unit": "bytes"}, ++ }, ++ ) ++ self.assertNotEqual(setxml.find("./memoryBacking/nosharepages"), None) ++ self.assertIsNone(setxml.find("./memoryBacking/nosharepages").text) ++ self.assertEqual([], setxml.find("./memoryBacking/nosharepages").keys()) ++ self.assertNotEqual(setxml.find("./memoryBacking/locked"), None) ++ self.assertIsNone(setxml.find("./memoryBacking/locked").text) ++ self.assertEqual([], setxml.find("./memoryBacking/locked").keys()) ++ self.assertEqual(setxml.find("./memoryBacking/source").attrib["type"], "file") ++ self.assertEqual(setxml.find("./memoryBacking/access").attrib["mode"], "shared") ++ self.assertNotEqual(setxml.find("./memoryBacking/discard"), None) ++ ++ # test adding iothreads ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", cpu={"iothreads": 5}), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("iothreads").text, "5") ++ ++ # test adding cpu tune parameters ++ cputune = { ++ "shares": 2048, ++ "period": 122000, ++ "quota": -1, ++ "global_period": 1000000, ++ "global_quota": -3, ++ "emulator_period": 1200000, ++ "emulator_quota": -10, ++ "iothread_period": 133000, ++ "iothread_quota": -1, ++ "vcpupin": {0: "1-4,^2", 1: "0,1", 2: "2,3", 3: "0,4"}, ++ "emulatorpin": "1-3", ++ "iothreadpin": {1: "5-6", 2: "7-8"}, ++ "vcpusched": [ ++ {"scheduler": "fifo", "priority": 1, "vcpus": "0"}, ++ {"scheduler": "fifo", "priotity": 2, "vcpus": "1"}, ++ {"scheduler": "idle", "priotity": 3, "vcpus": "2"}, ++ ], ++ "iothreadsched": [{"scheduler": "batch", "iothreads": "7"}], ++ "cachetune": { ++ "0-3": { ++ 0: {"level": 3, "type": "both", "size": 3}, ++ 1: {"level": 3, "type": "both", "size": 3}, ++ "monitor": {1: 3, "0-3": 3}, ++ }, ++ "4-5": {"monitor": {4: 3, 5: 2}}, ++ }, ++ "memorytune": {"0-2": {0: 60}, "3-4": {0: 50, 1: 70}}, ++ } ++ self.assertEqual( ++ { ++ "definition": True, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", cpu={"tuning": cputune}), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("cputune").find("shares").text, "2048") ++ self.assertEqual(setxml.find("cputune").find("period").text, "122000") ++ self.assertEqual(setxml.find("cputune").find("quota").text, "-1") ++ self.assertEqual(setxml.find("cputune").find("global_period").text, "1000000") ++ self.assertEqual(setxml.find("cputune").find("global_quota").text, "-3") ++ self.assertEqual(setxml.find("cputune").find("emulator_period").text, "1200000") ++ self.assertEqual(setxml.find("cputune").find("emulator_quota").text, "-10") ++ self.assertEqual(setxml.find("cputune").find("iothread_period").text, "133000") ++ self.assertEqual(setxml.find("cputune").find("iothread_quota").text, "-1") ++ self.assertEqual( ++ setxml.find("cputune").find("vcpupin[@vcpu='0']").attrib.get("cpuset"), ++ "1,3,4", ++ ) ++ self.assertEqual( ++ setxml.find("cputune").find("vcpupin[@vcpu='1']").attrib.get("cpuset"), ++ "0,1", ++ ) ++ self.assertEqual( ++ setxml.find("cputune").find("vcpupin[@vcpu='2']").attrib.get("cpuset"), ++ "2,3", ++ ) ++ self.assertEqual( ++ setxml.find("cputune").find("vcpupin[@vcpu='3']").attrib.get("cpuset"), ++ "0,4", ++ ) ++ self.assertEqual( ++ setxml.find("cputune").find("emulatorpin").attrib.get("cpuset"), "1,2,3" ++ ) ++ self.assertEqual( ++ setxml.find("cputune") ++ .find("iothreadpin[@iothread='1']") ++ .attrib.get("cpuset"), ++ "5,6", ++ ) ++ self.assertEqual( ++ setxml.find("cputune") ++ .find("iothreadpin[@iothread='2']") ++ .attrib.get("cpuset"), ++ "7,8", ++ ) ++ self.assertEqual( ++ setxml.find("cputune").find("vcpusched[@vcpus='0']").attrib.get("priority"), ++ "1", ++ ) ++ self.assertEqual( ++ setxml.find("cputune") ++ .find("vcpusched[@vcpus='0']") ++ .attrib.get("scheduler"), ++ "fifo", ++ ) ++ self.assertEqual( ++ setxml.find("cputune").find("iothreadsched").attrib.get("iothreads"), "7" ++ ) ++ self.assertEqual( ++ setxml.find("cputune").find("iothreadsched").attrib.get("scheduler"), ++ "batch", ++ ) ++ self.assertIsNotNone(setxml.find("./cputune/cachetune[@vcpus='0,1,2,3']")) ++ self.assertEqual( ++ setxml.find( ++ "./cputune/cachetune[@vcpus='0,1,2,3']/cache[@id='0']" ++ ).attrib.get("level"), ++ "3", ++ ) ++ self.assertEqual( ++ setxml.find( ++ "./cputune/cachetune[@vcpus='0,1,2,3']/cache[@id='0']" ++ ).attrib.get("type"), ++ "both", ++ ) ++ self.assertEqual( ++ setxml.find( ++ "./cputune/cachetune[@vcpus='0,1,2,3']/monitor[@vcpus='1']" ++ ).attrib.get("level"), ++ "3", ++ ) ++ self.assertNotEqual( ++ setxml.find("./cputune/cachetune[@vcpus='0,1,2,3']/monitor[@vcpus='1']"), ++ None, ++ ) ++ self.assertNotEqual( ++ setxml.find("./cputune/cachetune[@vcpus='4,5']").attrib.get("vcpus"), None ++ ) ++ self.assertEqual( ++ setxml.find("./cputune/cachetune[@vcpus='4,5']/cache[@id='0']"), None ++ ) ++ self.assertEqual( ++ setxml.find( ++ "./cputune/cachetune[@vcpus='4,5']/monitor[@vcpus='4']" ++ ).attrib.get("level"), ++ "3", ++ ) ++ self.assertEqual( ++ setxml.find( ++ "./cputune/cachetune[@vcpus='4,5']/monitor[@vcpus='5']" ++ ).attrib.get("level"), ++ "2", ++ ) ++ self.assertNotEqual(setxml.find("./cputune/memorytune[@vcpus='0,1,2']"), None) ++ self.assertEqual( ++ setxml.find( ++ "./cputune/memorytune[@vcpus='0,1,2']/node[@id='0']" ++ ).attrib.get("bandwidth"), ++ "60", ++ ) ++ self.assertNotEqual(setxml.find("./cputune/memorytune[@vcpus='3,4']"), None) ++ self.assertEqual( ++ setxml.find("./cputune/memorytune[@vcpus='3,4']/node[@id='0']").attrib.get( ++ "bandwidth" ++ ), ++ "50", ++ ) ++ self.assertEqual( ++ setxml.find("./cputune/memorytune[@vcpus='3,4']/node[@id='1']").attrib.get( ++ "bandwidth" ++ ), ++ "70", ++ ) ++ ++ # Update disks case ++ devattach_mock = MagicMock(return_value=0) ++ devdetach_mock = MagicMock(return_value=0) ++ domain_mock.attachDevice = devattach_mock ++ domain_mock.detachDevice = devdetach_mock ++ mock_chmod = MagicMock() ++ mock_run = MagicMock() ++ with patch.dict( ++ os.__dict__, {"chmod": mock_chmod, "makedirs": MagicMock()} ++ ): # pylint: disable=no-member ++ with patch.dict( ++ virt.__salt__, {"cmd.run": mock_run} ++ ): # pylint: disable=no-member ++ ret = virt.update( ++ "my_vm", ++ disk_profile="default", ++ disks=[ ++ { ++ "name": "cddrive", ++ "device": "cdrom", ++ "source_file": None, ++ "model": "ide", ++ }, ++ {"name": "added", "size": 2048, "iothreads": True}, ++ ], ++ ) ++ added_disk_path = os.path.join( ++ virt.__salt__["config.get"]("virt:images"), "my_vm_added.qcow2" ++ ) # pylint: disable=no-member ++ self.assertEqual( ++ mock_run.call_args[0][0], + 'qemu-img create -f qcow2 "{}" 2048M'.format(added_disk_path), + ) + self.assertEqual(mock_chmod.call_args[0][0], added_disk_path) +@@ -2170,6 +3192,11 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + self.assertEqual(devattach_mock.call_count, 2) + self.assertEqual(devdetach_mock.call_count, 2) + ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual( ++ "threads", setxml.find("devices/disk[3]/driver").get("io") ++ ) ++ + # Update nics case + yaml_config = """ + virt: +@@ -2244,6 +3271,19 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual("vnc", setxml.find("devices/graphics").get("type")) + ++ # Serial and console test case ++ self.assertEqual( ++ { ++ "definition": False, ++ "disk": {"attached": [], "detached": [], "updated": []}, ++ "interface": {"attached": [], "detached": []}, ++ }, ++ virt.update("my_vm", serials=[{"type": "tcp"}], consoles=[{"type": "tcp"}]), ++ ) ++ setxml = ET.fromstring(define_mock.call_args[0][0]) ++ self.assertEqual(setxml.find("devices/serial").attrib["type"], "pty") ++ self.assertEqual(setxml.find("devices/console").attrib["type"], "pty") ++ + # Update with no diff case + pool_mock = MagicMock() + default_pool_desc = "" +@@ -2644,48 +3684,6 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): + /usr/share/old/OVMF_CODE.fd + /usr/share/old/OVMF_VARS.ms.fd + +- +- +- +- +- +- +- +-
+- +- +- +- +- +- +- +-
+- +- +- +- +- +- +- +-
+- +- +- +- +- +- +- +-
+- +- +- +- +-