Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:tulip
salt
open-suse-3002.2-bigvm-310.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File open-suse-3002.2-bigvm-310.patch of Package salt
From 0d606b481752d1112321046ce78d3a7f9d2a6604 Mon Sep 17 00:00:00 2001 From: Cedric Bosdonnat <cbosdonnat@suse.com> 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 <guoqing_li@pm.me> Co-authored-by: gqlo <escita@pm.me> Co-authored-by: marina2209 <mm.antropova@gmail.com> --- 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 <https://libvirt.org/formatdomain.html#time-keeping>`_ 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 <https://libvirt.org/formatdomain.html#consoles-serial-parallel-channel-devices>`_. + .. 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 <https://libvirt.org/formatdomain.html#time-keeping>`_ 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<pool>[^/]+)/(?P<volume>.*)$", disks[disk]["file"],) + matcher = re.match("^(?P<pool>[^/]+)/(?P<volume>.*)$", 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 <https://libvirt.org/formatdomain.html#time-keeping>`_ 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 <https://libvirt.org/formatdomain.html#time-keeping>`_ 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" -%} + <source mode="bind" path="{{ dev.path }}"/> + {% elif dev.type in ["udp", "tcp"] -%} + <source mode="bind" host="{{ dev.get('host', '0.0.0.0') }}" service="{{ dev.port }}" + {% if dev.get('tls') is not none %}tls="{{'yes' if dev.tls else 'no'}}"{% endif %}/> + {% elif dev.type in ["pipe", "dev", "pty", "file"] and dev.path -%} + <source path="{{ dev.path }}"/> + {%- endif %} + {% if dev.type == "tcp" -%} + <protocol type="{{ dev.protocol }}"/> + {%- endif %} + {% if "target_port" in dev or "target_type" in dev -%} + <target port="{{ dev.get('target_port', 0) }}" {% if dev.target_type %}type="{{ dev.target_type }}"{% endif %}/> + {%- 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 -%} <domain type='{{ hypervisor }}'> <name>{{ name }}</name> - <vcpu>{{ cpu }}</vcpu> + {%- if cpu %} + <vcpu {{ opt_attribute(cpu, 'placement') }} {{ opt_attribute(cpu, 'cpuset') }} {{ opt_attribute(cpu, 'current') }}>{{ cpu.get('maximum', '') }}</vcpu> + {%- endif %} + {%- if cpu.get('vcpus') %} + <vcpus> + {%- for vcpu_id in cpu["vcpus"].keys() %} + <vcpu id='{{ vcpu_id }}' {{ opt_attribute(cpu.vcpus[vcpu_id], 'enabled', yesno) }} {{ opt_attribute(cpu.vcpus[vcpu_id], 'hotpluggable', yesno) }} {{ opt_attribute(cpu.vcpus[vcpu_id], 'order') }}/> + {%- endfor %} + </vcpus> + {%- endif %} + {%- if cpu %} + <cpu {{ opt_attribute(cpu, 'match') }} {{ opt_attribute(cpu, 'mode') }} {{ opt_attribute(cpu, 'check') }} > + {%- if cpu.model %} + <model {{ opt_attribute(cpu.model, 'fallback') }} {{ opt_attribute(cpu.model, 'vendor_id') }}>{{ cpu.model.get('name', '') }}</model> + {%- endif %} + {%- if cpu.vendor %} + <vendor>{{ cpu.get('vendor', '') }}</vendor> + {%- endif %} + {%- if cpu.topology %} + <topology {{ opt_attribute(cpu.topology, 'sockets') }} {{ opt_attribute(cpu.topology, 'dies') }} {{ opt_attribute(cpu.topology, 'cores') }} {{ opt_attribute(cpu.topology, 'threads') }}/> + {%- endif %} + {%- if cpu.cache %} + <cache {{ opt_attribute(cpu.cache, 'level') }} {{ opt_attribute(cpu.cache, 'mode') }}/> + {%- endif %} + {%- if cpu.features %} + {%- for k, v in cpu.features.items() %} + <feature policy='{{ v }}' name='{{ k }}'/> + {%- endfor %} + {%- endif %} + {%- if cpu.numa %} + <numa> + {%- for numa_id in cpu.numa.keys() %} + {%- if cpu.numa.get(numa_id) %} + <cell id='{{ numa_id }}' {{ opt_attribute(cpu.numa[numa_id], 'cpus') }} {{ opt_attribute(cpu.numa[numa_id], 'memory', to_kib) }} {{ opt_attribute(cpu.numa[numa_id], 'discard', yesno) }} {{ opt_attribute(cpu.numa[numa_id], 'memAccess') }}> + {%- if cpu.numa[numa_id].distances %} + <distances> + {%- for sibling_id in cpu.numa[numa_id].distances %} + <sibling id='{{ sibling_id }}' value='{{ cpu.numa[numa_id].distances[sibling_id] }}'/> + {%- endfor %} + </distances> + {%- endif %} + </cell> + {%- endif %} + {%- endfor %} + </numa> + {%- endif %} + </cpu> + {%- if cpu.iothreads %} + <iothreads>{{ cpu.iothreads }}</iothreads> + {%- endif %} + {%- endif %} + {%- if cpu.tuning %} + <cputune> + {%- if cpu.tuning.vcpupin %} + {%- for vcpu_id, cpuset in cpu.tuning.vcpupin.items() %} + <vcpupin vcpu='{{ vcpu_id }}' cpuset='{{ cpuset }}'/> + {%- endfor %} + {%- endif %} + {%- if cpu.tuning.emulatorpin %} + <emulatorpin cpuset="{{ cpu.tuning.emulatorpin }}"/> + {%- endif %} + {%- if cpu.tuning.iothreadpin %} + {%- for thread_id, cpuset in cpu.tuning.iothreadpin.items() %} + <iothreadpin iothread='{{ thread_id }}' cpuset='{{ cpuset }}'/> + {%- endfor %} + {%- endif %} + {%- if cpu.tuning.shares %} + <shares>{{ cpu.tuning.shares }}</shares> + {%- endif %} + {%- if cpu.tuning.period %} + <period>{{ cpu.tuning.period }}</period> + {%- endif %} + {%- if cpu.tuning.quota %} + <quota>{{ cpu.tuning.quota }}</quota> + {%- endif %} + {%- if cpu.tuning.global_period %} + <global_period>{{ cpu.tuning.global_period }}</global_period> + {%- endif %} + {%- if cpu.tuning.global_quota %} + <global_quota>{{ cpu.tuning.global_quota }}</global_quota> + {%- endif %} + {%- if cpu.tuning.emulator_period %} + <emulator_period>{{ cpu.tuning.emulator_period }}</emulator_period> + {%- endif %} + {%- if cpu.tuning.emulator_quota %} + <emulator_quota>{{ cpu.tuning.emulator_quota }}</emulator_quota> + {%- endif %} + {%- if cpu.tuning.iothread_period %} + <iothread_period>{{ cpu.tuning.iothread_period }}</iothread_period> + {%- endif %} + {%- if cpu.tuning.iothread_quota %} + <iothread_quota>{{ cpu.tuning.iothread_quota }}</iothread_quota> + {%- endif %} + {%- if cpu.tuning.vcpusched %} + {%- for sched in cpu.tuning.vcpusched %} + <vcpusched scheduler='{{ sched.scheduler }}' + {%- if sched.get("vcpus") %} vcpus='{{ sched.get("vcpus") }}'{% endif -%} + {%- if sched.get("priority") is not none %} priority='{{ sched.get("priority") }}'{% endif -%} + /> + {%- endfor %} + {%- endif %} + {%- if cpu.tuning.iothreadsched %} + {%- for sched in cpu.tuning.iothreadsched %} + <iothreadsched scheduler='{{ sched.scheduler }}' + {%- if sched.get("iothreads") %} iothreads='{{ sched.get("iothreads") }}'{% endif -%} + {%- if sched.get("priority") is not none %} priority='{{ sched.get("priority") }}'{% endif -%} + /> + {%- endfor %} + {%- endif %} + {%- if cpu.tuning.emulatorsched %} + <emulatorsched scheduler='{{ cpu.tuning.emulatorsched.scheduler }}' + {%- if cpu.tuning.emulatorsched.get("priority") is not none %} priority='{{ cpu.tuning.emulatorsched.get("priority") }}'{% endif -%} + /> + {%- endif %} + {%- if cpu.tuning.cachetune %} + {%- for k, v in cpu.tuning.cachetune.items() %} + <cachetune vcpus='{{ k }}'> + {%- for e, atrs in v.items() %} + {%- if e is number and atrs %} + <cache id='{{ e }}' {%- for atr, val in atrs.items() %} {{ atr }}='{{ val }}' {%- endfor %} /> + {%- elif e is not number %} + {%- for atr, val in atrs.items() %} + <monitor level='{{ val }}' vcpus='{{ atr }}'/> + {%- endfor %} + {%- endif %} + {%- endfor %} + </cachetune> + {%- endfor %} + {%- endif %} + {%- if cpu.tuning.memorytune %} + {%- for vcpus, nodes in cpu.tuning.memorytune.items() %} + <memorytune vcpus='{{ vcpus}}'> + {%- for id, bandwidth in nodes.items() %} + <node id='{{ id }}' bandwidth='{{ bandwidth }}'/> + {%- endfor %} + </memorytune> + {%- endfor %} + {%- endif %} + </cputune> + {%- endif %} {%- if mem.max %} - <maxMemory {{ mem.slots }} unit='KiB'> {{ mem.max }}</maxMemory> + <maxMemory {{ opt_attribute(mem, 'slots') }} unit='KiB'>{{ to_kib(mem.max) }}</maxMemory> {%- endif %} {%- if mem.boot %} - <memory unit='KiB'>{{ mem.boot }}</memory> + <memory unit='KiB'>{{ to_kib(mem.boot) }}</memory> {%- endif %} {%- if mem.current %} - <currentMemory unit='KiB'>{{ mem.current }}</currentMemory> + <currentMemory unit='KiB'>{{ to_kib(mem.current) }}</currentMemory> {%- endif %} {%- if mem %} <memtune> {%- if 'hard_limit' in mem and mem.hard_limit %} - <hard_limit unit="KiB">{{ mem.hard_limit }}</hard_limit> + <hard_limit unit="KiB">{{ to_kib(mem.hard_limit) }}</hard_limit> {%- endif %} {%- if 'soft_limit' in mem and mem.soft_limit %} - <soft_limit unit="KiB">{{ mem.soft_limit }}</soft_limit> + <soft_limit unit="KiB">{{ to_kib(mem.soft_limit) }}</soft_limit> {%- endif %} {%- if 'swap_hard_limit' in mem and mem.swap_hard_limit %} - <swap_hard_limit unit="KiB">{{ mem.swap_hard_limit }}</swap_hard_limit> + <swap_hard_limit unit="KiB">{{ to_kib(mem.swap_hard_limit) }}</swap_hard_limit> {%- endif %} {%- if 'min_guarantee' in mem and mem.min_guarantee %} - <min_guarantee unit="KiB">{{ mem.min_guarantee }}</min_guarantee> + <min_guarantee unit="KiB">{{ to_kib(mem.min_guarantee) }}</min_guarantee> {%- endif %} </memtune> {%- endif %} + {%- if numatune %} + <numatune> + {%- if 'memory' in numatune and numatune.memory %} + <memory mode='{{ numatune.memory.mode }}' + {%- if numatune.memory.nodeset %} nodeset='{{ numatune.memory.nodeset }}'{%- endif %} + /> + {%- endif %} + {%- if 'memnodes' in numatune and numatune.memnodes %} + {%- for cell_id in numatune['memnodes'] %} + <memnode cellid='{{ cell_id }}' mode='{{ numatune.memnodes[cell_id].mode }}' nodeset='{{ numatune.memnodes[cell_id].nodeset }}'/> + {%- endfor %} + {%- endif %} + </numatune> + {%- endif %} + {%- if mem %} + <memoryBacking> + {%- if mem.hugepages %} + <hugepages> + {%- for page in mem.hugepages %} + <page size="{{ to_kib(page.get("size")) }}" unit="KiB" + {%- if page.get("nodeset") or page.get("nodeset") == 0 %} nodeset='{{ page.get("nodeset") }}'{% endif -%} + /> + {%- endfor %} + </hugepages> + {%- if mem.nosharepages %} + <nosharepages/> + {%- endif %} + {%- if mem.locked %} + <locked/> + {%- endif %} + {%- if mem.source %} + <source type="{{ mem.source }}"/> + {%- endif %} + {%- if mem.access %} + <access mode="{{ mem.access }}"/> + {%- endif %} + {%- if mem.allocation %} + <allocation mode="{{ mem.allocation }}"/> + {%- endif %} + {%- if mem.discard %} + <discard/> + {%- endif %} + {%- endif %} + </memoryBacking> + {%- endif %} <os {{ boot.os_attrib }}> <type arch='{{ arch }}'>{{ os_type }}</type> {% if boot %} @@ -50,6 +238,18 @@ <boot dev='{{ dev }}' /> {% endfor %} </os> +{%- if clock %} + <clock offset="{{ clock.offset }}"{{ opt_attribute(clock, "adjustment") }}{{ opt_attribute(clock, "timezone") }}> + {%- for timer_name in clock.timers %} + {%- set timer = clock.timers[timer_name] %} + <timer name='{{ timer_name }}'{{ opt_attribute(timer, "track") }}{{ opt_attribute(timer, "tickpolicy") }}{{ opt_attribute(timer, "frequency") }}{{ opt_attribute(timer, "mode") }}{{ opt_attribute(timer, "present", yesno) }}> + {%- if "threshold" in timer or "slew" in timer or "limit" in timer %} + <catchup{{ opt_attribute(timer, "slew") }}{{ opt_attribute(timer, "threshold") }}{{ opt_attribute(timer, "limit") }}/> + {%- endif %} + </timer> + {%- endfor %} + </clock> +{%- endif %} <on_reboot>{{ on_reboot }}</on_reboot> <devices> {% for disk in disks %} @@ -69,7 +269,7 @@ <address type='drive' controller='0' bus='0' target='0' unit='{{ disk.index }}' /> {% endif %} {% if disk.driver -%} - <driver name='qemu' type='{{ disk.format}}' cache='none' io='native'/> + <driver name='qemu' type='{{ disk.format}}' cache='none' io='{{ disk.io }}'/> {% endif %} </disk> {% endfor %} @@ -104,35 +304,39 @@ address='{{ graphics.listen.address }}' {% endif %}/> </graphics> - {% endif %} - {% if serial_type == 'pty' %} - <serial type='pty'> - <target port='0'/> - </serial> - {% if console %} - <console type='pty'> - <target type='serial' port='0'/> - </console> - {% endif %} + + {% if graphics.type == "spice" -%} + <channel type='spicevmc'> + <target type='virtio' name='com.redhat.spice.0'/> + </channel> + {%- endif %} {% endif %} - {% if serial_type == 'tcp' %} - <serial type='tcp'> - <source mode='bind' host='' service='{{ telnet_port }}'/> - <protocol type='telnet'/> - <target port='0'/> + {%- for serial in serials %} + <serial type='{{ serial.type }}'> + {{ libvirt_chardevs.chardev(serial) }} </serial> - {% if console %} - <console type='tcp'> - <source mode='bind' host='' service='{{ telnet_port }}'/> - <protocol type='telnet'/> - <target type='serial' port='0'/> - </console> - {% endif %} - {% endif %} + {%- endfor %} + {%- for console in consoles %} + <console type='{{ console.type }}'> + {{ libvirt_chardevs.chardev(console) }} + </console> + {% endfor %} +{%- if hypervisor in ["qemu", "kvm"] %} + <channel type='unix'> + <target type='virtio' name='org.qemu.guest_agent.0'/> + </channel> +{%- endif %} </devices> <features> <acpi /> + <apic /> + <pae /> +{%- if hypervisor_features.get("kvm-hint-dedicated") %} + <kvm> + <hint-dedicated state="on"/> + </kvm> +{%- endif %} </features> </domain> 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 = """ +<capabilities> + <host> + <uuid>44454c4c-3400-105a-8033-b3c04f4b344a</uuid> + <cpu> + <arch>x86_64</arch> + <model>Nehalem</model> + <vendor>Intel</vendor> + <microcode version='25'/> + <topology sockets='1' cores='4' threads='2'/> + <feature name='vme'/> + <feature name='ds'/> + <feature name='acpi'/> + <pages unit='KiB' size='4'/> + <pages unit='KiB' size='2048'/> + </cpu> + <power_management> + <suspend_mem/> + <suspend_disk/> + <suspend_hybrid/> + </power_management> + <migration_features> + <live/> + <uri_transports> + <uri_transport>tcp</uri_transport> + <uri_transport>rdma</uri_transport> + </uri_transports> + </migration_features> + <topology> + <cells num='1'> + <cell id='0'> + <memory unit='KiB'>12367120</memory> + <pages unit='KiB' size='4'>3091780</pages> + <pages unit='KiB' size='2048'>0</pages> + <distances> + <sibling id='0' value='10'/> + </distances> + <cpus num='8'> + <cpu id='0' socket_id='0' core_id='0' siblings='0,4'/> + <cpu id='1' socket_id='0' core_id='1' siblings='1,5'/> + <cpu id='2' socket_id='0' core_id='2' siblings='2,6'/> + <cpu id='3' socket_id='0' core_id='3' siblings='3,7'/> + <cpu id='4' socket_id='0' core_id='0' siblings='0,4'/> + <cpu id='5' socket_id='0' core_id='1' siblings='1,5'/> + <cpu id='6' socket_id='0' core_id='2' siblings='2,6'/> + <cpu id='7' socket_id='0' core_id='3' siblings='3,7'/> + </cpus> + </cell> + </cells> + </topology> + <cache> + <bank id='0' level='3' type='both' size='8' unit='MiB' cpus='0-7'/> + </cache> + <secmodel> + <model>apparmor</model> + <doi>0</doi> + </secmodel> + <secmodel> + <model>dac</model> + <doi>0</doi> + <baselabel type='kvm'>+487:+486</baselabel> + <baselabel type='qemu'>+487:+486</baselabel> + </secmodel> + </host> + + <guest> + <os_type>hvm</os_type> + <arch name='i686'> + <wordsize>32</wordsize> + <emulator>/usr/bin/qemu-system-i386</emulator> + <machine maxCpus='255'>pc-i440fx-2.6</machine> + <machine canonical='pc-i440fx-2.6' maxCpus='255'>pc</machine> + <machine maxCpus='255'>pc-0.12</machine> + <domain type='qemu'/> + <domain type='kvm'> + <emulator>/usr/bin/qemu-kvm</emulator> + <machine maxCpus='255'>pc-i440fx-2.6</machine> + <machine canonical='pc-i440fx-2.6' maxCpus='255'>pc</machine> + <machine maxCpus='255'>pc-0.12</machine> + </domain> + </arch> + <features> + <cpuselection/> + <deviceboot/> + <disksnapshot default='on' toggle='no'/> + <acpi default='on' toggle='yes'/> + <apic default='on' toggle='no'/> + <pae/> + <nonpae/> + </features> + </guest> + + <guest> + <os_type>hvm</os_type> + <arch name='x86_64'> + <wordsize>64</wordsize> + <emulator>/usr/bin/qemu-system-x86_64</emulator> + <machine maxCpus='255'>pc-i440fx-2.6</machine> + <machine canonical='pc-i440fx-2.6' maxCpus='255'>pc</machine> + <machine maxCpus='255'>pc-0.12</machine> + <domain type='qemu'/> + <domain type='kvm'> + <emulator>/usr/bin/qemu-kvm</emulator> + <machine maxCpus='255'>pc-i440fx-2.6</machine> + <machine canonical='pc-i440fx-2.6' maxCpus='255'>pc</machine> + <machine maxCpus='255'>pc-0.12</machine> + </domain> + </arch> + <features> + <cpuselection/> + <deviceboot/> + <disksnapshot default='on' toggle='no'/> + <acpi default='on' toggle='yes'/> + <apic default='on' toggle='no'/> + </features> + </guest> + +</capabilities>""" + + 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 = """ + <domain type="kvm"> + <name>my_vm</name> + <memory unit='KiB'>3177680</memory> + <currentMemory unit='KiB'>3177680</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64'>hvm</type> + </os> + <on_reboot>restart</on_reboot> + </domain> + """ + 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 = """ + <domain type="kvm"> + <name>my_vm</name> + <memory unit='KiB'>524288</memory> + <currentMemory unit='KiB'>524288</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64'>linux</type> + <kernel>/usr/lib/grub2/x86_64-xen/grub.xen</kernel> + </os> + <features> + <kvm> + <hint-dedicated state="on"/> + </kvm> + </features> + <on_reboot>restart</on_reboot> + </domain> + """ + 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 = """ + <domain type="kvm"> + <name>my_vm</name> + <memory unit='KiB'>524288</memory> + <currentMemory unit='KiB'>524288</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64'>linux</type> + <kernel>/usr/lib/grub2/x86_64-xen/grub.xen</kernel> + </os> + </domain> + """ + 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 = """ + <domain type="kvm"> + <name>my_vm</name> + <memory unit='KiB'>524288</memory> + <currentMemory unit='KiB'>524288</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64'>linux</type> + <kernel>/usr/lib/grub2/x86_64-xen/grub.xen</kernel> + </os> + <clock offset="localtime" adjustment="-3600"> + <timer name="tsc" frequency="3504000000" mode="native" /> + <timer name="kvmclock" present="no" /> + </clock> + <on_reboot>restart</on_reboot> + </domain> + """ + 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 = """ + <domain type='kvm'> + <name>my_vm</name> + <memory unit='KiB'>524288</memory> + <currentMemory unit='KiB'>524288</currentMemory> + <vcpu placement='static'>1</vcpu> + <on_reboot>destroy</on_reboot> + <os> + <type arch='x86_64'>hvm</type> + </os> + </domain>""" + 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 = """ + <domain type='kvm'> + <name>my_vm</name> + <memory unit='KiB'>524288</memory> + <currentMemory unit='KiB'>524288</currentMemory> + <vcpu placement='static'>1</vcpu> + <os> + <type arch='x86_64'>hvm</type> + </os> + </domain>""" + 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(): <vcpus> <vcpu enabled="yes" id="1"/> </vcpus> + <memtune> + <hugepages> + <page size="128"/> + </hugepages> + </memtune> </domain> """ ) @@ -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): <alias name='video0'/> <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> </video> + <serial type='pty'/> + <console type='pty'/> </devices> </domain> """.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 = "<pool type='dir'></pool>" @@ -2644,48 +3684,6 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): <loader>/usr/share/old/OVMF_CODE.fd</loader> <nvram>/usr/share/old/OVMF_VARS.ms.fd</nvram> </os> - <devices> - <disk type='file' device='disk'> - <driver name='qemu' type='qcow2'/> - <source file='{0}{1}vm_with_boot_param_system.qcow2'/> - <backingStore/> - <target dev='vda' bus='virtio'/> - <alias name='virtio-disk0'/> - <address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x0'/> - </disk> - <disk type='file' device='disk'> - <driver name='qemu' type='qcow2'/> - <source file='{0}{1}vm_with_boot_param_data.qcow2'/> - <backingStore/> - <target dev='vdb' bus='virtio'/> - <alias name='virtio-disk1'/> - <address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x1'/> - </disk> - <interface type='network'> - <mac address='52:54:00:39:02:b1'/> - <source network='default' bridge='virbr0'/> - <target dev='vnet0'/> - <model type='virtio'/> - <alias name='net0'/> - <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> - </interface> - <interface type='network'> - <mac address='52:54:00:39:02:b2'/> - <source network='oldnet' bridge='virbr1'/> - <target dev='vnet1'/> - <model type='virtio'/> - <alias name='net1'/> - <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x1'/> - </interface> - <graphics type='spice' port='5900' autoport='yes' listen='127.0.0.1'> - <listen type='address' address='127.0.0.1'/> - </graphics> - <video> - <model type='qxl' ram='65536' vram='65536' vgamem='16384' heads='1' primary='yes'/> - <alias name='video0'/> - <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> - </video> - </devices> </domain> """ domain_mock_boot = self.set_mock_vm("vm_with_boot_param", xml_boot) @@ -2722,71 +3720,909 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): ) self.assertEqual( - { - "definition": True, - "disk": {"attached": [], "detached": [], "updated": []}, - "interface": {"attached": [], "detached": []}, - }, - virt.update("vm_with_boot_param", boot=uefi_boot_new), + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_boot_param", boot=uefi_boot_new), + ) + + setxml = ET.fromstring(define_mock_boot.call_args[0][0]) + self.assertEqual( + setxml.find("os").find("loader").text, "/usr/share/new/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/new/OVMF_VARS.ms.fd", + ) + + kernel_none = { + "kernel": None, + "initrd": None, + "cmdline": None, + } + + uefi_none = {"loader": None, "nvram": None} + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_boot_param", boot=kernel_none), + ) + + setxml = ET.fromstring(define_mock_boot.call_args[0][0]) + self.assertEqual(setxml.find("os").find("kernel"), None) + self.assertEqual(setxml.find("os").find("initrd"), None) + self.assertEqual(setxml.find("os").find("cmdline"), None) + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_boot_param", boot={"efi": False}), + ) + setxml = ET.fromstring(define_mock_boot.call_args[0][0]) + self.assertEqual(setxml.find("os").find("nvram"), None) + self.assertEqual(setxml.find("os").find("loader"), None) + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_boot_param", boot=uefi_none), + ) + + setxml = ET.fromstring(define_mock_boot.call_args[0][0]) + self.assertEqual(setxml.find("os").find("loader"), None) + self.assertEqual(setxml.find("os").find("nvram"), None) + + def test_update_existing_numatune_params(self): + """ + Test virt.update() with existing numatune parameters. + """ + xml_numatune = """ + <domain type='kvm' id='8'> + <name>vm_with_numatune_param</name> + <memory unit='KiB'>1048576</memory> + <currentMemory unit='KiB'>1048576</currentMemory> + <maxMemory slots="12" unit="bytes">1048576</maxMemory> + <vcpu placement='auto'>1</vcpu> + <numatune> + <memory mode="strict" nodeset="0-11"/> + <memnode cellid="1" mode="strict" nodeset="3"/> + <memnode cellid="3" mode="preferred" nodeset="7"/> + </numatune> + <os> + <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type> + </os> + <on_reboot>restart</on_reboot> + </domain> + """ + domain_mock = self.set_mock_vm("vm_with_numatune_param", xml_numatune) + domain_mock.OSType = MagicMock(return_value="hvm") + define_mock = MagicMock(return_value=True) + self.mock_conn.defineXML = define_mock + + # test update existing numatune node + numatune = { + "memory": {"mode": "preferred", "nodeset": "0-5"}, + "memnodes": { + 0: {"mode": "strict", "nodeset": "4"}, + 3: {"mode": "preferred", "nodeset": "7"}, + 4: {"mode": "strict", "nodeset": "6"}, + }, + } + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_numatune_param", numatune=numatune), + ) + + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("numatune").find("memory").attrib.get("mode"), "preferred" + ) + + self.assertEqual( + setxml.find("numatune").find("memory").attrib.get("nodeset"), + ",".join([str(i) for i in range(0, 6)]), + ) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='0']").attrib.get("mode"), "strict" + ) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='0']").attrib.get("nodeset"), "4" + ) + + self.assertEqual(setxml.find("./numatune/memnode/[@cellid='1']"), None) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='3']").attrib.get("mode"), + "preferred", + ) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='3']").attrib.get("nodeset"), "7" + ) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='4']").attrib.get("mode"), "strict" + ) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='4']").attrib.get("nodeset"), "6" + ) + + self.assertEqual(setxml.find("./numatune/memnode/[@cellid='2']"), None) + + numatune_mem_none = { + "memory": None, + "memnodes": { + 0: {"mode": "strict", "nodeset": "4"}, + 3: {"mode": "preferred", "nodeset": "7"}, + 4: {"mode": "strict", "nodeset": "6"}, + }, + } + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_numatune_param", numatune=numatune_mem_none), + ) + + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("numatune").find("memory"), None) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='0']").attrib.get("mode"), "strict" + ) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='0']").attrib.get("nodeset"), "4" + ) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='3']").attrib.get("mode"), + "preferred", + ) + + self.assertEqual( + setxml.find("./numatune/memnode/[@cellid='3']").attrib.get("nodeset"), "7" + ) + + self.assertEqual(setxml.find("./numatune/memnode/[@cellid='2']"), None) + + numatune_mnodes_none = { + "memory": {"mode": "preferred", "nodeset": "0-5"}, + "memnodes": None, + } + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_numatune_param", numatune=numatune_mnodes_none), + ) + + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("numatune").find("memory").attrib.get("mode"), "preferred" + ) + + self.assertEqual( + setxml.find("numatune").find("memory").attrib.get("nodeset"), + ",".join([str(i) for i in range(0, 6)]), + ) + + self.assertEqual(setxml.find("./numatune/memnode"), None) + + numatune_without_change = { + "memory": {"mode": "strict", "nodeset": "0-5,6,7-11"}, + "memnodes": { + 1: {"mode": "strict", "nodeset": "3"}, + 3: {"mode": "preferred", "nodeset": "7"}, + }, + } + + self.assertEqual( + { + "definition": False, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_numatune_param", numatune=numatune_without_change), + ) + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update( + "vm_with_numatune_param", numatune={"memory": None, "memnodes": None} + ), + ) + + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("numatune"), None) + + def test_update_existing_cpu_params(self): + """ + Test virt.update() with existing cpu-related parameters. + """ + xml_with_existing_params = """ + <domain type='kvm' id='8'> + <name>vm_with_boot_param</name> + <memory unit='KiB'>1048576</memory> + <currentMemory unit='KiB'>1048576</currentMemory> + <vcpu placement="static" cpuset="0-11" current="3">6</vcpu> + <vcpus> + <vcpu id="0" enabled="yes" hotpluggable="no" order="1"/> + <vcpu id="1" enabled="no" hotpluggable="yes"/> + <vcpu id="2" enabled="no" hotpluggable="yes"/> + <vcpu id="3" enabled="no" hotpluggable="yes"/> + <vcpu id="4" enabled="no" hotpluggable="yes"/> + <vcpu id="5" enabled="no" hotpluggable="yes"/> + <vcpu id="6" enabled="no" hotpluggable="yes"/> + <vcpu id="7" enabled="no" hotpluggable="yes"/> + <vcpu id="8" enabled="no" hotpluggable="yes"/> + <vcpu id="9" enabled="no" hotpluggable="yes"/> + <vcpu id="10" enabled="no" hotpluggable="yes"/> + <vcpu id="11" enabled="no" hotpluggable="yes"/> + </vcpus> + <cpu mode="custom" match="exact" check="full"> + <model fallback="allow" vendor_id="Genuine20201">core2duo</model> + <vendor>Intel</vendor> + <topology sockets="2" cores="5" threads="2"/> + <cache level="3" mode="emulate"/> + <feature policy="optional" name="lahf_lm"/> + <feature policy="require" name="pcid"/> + <numa> + <cell id="0" cpus="0-3" memory="1073741824" unit="KiB" discard="no"> + <distances> + <sibling id="0" value="10"/> + <sibling id="1" value="21"/> + <sibling id="2" value="31"/> + <sibling id="3" value="41"/> + </distances> + </cell> + <cell id="1" cpus="4-6" memory="1073741824" unit="KiB" memAccess="private"> + <distances> + <sibling id="0" value="21"/> + <sibling id="1" value="10"/> + <sibling id="2" value="21"/> + <sibling id="3" value="31"/> + </distances> + </cell> + </numa> + </cpu> + <os> + <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type> + </os> + </domain> + """ + domain_mock = self.set_mock_vm( + "vm_with_existing_param", xml_with_existing_params + ) + domain_mock.OSType = MagicMock(return_value="hvm") + define_mock = MagicMock(return_value=True) + self.mock_conn.defineXML = define_mock + + # test update vcpu with existing attributes case + setvcpus_mock = MagicMock(return_value=0) + domain_mock.setVcpusFlags = setvcpus_mock + + cpu_attr = {"placement": "static", "cpuset": "0-5", "current": 3, "maximum": 5} + self.assertEqual( + { + "definition": True, + "cpu": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_attr), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("vcpu").text, "5") + self.assertEqual(setxml.find("vcpu").attrib["placement"], "static") + self.assertEqual( + setxml.find("vcpu").attrib["cpuset"], + ",".join([str(i) for i in range(0, 6)]), + ) + self.assertEqual(setxml.find("vcpu").attrib["current"], "3") + + # test removing vcpu attribute + cpu_none = {"placement": "auto", "cpuset": None, "current": 2, "maximum": 5} + self.assertEqual( + { + "definition": True, + "cpu": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("vcpu").text, "5") + self.assertEqual(setxml.find("vcpu").attrib["placement"], "auto") + self.assertEqual(setxml.find("vcpu").attrib.get("cpuset"), None) + self.assertEqual(setxml.find("vcpu").attrib.get("current"), "2") + + # test update individual vcpu with exisiting attributes + vcpus = { + "vcpus": { + "0": {"enabled": False, "hotpluggable": True, "order": 5}, + "3": {"enabled": True, "hotpluggable": False, "order": 3}, + "7": {"enabled": True, "hotpluggable": False}, + } + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", 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"], "no") + self.assertEqual( + setxml.find("./vcpus/vcpu/[@id='0']").attrib["hotpluggable"], "yes" + ) + self.assertEqual(setxml.find("./vcpus/vcpu/[@id='0']").attrib["order"], "5") + self.assertEqual(setxml.find("./vcpus/vcpu/[@id='3']").attrib["id"], "3") + self.assertEqual(setxml.find("./vcpus/vcpu/[@id='3']").attrib["enabled"], "yes") + self.assertEqual( + setxml.find("./vcpus/vcpu/[@id='3']").attrib["hotpluggable"], "no" + ) + self.assertEqual(setxml.find("./vcpus/vcpu/[@id='3']").attrib["order"], "3") + self.assertEqual(setxml.find("./vcpus/vcpu/[@id='7']").attrib["id"], "7") + self.assertEqual(setxml.find("./vcpus/vcpu/[@id='7']").attrib["enabled"], "yes") + self.assertEqual( + setxml.find("./vcpus/vcpu/[@id='7']").attrib["hotpluggable"], "no" + ) + self.assertEqual( + setxml.find("./vcpus/vcpu/[@id='7']").attrib.get("order"), None + ) + + # test removing vcpu element + ind_vcpu = { + "vcpus": {"3": {"enabled": True, "hotpluggable": False, "order": None}} + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=ind_vcpu), + ) + + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("./vcpus/vcpu/[@id='0']"), None) + self.assertEqual(setxml.find("./vcpus/vcpu/[@id='3']").attrib["enabled"], "yes") + self.assertEqual( + setxml.find("./vcpus/vcpu/[@id='3']").attrib["hotpluggable"], "no" + ) + self.assertEqual( + setxml.find("./vcpus/vcpu/[@id='3']").attrib.get("order"), None + ) + + # test removing vcpus element + vcpus_none = {"vcpus": None} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=vcpus_none), + ) + + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("vcpus"), None) + + # test removing cpu attrbutes + cpu_atr_none = {"match": None, "mode": None, "check": None} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_atr_none), + ) + + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").attrib, {}) + + cpu_atr_mn = {"match": None} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_atr_mn), + ) + + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").attrib.get("match"), None) + self.assertEqual(setxml.find("cpu").attrib.get("mode"), "custom") + self.assertEqual(setxml.find("cpu").attrib.get("check"), "full") + + # test update existing cpu model + cpu_model_none = {"model": None} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_model_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").find("model"), None) + + cpu_model_atr_none = { + "model": {"name": "coresolo", "fallback": "forbid", "vendor_id": None} + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_model_atr_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").find("model").attrib.get("vendor_id"), None) + self.assertEqual( + setxml.find("cpu").find("model").attrib.get("fallback"), "forbid" + ) + self.assertEqual(setxml.find("cpu").find("model").text, "coresolo") + + cpu_model_atr = { + "model": { + "name": "coresolo", + "fallback": "forbid", + "vendor_id": "AuthenticAMD", + } + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_model_atr), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("cpu").find("model").attrib.get("fallback"), "forbid" + ) + self.assertEqual( + setxml.find("cpu").find("model").attrib.get("vendor_id"), "AuthenticAMD" + ) + self.assertEqual(setxml.find("cpu").find("model").text, "coresolo") + + # test update existing cpu vendor + cpu_vendor = {"vendor": "AMD"} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_vendor), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").find("vendor").text, "AMD") + + cpu_vendor_none = {"vendor": None} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_vendor_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").find("vendor"), None) + + # test update exisiting cpu topology + cpu_topology = {"topology": {"sockets": 1, "cores": 12, "threads": 1}} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", 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") + + cpu_topology_atr_none = { + "topology": {"sockets": None, "cores": 12, "threads": 1} + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_topology_atr_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("cpu").find("topology").attrib.get("sockets"), None + ) + self.assertEqual(setxml.find("cpu").find("topology").attrib.get("cores"), "12") + self.assertEqual(setxml.find("cpu").find("topology").attrib.get("threads"), "1") + + cpu_topology_none = {"topology": None} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_topology_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").find("topology"), None) + + # test update existing cache + cpu_cache = {"cache": {"mode": "passthrough", "level": 2}} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_cache), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").find("cache").attrib.get("level"), "2") + self.assertEqual( + setxml.find("cpu").find("cache").attrib.get("mode"), "passthrough" + ) + + cpu_cache_atr_none = {"cache": {"mode": "passthrough", "level": None}} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_cache_atr_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").find("cache").attrib.get("level"), None) + self.assertEqual( + setxml.find("cpu").find("cache").attrib.get("mode"), "passthrough" + ) + + cpu_cache_none = {"cache": None} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_cache_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cpu").find("cache"), None) + + # test update existing feature + cpu_feature = {"features": {"lahf_lm": "require", "pcid": "optional"}} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_feature), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("./cpu/feature[@name='pcid']").attrib.get("policy"), "optional" + ) + self.assertEqual( + setxml.find("./cpu/feature[@name='lahf_lm']").attrib.get("policy"), + "require", + ) + + cpu_feature_atr_none = {"features": {"pcid": "optional", "lahf_lm": "disable"}} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_feature_atr_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("./cpu/feature[@name='lahf_lm']").attrib.get("policy"), + "disable", + ) + self.assertEqual( + setxml.find("./cpu/feature[@name='pcid']").attrib.get("policy"), "optional" + ) + + cpu_feature_none = {"features": {"lahf_lm": None, "pcid": None}} + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=cpu_feature_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("./cpu/feature"), None) + + # test update existing numa cell + numa_cell = { + "numa": { + 0: { + "cpus": "0-6", + "memory": "512m", + "discard": True, + "distances": {0: 15, 1: 16, 2: 17, 3: 18}, + }, + 1: { + "cpus": "7-12", + "memory": "2g", + "discard": True, + "memAccess": "shared", + "distances": {0: 23, 1: 24, 2: 25, 3: 26}, + }, + } + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=numa_cell), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']").attrib["cpus"], + ",".join([str(i) for i in range(0, 7)]), + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']").attrib["memory"], + str(512 * 1024 ** 2), + ) + 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" + ], + "15", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='1']").attrib[ + "value" + ], + "16", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='2']").attrib[ + "value" + ], + "17", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='3']").attrib[ + "value" + ], + "18", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']").attrib["cpus"], + ",".join([str(i) for i in range(7, 13)]), + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']").attrib["memory"], + str(int(2 * 1024 ** 3)), + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']").get("unit"), "bytes", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']").attrib["discard"], "yes" + ) + 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" + ], + "23", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='1']").attrib[ + "value" + ], + "24", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='2']").attrib[ + "value" + ], + "25", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='3']").attrib[ + "value" + ], + "26", + ) + + numa_cell_atr_none = { + "numa": { + "0": { + "cpus": "0-6", + "memory": "512m", + "discard": False, + "distances": {0: 15, 2: 17, 3: 18}, + }, + "1": { + "cpus": "7-12", + "memory": "2g", + "discard": True, + "distances": {0: 23, 1: 24, 2: 25}, + }, + } + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_existing_param", cpu=numa_cell_atr_none), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']").attrib["cpus"], + ",".join([str(i) for i in range(0, 7)]), + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']").attrib["memory"], + str(512 * 1024 ** 2), + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']").get("unit"), "bytes", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']").attrib.get("discard"), "no" + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='0']").attrib[ + "value" + ], + "15", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='1']"), None + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='2']").attrib[ + "value" + ], + "17", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='0']/distances/sibling/[@id='3']").attrib[ + "value" + ], + "18", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']").attrib["cpus"], + ",".join([str(i) for i in range(7, 13)]), + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']").attrib["memory"], + str(int(2 * 1024 ** 3)), + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']").attrib["discard"], "yes" + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='0']").attrib[ + "value" + ], + "23", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='1']").attrib[ + "value" + ], + "24", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='2']").attrib[ + "value" + ], + "25", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='3']"), None + ) + + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']").attrib["cpus"], + ",".join([str(i) for i in range(7, 13)]), ) - - setxml = ET.fromstring(define_mock_boot.call_args[0][0]) self.assertEqual( - setxml.find("os").find("loader").text, "/usr/share/new/OVMF_CODE.fd" + setxml.find("./cpu/numa/cell/[@id='1']").attrib["memory"], + str(int(1024 ** 3 * 2)), ) - 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/new/OVMF_VARS.ms.fd", + setxml.find("./cpu/numa/cell/[@id='1']").attrib["discard"], "yes" ) - - kernel_none = { - "kernel": None, - "initrd": None, - "cmdline": None, - } - - uefi_none = {"loader": None, "nvram": None} - self.assertEqual( - { - "definition": True, - "disk": {"attached": [], "detached": [], "updated": []}, - "interface": {"attached": [], "detached": []}, - }, - virt.update("vm_with_boot_param", boot=kernel_none), + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='0']").attrib[ + "value" + ], + "23", ) - - setxml = ET.fromstring(define_mock_boot.call_args[0][0]) - self.assertEqual(setxml.find("os").find("kernel"), None) - self.assertEqual(setxml.find("os").find("initrd"), None) - self.assertEqual(setxml.find("os").find("cmdline"), None) - self.assertEqual( - { - "definition": True, - "disk": {"attached": [], "detached": [], "updated": []}, - "interface": {"attached": [], "detached": []}, - }, - virt.update("vm_with_boot_param", boot={"efi": False}), + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='1']").attrib[ + "value" + ], + "24", ) - setxml = ET.fromstring(define_mock_boot.call_args[0][0]) - self.assertEqual(setxml.find("os").find("nvram"), None) - self.assertEqual(setxml.find("os").find("loader"), None) - self.assertEqual( - { - "definition": True, - "disk": {"attached": [], "detached": [], "updated": []}, - "interface": {"attached": [], "detached": []}, - }, - virt.update("vm_with_boot_param", boot=uefi_none), + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='2']").attrib[ + "value" + ], + "25", + ) + self.assertEqual( + setxml.find("./cpu/numa/cell/[@id='1']/distances/sibling/[@id='3']"), None, ) - - setxml = ET.fromstring(define_mock_boot.call_args[0][0]) - self.assertEqual(setxml.find("os").find("loader"), None) - self.assertEqual(setxml.find("os").find("nvram"), None) def test_update_memtune_params(self): """ @@ -2965,6 +4801,517 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(setxml.find("currentMemory").text, str(int(1 * 1024 ** 2))) self.assertEqual(setxml.find("memory").text, str(int(1 * 1024 ** 2))) + def test_update_exist_memorybacking_params(self): + """ + Test virt.update() with memory backing parameters. + """ + xml_with_memback_params = """ + <domain type='kvm' id='8'> + <name>vm_with_memback_param</name> + <memory unit='KiB'>1048576</memory> + <currentMemory unit='KiB'>1048576</currentMemory> + <maxMemory slots="12" unit="KiB">1048576</maxMemory> + <vcpu placement='auto'>1</vcpu> + <memoryBacking> + <hugepages> + <page size="2048" unit="KiB"/> + <page size="3145728" nodeset="1-4,^3" unit="KiB"/> + <page size="1048576" nodeset="3" unit="KiB"/> + </hugepages> + <nosharepages/> + <locked/> + <source type="file"/> + <access mode="shared"/> + <discard/> + </memoryBacking> + <os> + <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type> + </os> + <on_reboot>restart</on_reboot> + </domain> + """ + domain_mock = self.set_mock_vm("vm_with_memback_param", xml_with_memback_params) + domain_mock.OSType = MagicMock(return_value="hvm") + define_mock = MagicMock(return_value=True) + self.mock_conn.defineXML = define_mock + + # update memory backing case + mem_back_param = { + "hugepages": [ + {"nodeset": "1-4,^3", "size": "1g"}, + {"nodeset": "3", "size": "2g"}, + ], + "nosharepages": None, + "locked": None, + "source": "anonymous", + "access": "private", + "allocation": "ondemand", + "discard": None, + } + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_memback_param", mem=mem_back_param), + ) + 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,4": {"size": str(1024 ** 3), "unit": "bytes"}, + "3": {"size": str(2 * 1024 ** 3), "unit": "bytes"}, + }, + ) + self.assertEqual(setxml.find("./memoryBacking/nosharepages"), None) + self.assertEqual(setxml.find("./memoryBacking/locked"), None) + self.assertEqual( + setxml.find("./memoryBacking/source").attrib["type"], "anonymous" + ) + self.assertEqual( + setxml.find("./memoryBacking/access").attrib["mode"], "private" + ) + self.assertEqual( + setxml.find("./memoryBacking/allocation").attrib["mode"], "ondemand" + ) + self.assertEqual(setxml.find("./memoryBacking/discard"), None) + + unchanged_page = { + "hugepages": [ + {"size": "2m"}, + {"nodeset": "1-4,^3", "size": "3g"}, + {"nodeset": "3", "size": "1g"}, + ], + } + + self.assertEqual( + { + "definition": False, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("vm_with_memback_param", mem=unchanged_page), + ) + + def test_update_iothreads_params(self): + """ + Test virt.update() with iothreads parameters. + """ + xml_with_iothreads_params = """ + <domain type='kvm' id='8'> + <name>xml_with_iothreads_params</name> + <memory unit='KiB'>1048576</memory> + <currentMemory unit='KiB'>1048576</currentMemory> + <maxMemory slots="12" unit="KiB">1048576</maxMemory> + <vcpu placement='auto'>1</vcpu> + <iothreads>6</iothreads> + <os> + <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type> + </os> + </domain> + """ + domain_mock = self.set_mock_vm( + "xml_with_iothreads_params", xml_with_iothreads_params + ) + domain_mock.OSType = MagicMock(return_value="hvm") + define_mock = MagicMock(return_value=True) + self.mock_conn.defineXML = define_mock + + # test updating existing iothreads + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("xml_with_iothreads_params", cpu={"iothreads": 7}), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("iothreads").text, "7") + + def test_update_cputune_paramters(self): + """ + Test virt.update() with cputune parameters. + """ + xml_with_cputune_params = """ + <domain type='kvm' id='8'> + <name>xml_with_cputune_params</name> + <memory unit='KiB'>1048576</memory> + <currentMemory unit='KiB'>1048576</currentMemory> + <maxMemory slots="12" unit="KiB">1048576</maxMemory> + <vcpu placement='auto'>1</vcpu> + <iothreads>4</iothreads> + <cputune> + <shares>2048</shares> + <period>1000000</period> + <quota>-1</quota> + <global_period>1000000</global_period> + <global_quota>-1</global_quota> + <emulator_period>1000000</emulator_period> + <emulator_quota>-1</emulator_quota> + <iothread_period>1000000</iothread_period> + <iothread_quota>-1</iothread_quota> + <vcpupin vcpu="0" cpuset="0-2"/> + <vcpupin vcpu="1" cpuset="3"/> + <vcpupin vcpu="2" cpuset="4"/> + <vcpupin vcpu="3" cpuset="5-7"/> + <emulatorpin cpuset="1-2"/> + <iothreadpin iothread="1" cpuset="1-5"/> + <iothreadpin iothread="2" cpuset="6-7"/> + <vcpusched vcpus="0" scheduler="idle" priority="3"/> + <vcpusched vcpus="1" scheduler="rr" priority="1"/> + <vcpusched vcpus="2" scheduler="fifo" priority="2"/> + <iothreadsched iothreads="4" scheduler="fifo"/> + <emulatorsched scheduler="idle"/> + <cachetune vcpus="0-4"> + <cache id="0" level="2" type="both" size="4" unit="KiB"/> + <cache id="1" level="2" type="both" size="4" unit="KiB"/> + <monitor level="5" vcpus="0-2"/> + <monitor level="6" vcpus="1-3"/> + </cachetune> + <cachetune vcpus="5-8"> + <monitor level="5" vcpus="5-6"/> + <monitor level="3" vcpus="7-8"/> + </cachetune> + <memorytune vcpus="0-6"> + <node id="0" bandwidth="45"/> + </memorytune> + <memorytune vcpus="7-8"> + <node id="0" bandwidth="120"/> + </memorytune> + </cputune> + <os> + <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type> + </os> + </domain> + """ + domain_mock = self.set_mock_vm( + "xml_with_cputune_params", xml_with_cputune_params + ) + domain_mock.OSType = MagicMock(return_value="hvm") + define_mock = MagicMock(return_value=True) + self.mock_conn.defineXML = define_mock + + # test updating existing cputune parameters + cputune = { + "shares": 1024, + "period": 5000, + "quota": -20, + "global_period": 4000, + "global_quota": -30, + "emulator_period": 3000, + "emulator_quota": -4, + "iothread_period": 7000, + "iothread_quota": -5, + "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": "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}}, + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("xml_with_cputune_params", cpu={"tuning": cputune}), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cputune").find("shares").text, "1024") + self.assertEqual(setxml.find("cputune").find("period").text, "5000") + self.assertEqual(setxml.find("cputune").find("quota").text, "-20") + self.assertEqual(setxml.find("cputune").find("global_period").text, "4000") + self.assertEqual(setxml.find("cputune").find("global_quota").text, "-30") + self.assertEqual(setxml.find("cputune").find("emulator_period").text, "3000") + self.assertEqual(setxml.find("cputune").find("emulator_quota").text, "-4") + self.assertEqual(setxml.find("cputune").find("iothread_period").text, "7000") + self.assertEqual(setxml.find("cputune").find("iothread_quota").text, "-5") + 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.assertDictEqual( + { + s.get("vcpus"): { + "scheduler": s.get("scheduler"), + "priority": s.get("priority"), + } + for s in setxml.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 setxml.findall("cputune/iothreadsched") + }, + {"5,6,7": {"scheduler": "batch", "priority": "1"}}, + ) + self.assertEqual(setxml.find("cputune/emulatorsched").get("scheduler"), "rr") + self.assertEqual(setxml.find("cputune/emulatorsched").get("priority"), "2") + 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", + ) + + # test removing cputune attributes and sub elements + cputune = { + "shares": None, + "period": 20000, + "quota": None, + "global_period": 5000, + "global_quota": None, + "emulator_period": 2000, + "emulator_quota": -4, + "iothread_period": None, + "iothread_quota": -5, + "vcpupin": {0: "1-4,^2", 2: "2,4"}, + "emulatorpin": None, + "iothreadpin": {1: "5-6"}, + "vcpusched": [{"scheduler": "idle", "priority": 5, "vcpus": "1"}], + "iothreadsched": None, + "cachetune": { + "0-3": { + 0: {"level": 4, "type": "data", "size": 7}, + "monitor": {"1-2": 11}, + }, + }, + "memorytune": {"3-4": {0: 37, 1: 73}}, + } + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("xml_with_cputune_params", cpu={"tuning": cputune}), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cputune").find("shares"), None) + self.assertEqual(setxml.find("cputune").find("period").text, "20000") + self.assertEqual(setxml.find("cputune").find("quota"), None) + self.assertEqual(setxml.find("cputune").find("global_period").text, "5000") + self.assertEqual(setxml.find("cputune").find("global_quota"), None) + self.assertEqual(setxml.find("cputune").find("emulator_period").text, "2000") + self.assertEqual(setxml.find("cputune").find("emulator_quota").text, "-4") + self.assertEqual(setxml.find("cputune").find("iothread_period"), None) + self.assertEqual(setxml.find("cputune").find("iothread_quota").text, "-5") + self.assertEqual( + setxml.find("cputune").find("vcpupin[@vcpu='0']").attrib.get("cpuset"), + "1,3,4", + ) + self.assertEqual(setxml.find("cputune").find("vcpupin[@vcpu='1']"), None) + self.assertEqual( + setxml.find("cputune").find("vcpupin[@vcpu='2']").attrib.get("cpuset"), + "2,4", + ) + self.assertEqual(setxml.find("cputune").find("vcpupin[@vcpu='3']"), None) + self.assertEqual(setxml.find("cputune").find("emulatorpin"), None) + self.assertEqual( + setxml.find("cputune") + .find("iothreadpin[@iothread='1']") + .attrib.get("cpuset"), + "5,6", + ) + self.assertEqual( + setxml.find("cputune").find("iothreadpin[@iothread='2']"), None + ) + self.assertDictEqual( + { + s.get("vcpus"): { + "scheduler": s.get("scheduler"), + "priority": s.get("priority"), + } + for s in setxml.findall("cputune/vcpusched") + }, + {"1": {"scheduler": "idle", "priority": "5"}}, + ) + self.assertEqual(setxml.find("cputune").find("iothreadsched"), None) + 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("size"), + "7", + ) + self.assertEqual( + setxml.find( + "./cputune/cachetune[@vcpus='0,1,2,3']/cache[@id='0']" + ).attrib.get("level"), + "4", + ) + self.assertEqual( + setxml.find( + "./cputune/cachetune[@vcpus='0,1,2,3']/cache[@id='0']" + ).attrib.get("type"), + "data", + ) + self.assertEqual( + setxml.find( + "./cputune/cachetune[@vcpus='0,1,2,3']/monitor[@vcpus='1,2']" + ).attrib.get("level"), + "11", + ) + self.assertEqual( + setxml.find("./cputune/cachetune[@vcpus='0,1,2,3']/monitor[@vcpus='3,4']"), + None, + ) + self.assertEqual( + setxml.find("./cputune/cachetune[@vcpus='0,1,2,3']/cache[@id='1']"), None + ) + self.assertEqual(setxml.find("./cputune/cachetune[@vcpus='4,5']"), None) + self.assertEqual(setxml.find("./cputune/memorytune[@vcpus='0,1,2']"), None) + self.assertEqual( + setxml.find("./cputune/memorytune[@vcpus='3,4']/node[@id='0']").attrib.get( + "bandwidth" + ), + "37", + ) + self.assertEqual( + setxml.find("./cputune/memorytune[@vcpus='3,4']/node[@id='1']").attrib.get( + "bandwidth" + ), + "73", + ) + + cputune_subelement = { + "vcpupin": None, + "iothreadpin": None, + "vcpusched": None, + "iothreadsched": None, + "cachetune": None, + "memorytune": None, + } + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("xml_with_cputune_params", cpu={"tuning": cputune_subelement}), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("cputune").find("vcpupin"), None) + self.assertEqual(setxml.find("cputune").find("iothreadpin"), None) + self.assertEqual(setxml.find("cputune").find("vcpusched"), None) + self.assertEqual(setxml.find("cputune").find("iothreadsched"), None) + self.assertEqual(setxml.find("cputune").find("cachetune"), None) + self.assertEqual(setxml.find("cputune").find("memorytune"), None) + def test_handle_unit(self): """ Test regex function for handling units diff --git a/tests/unit/states/test_virt.py b/tests/unit/states/test_virt.py index 1923ae5c0f..dadc6dd08e 100644 --- a/tests/unit/states/test_virt.py +++ b/tests/unit/states/test_virt.py @@ -327,6 +327,14 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): "type": "spice", "listen": {"type": "address", "address": "192.168.0.1"}, } + serials = [ + {"type": "tcp", "port": 22223, "protocol": "telnet"}, + {"type": "pty"}, + ] + consoles = [ + {"type": "tcp", "port": 22223, "protocol": "telnet"}, + {"type": "pty"}, + ] self.assertDictEqual( virt.defined( "myvm", @@ -345,10 +353,14 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): install=False, pub_key="/path/to/key.pub", priv_key="/path/to/key", + hypervisor_features={"kvm-hint-dedicated": True}, + clock={"utc": True}, stop_on_reboot=True, connection="someconnection", username="libvirtuser", password="supersecret", + serials=serials, + consoles=consoles, ), ret, ) @@ -367,14 +379,19 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): hypervisor="qemu", seed=False, boot=None, + numatune=None, install=False, start=False, pub_key="/path/to/key.pub", priv_key="/path/to/key", + hypervisor_features={"kvm-hint-dedicated": True}, + clock={"utc": True}, stop_on_reboot=True, connection="someconnection", username="libvirtuser", password="supersecret", + serials=serials, + consoles=consoles, ) # Working update case when running @@ -484,7 +501,12 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): username=None, password=None, boot=None, + numatune=None, test=False, + hypervisor_features=None, + clock=None, + serials=None, + consoles=None, stop_on_reboot=False, ) @@ -597,8 +619,13 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): username=None, password=None, boot=None, + numatune=None, test=True, boot_dev=None, + hypervisor_features=None, + clock=None, + serials=None, + consoles=None, stop_on_reboot=False, ) @@ -633,8 +660,13 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): username=None, password=None, boot=None, + numatune=None, test=True, boot_dev=None, + hypervisor_features=None, + clock=None, + serials=None, + consoles=None, stop_on_reboot=False, ) @@ -701,6 +733,7 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): os_type=None, arch=None, boot=None, + numatune=None, disk=None, disks=[{"name": "system", "image": "/path/to/img.qcow2"}], nic=None, @@ -713,10 +746,14 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): pub_key=None, priv_key=None, boot_dev=None, + hypervisor_features=None, + clock=None, stop_on_reboot=False, connection=None, username=None, password=None, + serials=None, + consoles=None, ) start_mock.assert_called_with( "myvm", connection=None, username=None, password=None @@ -797,15 +834,20 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): hypervisor="qemu", seed=False, boot=None, + numatune=None, install=False, start=False, pub_key="/path/to/key.pub", priv_key="/path/to/key", boot_dev="network hd", + hypervisor_features=None, + clock=None, stop_on_reboot=True, connection="someconnection", username="libvirtuser", password="supersecret", + serials=None, + consoles=None, ) start_mock.assert_called_with( "myvm", @@ -946,8 +988,13 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): username=None, password=None, boot=None, + numatune=None, test=False, boot_dev=None, + hypervisor_features=None, + clock=None, + serials=None, + consoles=None, stop_on_reboot=False, ) @@ -1067,8 +1114,13 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): username=None, password=None, boot=None, + numatune=None, test=True, boot_dev=None, + hypervisor_features=None, + clock=None, + serials=None, + consoles=None, stop_on_reboot=False, ) start_mock.assert_not_called() @@ -1105,8 +1157,13 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): username=None, password=None, boot=None, + numatune=None, test=True, boot_dev=None, + hypervisor_features=None, + clock=None, + serials=None, + consoles=None, stop_on_reboot=False, ) -- 2.29.2
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor