Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-12-SP2:GA
salt.21019
opensuse-3000-virtual-network-backports-329.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File opensuse-3000-virtual-network-backports-329.patch of Package salt.21019
From 9983a8165812d41b3f6b251f74c46cafb4c84b44 Mon Sep 17 00:00:00 2001 From: Cedric Bosdonnat <cbosdonnat@suse.com> Date: Wed, 10 Mar 2021 12:06:06 +0100 Subject: [PATCH] openSUSE 3000 virtual network backports (#329) * Fix the new iothreads virtual disk parameter io_uring has recently been added as another IO policy on virtual disks. Keep the parameter opened for changes. Also add the optional iothread ID in case the user wants to pin a disk to some IO thread (and thus to a CPU). * Remove deprecated update parameter in virt.defined and virt.running * Fix indentation of Jinja instructions in libvirt_domain.jinja * Use more opt_attribute macro in libvirt_domain.jinja * Unify XML indentation in libvirt_domain.jinja * Move virt network generation tests to pytest * Extract XML space stripping function from virt module Stripping spaces and indentation from XML could also be useful in other places, moving to xmlutil to help reuse. * Fix link in virt state documentation * Extract the XML cleanup code from virt.pool_update The network_update code will be rather similar to the pool_update one. In order to share the XML tree cleanup for easier comparisons, create a helper function in the virt module. * virt: expose more properties in virt.network_define In order to let users define more types of virtual networks, expose more of the libvirt virtual network properties. * Remove useless code in virt pytest fixture * Add virt.network_update function In order to enhance the virt.network_defined state, a function to test if an update is needed and update the network is needed. This is done by the newly added virt.network_update function. * Convert the virt network state unit tests to pytest Converting these tests helped reducing the number of lines of code thanks to the pytest parametrize feature. This is also the occasion to split the big tests into smaller ones to report more meaningfull errors and make it more readable. * Let virt.network_update state change existing networks Instead of simply reporting existing networks, update them if needed like other states. Also bubble up the new properties from the virt.define() function. * Add virt.node_devices function For the user to be able to pass host devices through he needs to get a list of the devices that can be passed. * virt: add PCI and USB host devices support to virt init and update In quite a few cases it may be useful to pass a PCI or USB device from the host to the VM. Add support for this in the virt.init() and virt.update() functions. * Convert virt domain state unit tests to pytest While converting the virt domain-related states to pytest I realized the __opts__["test"] == False case was not handled in some of them. This commit also fixes the return code for virt.shutdown, virt.powered_off, virt.snapshot and virt.rebooted states. It also prevents the actual call to be issued in test mode. * Add host_devices to virt running and defined states Expose the new host_devices parameter to the virt.running and virt.defined states. * Convert virt _diff_nics() unit test to pytest * virt: better compare NICs of running VMs On a running guest, libvirt changes the XML definition of the network interfaces of type "network" to the type of the network (for instance bridge). In such a case the virt.update() function will find the two NICs different even if they may not be... so we need to try harder to compare. * virt: hostdev network fixes A network with hostdev forward mode has no bridge and no mac. So we need to handle this in a few places in the virt module. * virt: better handle comparison of hostdev NIC interfaces When a domain has a NIC of type network pointing to a network with hostdev forward, libvirt changes its running XML definition with a hostdev interface with a PCI address from those in the network. Handle this case to avoid useless interface detaching / attaching. * virt: better compare hostdev networks Libvirt adds the PCI addresses of the SR-IOV device virtual functions when only providing the physical function. Those need to be removed in order to avoid network changes for no reason in virt.network_update() * Add xmlutil function dumping a node into a string * Fix virtual network generated DNS XML for SRV records libvirt network's srv element doesn't take a `name` property but a `service` one. * Add missing property in virt.network_define dns srv doc * unCamelCase assertEqualUnit in virt module unit tests In order to ease future virt module unit tests migration to pytest, change the assertEqualUnit function to match the corresponding one in pytest helpers. * Backport missing virt bits * virt.network_update: handle missing ipv4 netmask attribute In the libvirt definition, the IPv4 netmask XML attribute may be replaced by the prefix one. Handle this situation gracefully rather than miserably failing. --- changelog/59143.added | 1 + changelog/59692.fixed | 1 + salt/modules/virt.py | 948 ++++++++- salt/states/virt.py | 477 ++++- salt/templates/virt/libvirt_domain.jinja | 646 ++++--- salt/templates/virt/libvirt_macros.jinja | 3 + salt/templates/virt/libvirt_network.jinja | 98 +- salt/utils/xmlutil.py | 29 + tests/pytests/unit/modules/virt/conftest.py | 72 +- .../pytests/unit/modules/virt/test_domain.py | 463 +++++ .../pytests/unit/modules/virt/test_helpers.py | 1 + tests/pytests/unit/modules/virt/test_host.py | 219 +++ .../pytests/unit/modules/virt/test_network.py | 455 +++++ tests/pytests/unit/states/virt/__init__.py | 0 tests/pytests/unit/states/virt/conftest.py | 36 + tests/pytests/unit/states/virt/test_domain.py | 840 ++++++++ .../pytests/unit/states/virt/test_helpers.py | 99 + .../pytests/unit/states/virt/test_network.py | 476 +++++ tests/pytests/unit/utils/test_xmlutil.py | 14 + tests/unit/modules/test_virt.py | 135 +- tests/unit/states/test_virt.py | 1703 +---------------- 21 files changed, 4391 insertions(+), 2325 deletions(-) create mode 100644 changelog/59143.added create mode 100644 changelog/59692.fixed create mode 100644 salt/templates/virt/libvirt_macros.jinja create mode 100644 tests/pytests/unit/modules/virt/test_host.py create mode 100644 tests/pytests/unit/modules/virt/test_network.py create mode 100644 tests/pytests/unit/states/virt/__init__.py create mode 100644 tests/pytests/unit/states/virt/conftest.py create mode 100644 tests/pytests/unit/states/virt/test_domain.py create mode 100644 tests/pytests/unit/states/virt/test_helpers.py create mode 100644 tests/pytests/unit/states/virt/test_network.py diff --git a/changelog/59143.added b/changelog/59143.added new file mode 100644 index 0000000000..802e925a53 --- /dev/null +++ b/changelog/59143.added @@ -0,0 +1 @@ +Add more network and PCI/USB host devices passthrough support to virt module and states diff --git a/changelog/59692.fixed b/changelog/59692.fixed new file mode 100644 index 0000000000..b4f4533ccc --- /dev/null +++ b/changelog/59692.fixed @@ -0,0 +1 @@ +Don't fail updating network without netmask ip attribute diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 252646466d..c2ae0e44b8 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -431,7 +431,8 @@ def _get_nics(dom): Get domain network interfaces from a libvirt domain object. """ nics = {} - doc = ElementTree.fromstring(dom.XMLDesc(0)) + # Don't expose the active configuration since it may be changed by libvirt + doc = ElementTree.fromstring(dom.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)) for iface_node in doc.findall("devices/interface"): nic = {} nic["type"] = iface_node.get("type") @@ -817,6 +818,7 @@ def _gen_xml( serials=None, consoles=None, stop_on_reboot=False, + host_devices=None, **kwargs ): """ @@ -963,7 +965,8 @@ def _gen_xml( "disk_bus": disk["model"], "format": disk.get("format", "raw"), "index": six.text_type(i), - "io": "threads" if disk.get("iothreads", False) else "native", + "io": disk.get("io", "native"), + "iothread": disk.get("iothread_id", None), } targets.append(disk_context["target_dev"]) if disk.get("source_file"): @@ -1011,6 +1014,44 @@ def _gen_xml( context["disks"].append(disk_context) context["nics"] = nicp + # Process host devices passthrough + hostdev_context = [] + try: + for hostdev_name in host_devices or []: + hostdevice = conn.nodeDeviceLookupByName(hostdev_name) + doc = ElementTree.fromstring(hostdevice.XMLDesc()) + if "pci" in hostdevice.listCaps(): + hostdev_context.append( + { + "type": "pci", + "domain": "0x{:04x}".format( + int(doc.find("./capability[@type='pci']/domain").text) + ), + "bus": "0x{:02x}".format( + int(doc.find("./capability[@type='pci']/bus").text) + ), + "slot": "0x{:02x}".format( + int(doc.find("./capability[@type='pci']/slot").text) + ), + "function": "0x{}".format( + doc.find("./capability[@type='pci']/function").text + ), + } + ) + elif "usb_device" in hostdevice.listCaps(): + vendor_id = doc.find(".//vendor").get("id") + product_id = doc.find(".//product").get("id") + hostdev_context.append( + {"type": "usb", "vendor": vendor_id, "product": product_id} + ) + # For the while we only handle pci and usb passthrough + except libvirt.libvirtError as err: + conn.close() + raise CommandExecutionError( + "Failed to get host devices: " + err.get_error_message() + ) + context["hostdevs"] = hostdev_context + context["os_type"] = os_type context["arch"] = arch fn_ = "libvirt_domain.jinja" @@ -1054,23 +1095,75 @@ def _gen_vol_xml( return template.render(**context) -def _gen_net_xml(name, bridge, forward, vport, tag=None, ip_configs=None): +def _gen_net_xml( + name, + bridge, + forward, + vport, + tag=None, + ip_configs=None, + mtu=None, + domain=None, + nat=None, + interfaces=None, + addresses=None, + physical_function=None, + dns=None, +): """ Generate the XML string to define a libvirt network """ + if isinstance(vport, str): + vport_context = {"type": vport} + else: + vport_context = vport + + if isinstance(tag, (str, int)): + tag_context = {"tags": [{"id": tag}]} + else: + tag_context = tag + + addresses_context = [] + if addresses: + matches = [ + re.fullmatch(r"([0-9]+):([0-9A-Fa-f]+):([0-9A-Fa-f]+)\.([0-9])", addr) + for addr in addresses.lower().split(" ") + ] + addresses_context = [ + { + "domain": m.group(1), + "bus": m.group(2), + "slot": m.group(3), + "function": m.group(4), + } + for m in matches + if m + ] + context = { "name": name, "bridge": bridge, + "mtu": mtu, + "domain": domain, "forward": forward, - "vport": vport, - "tag": tag, + "nat": nat, + "interfaces": interfaces.split(" ") if interfaces else [], + "addresses": addresses_context, + "pf": physical_function, + "vport": vport_context, + "vlan": tag_context, + "dns": dns, "ip_configs": [ { "address": ipaddress.ip_network(config["cidr"]), "dhcp_ranges": config.get("dhcp_ranges", []), + "hosts": config.get("hosts", {}), + "bootp": config.get("bootp", {}), + "tftp": config.get("tftp"), } for config in ip_configs or [] ], + "yesno": lambda v: "yes" if v else "no", } fn_ = "libvirt_network.jinja" try: @@ -1501,7 +1594,7 @@ def _disk_profile(conn, profile, hypervisor, disks, vm_name): disk["format"] = ( "qcow2" if disk.get("device", "disk") != "cdrom" else "raw" ) - elif disk.get("device", "disk") == "disk": + elif vm_name and disk.get("device", "disk") == "disk": _fill_disk_filename(conn, vm_name, disk, hypervisor, pool_caps) return disklist @@ -1825,6 +1918,7 @@ def init( serials=None, consoles=None, stop_on_reboot=False, + host_devices=None, **kwargs ): """ @@ -2155,6 +2249,13 @@ def init( .. versionadded:: Aluminium + :param host_devices: + List of host devices to passthrough to the guest. + The value is a list of device names as provided by the :py:func:`~salt.modules.virt.node_devices` function. + (Default: ``None``) + + .. versionadded:: Aluminium + .. _init-cpu-def: .. rubric:: cpu parameters definition @@ -2497,9 +2598,17 @@ 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``) + io + I/O control policy. String value amongst ``native``, ``threads`` and ``io_uring``. + (Default: ``native``) + + ..versionadded:: Aluminium + + iothread_id + I/O thread id to assign the disk to. + (Default: none assigned) + + ..versionadded:: Aluminium .. _init-graphics-def: @@ -2718,6 +2827,7 @@ def init( serials, consoles, stop_on_reboot, + host_devices, **kwargs ) log.debug("New virtual machine definition: %s", vm_xml) @@ -2776,10 +2886,20 @@ def _nics_equal(nic1, nic2): """ Filter out elements to ignore when comparing nics """ + source_node = nic.find("source") + source_attrib = source_node.attrib if source_node is not None else {} + source_type = "network" if "network" in source_attrib else nic.attrib["type"] + + source_getters = { + "network": lambda n: n.get("network"), + "bridge": lambda n: n.get("bridge"), + "direct": lambda n: n.get("dev"), + "hostdev": lambda n: _format_pci_address(n.find("address")), + } return { - "type": nic.attrib["type"], - "source": nic.find("source").attrib[nic.attrib["type"]] - if nic.find("source") is not None + "type": source_type, + "source": source_getters[source_type](source_node) + if source_node is not None else None, "model": nic.find("model").attrib["type"] if nic.find("model") is not None @@ -2831,6 +2951,32 @@ def _graphics_equal(gfx1, gfx2): ) +def _hostdevs_equal(dev1, dev2): + """ + Test if two hostdevs devices should be considered the same device + """ + + def _filter_hostdevs(dev): + """ + When the domain is running, the hostdevs element may contain additional properties. + This function will only keep the ones we care about + """ + type_ = dev.get("type") + definition = { + "type": type_, + } + if type_ == "pci": + address_node = dev.find("./source/address") + for attr in ["domain", "bus", "slot", "function"]: + definition[attr] = address_node.get(attr) + elif type_ == "usb": + for attr in ["vendor", "product"]: + definition[attr] = dev.find("./source/" + attr).get("id") + return definition + + return _filter_hostdevs(dev1) == _filter_hostdevs(dev2) + + def _diff_lists(old, new, comparator): """ Compare lists to extract the changes @@ -2931,6 +3077,16 @@ def _diff_graphics_lists(old, new): return _diff_lists(old, new, _graphics_equal) +def _diff_hostdev_lists(old, new): + """ + Compare hostdev devices definitions to extract the changes + + :param old: list of ElementTree nodes representing the old hostdev devices + :param new: list of ElementTree nodes representing the new hostdev devices + """ + return _diff_lists(old, new, _hostdevs_equal) + + def _expand_cpuset(cpuset): """ Expand the libvirt cpuset and nodeset values into a list of cpu/node IDs @@ -3026,6 +3182,15 @@ def _diff_console_lists(old, new): return _diff_lists(old, new, _serial_or_concole_equal) +def _format_pci_address(node): + return "{}:{}:{}.{}".format( + node.get("domain").replace("0x", ""), + node.get("bus").replace("0x", ""), + node.get("slot").replace("0x", ""), + node.get("function").replace("0x", ""), + ) + + def _almost_equal(current, new): """ return True if the parameters are numbers that are almost @@ -3050,6 +3215,41 @@ def _compute_device_changes(old_xml, new_xml, to_skip): return changes +def _get_pci_addresses(node): + """ + Get all the pci addresses in the node in 0000:00:00.0 form + """ + return {_format_pci_address(address) for address in node.findall(".//address")} + + +def _correct_networks(conn, desc): + """ + Adjust the interface devices matching existing networks. + Returns the network interfaces XML definition as string mapped to the new device node. + """ + networks = [ElementTree.fromstring(net.XMLDesc()) for net in conn.listAllNetworks()] + nics = desc.findall("devices/interface") + device_map = {} + for nic in nics: + if nic.get("type") == "hostdev": + # Do we have a network matching this NIC PCI address? + addr = _get_pci_addresses(nic.find("source")) + matching_nets = [ + net + for net in networks + if net.find("forward").get("mode") == "hostdev" + and addr & _get_pci_addresses(net) + ] + if matching_nets: + # We need to store the XML before modifying it + # since libvirt needs it to detach the device + old_xml = ElementTree.tostring(nic) + nic.set("type", "network") + nic.find("source").set("network", matching_nets[0].find("name").text) + device_map[nic] = old_xml + return device_map + + def _update_live(domain, new_desc, mem, cpu, old_mem, old_cpu, to_skip, test): """ Perform the live update of a domain. @@ -3095,9 +3295,9 @@ def _update_live(domain, new_desc, mem, cpu, old_mem, old_cpu, to_skip, test): ) # Compute the changes with the live definition - changes = _compute_device_changes( - ElementTree.fromstring(domain.XMLDesc(0)), new_desc, to_skip - ) + old_desc = ElementTree.fromstring(domain.XMLDesc(0)) + changed_devices = {"interface": _correct_networks(domain.connect(), old_desc)} + changes = _compute_device_changes(old_desc, new_desc, to_skip) # Look for removable device source changes removable_changes = [] @@ -3147,20 +3347,19 @@ def _update_live(domain, new_desc, mem, cpu, old_mem, old_cpu, to_skip, test): { "device": dev_type, "cmd": "attachDevice", - "args": [ - salt.utils.stringutils.to_str(ElementTree.tostring(added)) - ], + "args": [xmlutil.element_to_str(added)], } ) for removed in changes[dev_type].get("deleted", []): + removed_def = changed_devices.get(dev_type, {}).get( + removed, ElementTree.tostring(removed) + ) commands.append( { "device": dev_type, "cmd": "detachDevice", - "args": [ - salt.utils.stringutils.to_str(ElementTree.tostring(removed)) - ], + "args": [salt.utils.stringutils.to_str(removed_def)], } ) @@ -3169,9 +3368,7 @@ def _update_live(domain, new_desc, mem, cpu, old_mem, old_cpu, to_skip, test): { "device": "disk", "cmd": "updateDeviceFlags", - "args": [ - salt.utils.stringutils.to_str(ElementTree.tostring(updated_disk)) - ], + "args": [xmlutil.element_to_str(updated_disk)], } ) @@ -3216,6 +3413,7 @@ def update( serials=None, consoles=None, stop_on_reboot=False, + host_devices=None, **kwargs ): """ @@ -3403,6 +3601,13 @@ def update( hpet: present: False + :param host_devices: + List of host devices to passthrough to the guest. + The value is a list of device names as provided by the :py:func:`~salt.modules.virt.node_devices` function. + (Default: ``None``) + + .. versionadded:: Aluminium + :return: Returns a dictionary indicating the status of what has been done. It is structured in @@ -3437,7 +3642,7 @@ def update( } conn = __get_conn(**kwargs) domain = _get_domain(conn, name) - desc = ElementTree.fromstring(domain.XMLDesc(0)) + desc = ElementTree.fromstring(domain.XMLDesc(libvirt.VIR_DOMAIN_XML_INACTIVE)) need_update = False # Compute the XML to get the disks, interfaces and graphics @@ -3466,6 +3671,7 @@ def update( serials=serials, consoles=consoles, stop_on_reboot=stop_on_reboot, + host_devices=host_devices, **kwargs ) ) @@ -3889,23 +4095,22 @@ def update( 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) + xml_desc = xmlutil.element_to_str(desc) log.debug("Update virtual machine definition: %s", xml_desc) - conn.defineXML(salt.utils.stringutils.to_str(xml_desc)) + conn.defineXML(xml_desc) status["definition"] = True except libvirt.libvirtError as err: conn.close() raise err - if live: - live_status, errors = _update_live( - domain, new_desc, mem, cpu, old_mem, old_cpu, to_skip, test - ) - status.update(live_status) - if errors: - if "errors" not in status: - status["errors"] = [] - status["errors"] += errors + if live: + live_status, errors = _update_live( + domain, new_desc, mem, cpu, old_mem, old_cpu, to_skip, test + ) + status.update(live_status) + if errors: + status_errors = status.setdefault("errors", []) + status_errors += errors conn.close() return status @@ -4155,6 +4360,121 @@ def node_info(**kwargs): return info +def _node_devices(conn): + """ + List the host available devices, using an established connection. + + :param conn: the libvirt connection handle to use. + + .. versionadded:: Aluminium + """ + devices = conn.listAllDevices() + + devices_infos = [] + for dev in devices: + root = ElementTree.fromstring(dev.XMLDesc()) + + # Only list PCI and USB devices that can be passed through as well as NICs + if not set(dev.listCaps()) & {"pci", "usb_device", "net"}: + continue + + infos = { + "caps": " ".join(dev.listCaps()), + } + + if "net" in dev.listCaps(): + parent = root.find(".//parent").text + # Don't show, lo, dummies and libvirt-created NICs + if parent == "computer": + continue + infos.update( + { + "name": root.find(".//interface").text, + "address": root.find(".//address").text, + "device name": parent, + "state": root.find(".//link").get("state"), + } + ) + devices_infos.append(infos) + continue + + vendor_node = root.find(".//vendor") + vendor_id = vendor_node.get("id").lower() + product_node = root.find(".//product") + product_id = product_node.get("id").lower() + infos.update( + {"name": dev.name(), "vendor_id": vendor_id, "product_id": product_id} + ) + + # Vendor or product display name may not be set + if vendor_node.text: + infos["vendor"] = vendor_node.text + if product_node.text: + infos["product"] = product_node.text + + if "pci" in dev.listCaps(): + infos["address"] = "{:04x}:{:02x}:{:02x}.{}".format( + int(root.find(".//domain").text), + int(root.find(".//bus").text), + int(root.find(".//slot").text), + root.find(".//function").text, + ) + class_node = root.find(".//class") + if class_node is not None: + infos["PCI class"] = class_node.text + + # Get the list of Virtual Functions if any + vf_addresses = [ + _format_pci_address(vf) + for vf in root.findall( + "./capability[@type='pci']/capability[@type='virt_functions']/address" + ) + ] + if vf_addresses: + infos["virtual functions"] = vf_addresses + + # Get the Physical Function if any + pf = root.find( + "./capability[@type='pci']/capability[@type='phys_function']/address" + ) + if pf is not None: + infos["physical function"] = _format_pci_address(pf) + elif "usb_device" in dev.listCaps(): + infos["address"] = "{:03}:{:03}".format( + int(root.find(".//bus").text), int(root.find(".//device").text) + ) + + # Don't list the pci bridges and USB hosts from the linux foundation + linux_usb_host = vendor_id == "0x1d6b" and product_id in [ + "0x0001", + "0x0002", + "0x0003", + ] + if ( + root.find(".//capability[@type='pci-bridge']") is None + and not linux_usb_host + ): + devices_infos.append(infos) + + return devices_infos + + +def node_devices(**kwargs): + """ + List the host available devices. + + :param connection: libvirt connection URI, overriding defaults + :param username: username to connect with, overriding defaults + :param password: password to connect with, overriding defaults + + .. versionadded:: Aluminium + """ + conn = __get_conn(**kwargs) + devs = _node_devices(conn) + conn.close() + return devs + + def get_nics(vm_, **kwargs): """ Return info about the network interfaces of a named vm @@ -5735,9 +6055,7 @@ def snapshot(domain, name=None, suffix=None, **kwargs): n_name.text = name conn = __get_conn(**kwargs) - _get_domain(conn, domain).snapshotCreateXML( - salt.utils.stringutils.to_str(ElementTree.tostring(doc)) - ) + _get_domain(conn, domain).snapshotCreateXML(xmlutil.element_to_str(doc)) conn.close() return {"name": name} @@ -6407,10 +6725,8 @@ def cpu_baseline(full=False, migratable=False, out="libvirt", **kwargs): conn = __get_conn(**kwargs) caps = ElementTree.fromstring(conn.getCapabilities()) cpu = caps.find("host/cpu") - log.debug( - "Host CPU model definition: %s", - salt.utils.stringutils.to_str(ElementTree.tostring(cpu)), - ) + host_cpu_def = xmlutil.element_to_str(cpu) + log.debug("Host CPU model definition: %s", host_cpu_def) flags = 0 if migratable: @@ -6425,11 +6741,7 @@ def cpu_baseline(full=False, migratable=False, out="libvirt", **kwargs): # This one is only in 1.1.3+ flags += libvirt.VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES - cpu = ElementTree.fromstring( - conn.baselineCPU( - [salt.utils.stringutils.to_str(ElementTree.tostring(cpu))], flags - ) - ) + cpu = ElementTree.fromstring(conn.baselineCPU([host_cpu_def], flags)) conn.close() if full and not getattr(libvirt, "VIR_CONNECT_BASELINE_CPU_EXPAND_FEATURES", False): @@ -6475,18 +6787,70 @@ def cpu_baseline(full=False, migratable=False, out="libvirt", **kwargs): return ElementTree.tostring(cpu) -def network_define(name, bridge, forward, ipv4_config=None, ipv6_config=None, **kwargs): +def network_define( + name, + bridge, + forward, + ipv4_config=None, + ipv6_config=None, + vport=None, + tag=None, + autostart=True, + start=True, + mtu=None, + domain=None, + nat=None, + interfaces=None, + addresses=None, + physical_function=None, + dns=None, + **kwargs +): """ Create libvirt network. - :param name: Network name - :param bridge: Bridge name - :param forward: Forward mode(bridge, router, nat) - :param vport: Virtualport type - :param tag: Vlan tag - :param autostart: Network autostart (default True) - :param start: Network start (default True) - :param ipv4_config: IP v4 configuration + :param name: Network name. + :param bridge: Bridge name. + :param forward: Forward mode (bridge, router, nat). + + .. versionchanged:: Aluminium + a ``None`` value creates an isolated network with no forwarding at all + + :param vport: Virtualport type. + The value can also be a dictionary with ``type`` and ``parameters`` keys. + The ``parameters`` value is a dictionary of virtual port parameters. + + .. code-block:: yaml + + - vport: + type: openvswitch + parameters: + interfaceid: 09b11c53-8b5c-4eeb-8f00-d84eaa0aaa4f + + .. versionchanged:: Aluminium + possible dictionary value + + :param tag: Vlan tag. + The value can also be a dictionary with the ``tags`` and optional ``trunk`` keys. + ``trunk`` is a boolean value indicating whether to use VLAN trunking. + ``tags`` is a list of dictionaries with keys ``id`` and ``nativeMode``. + The ``nativeMode`` value can be one of ``tagged`` or ``untagged``. + + .. code-block:: yaml + + - tag: + trunk: True + tags: + - id: 42 + nativeMode: untagged + - id: 47 + + .. versionchanged:: Aluminium + possible dictionary value + + :param autostart: Network autostart (default True). + :param start: Network start (default True). + :param ipv4_config: IP v4 configuration. Dictionary describing the IP v4 setup like IP range and a possible DHCP configuration. The structure is documented in net-define-ip_. @@ -6494,7 +6858,7 @@ def network_define(name, bridge, forward, ipv4_config=None, ipv6_config=None, ** .. versionadded:: 3000 :type ipv4_config: dict or None - :param ipv6_config: IP v6 configuration + :param ipv6_config: IP v6 configuration. Dictionary describing the IP v6 setup like IP range and a possible DHCP configuration. The structure is documented in net-define-ip_. @@ -6502,13 +6866,108 @@ def network_define(name, bridge, forward, ipv4_config=None, ipv6_config=None, ** .. versionadded:: 3000 :type ipv6_config: dict or None - :param connection: libvirt connection URI, overriding defaults - :param username: username to connect with, overriding defaults - :param password: password to connect with, overriding defaults + :param connection: libvirt connection URI, overriding defaults. + :param username: username to connect with, overriding defaults. + :param password: password to connect with, overriding defaults. + + :param mtu: size of the Maximum Transmission Unit (MTU) of the network. + (default ``None``) + + .. versionadded:: Aluminium + + :param domain: DNS domain name of the DHCP server. + The value is a dictionary with a mandatory ``name`` property and an optional ``localOnly`` boolean one. + (default ``None``) + + .. code-block:: yaml + + - domain: + name: lab.acme.org + localOnly: True + + .. versionadded:: Aluminium + + :param nat: addresses and ports to route in NAT forward mode. + The value is a dictionary with optional keys ``address`` and ``port``. + Both values are a dictionary with ``start`` and ``end`` values. + (default ``None``) + + .. code-block:: yaml + + - forward: nat + - nat: + address: + start: 1.2.3.4 + end: 1.2.3.10 + port: + start: 500 + end: 1000 + + .. versionadded:: Aluminium + + :param interfaces: whitespace separated list of network interfaces devices that can be used for this network. + (default ``None``) + + .. code-block:: yaml + + - forward: passthrough + - interfaces: "eth10 eth11 eth12" + + .. versionadded:: Aluminium + + :param addresses: whitespace separated list of addreses of PCI devices that can be used for this network in `hostdev` forward mode. + (default ``None``) + + .. code-block:: yaml + + - forward: hostdev + - interfaces: "0000:04:00.1 0000:e3:01.2" + + .. versionadded:: Aluminium + + :param physical_function: device name of the physical interface to use in ``hostdev`` forward mode. + (default ``None``) + + .. code-block:: yaml + + - forward: hostdev + - physical_function: "eth0" + + .. versionadded:: Aluminium + + :param dns: virtual network DNS configuration. + The value is a dictionary described in net-define-dns_. + (default ``None``) + + .. code-block:: yaml + + - dns: + forwarders: + - domain: example.com + addr: 192.168.1.1 + - addr: 8.8.8.8 + - domain: www.example.com + txt: + example.com: "v=spf1 a -all" + _http.tcp.example.com: "name=value,paper=A4" + hosts: + 192.168.1.2: + - mirror.acme.lab + - test.acme.lab + srvs: + - name: ldap + protocol: tcp + domain: ldapserver.example.com + target: . + port: 389 + priority: 1 + weight: 10 + + .. versionadded:: Aluminium .. _net-define-ip: - ** IP configuration definition + .. rubric:: IP configuration definition Both the IPv4 and IPv6 configuration dictionaries can contain the following properties: @@ -6516,7 +6975,47 @@ def network_define(name, bridge, forward, ipv4_config=None, ipv6_config=None, ** CIDR notation for the network. For example '192.168.124.0/24' dhcp_ranges - A list of dictionary with ``'start'`` and ``'end'`` properties. + A list of dictionaries with ``'start'`` and ``'end'`` properties. + + hosts + A list of dictionaries with ``ip`` property and optional ``name``, ``mac`` and ``id`` properties. + + .. versionadded:: Aluminium + + bootp + A dictionary with a ``file`` property and an optional ``server`` one. + + .. versionadded:: Aluminium + + tftp + The path to the TFTP root directory to serve. + + .. versionadded:: Aluminium + + .. _net-define-dns: + + .. rubric:: DNS configuration definition + + The DNS configuration dictionary contains the following optional properties: + + forwarders + List of alternate DNS forwarders to use. + Each item is a dictionary with the optional ``domain`` and ``addr`` keys. + If both are provided, the requests to the domain are forwarded to the server at the ``addr``. + If only ``domain`` is provided the requests matching this domain will be resolved locally. + If only ``addr`` is provided all requests will be forwarded to this DNS server. + + txt: + Dictionary of TXT fields to set. + + hosts: + Dictionary of host DNS entries. + The key is the IP of the host, and the value is a list of hostnames for it. + + srvs: + List of SRV DNS entries. + Each entry is a dictionary with the mandatory ``name`` and ``protocol`` keys. + Entries can also have ``target``, ``port``, ``priority``, ``domain`` and ``weight`` optional properties. CLI Example: @@ -6529,8 +7028,6 @@ def network_define(name, bridge, forward, ipv4_config=None, ipv6_config=None, ** conn = __get_conn(**kwargs) vport = kwargs.get("vport", None) tag = kwargs.get("tag", None) - autostart = kwargs.get("autostart", True) - starting = kwargs.get("start", True) net_xml = _gen_net_xml( name, @@ -6539,6 +7036,13 @@ def network_define(name, bridge, forward, ipv4_config=None, ipv6_config=None, ** vport, tag=tag, ip_configs=[config for config in [ipv4_config, ipv6_config] if config], + mtu=mtu, + domain=domain, + nat=nat, + interfaces=interfaces, + addresses=addresses, + physical_function=physical_function, + dns=dns, ) try: conn.networkDefineXML(net_xml) @@ -6558,12 +7062,12 @@ def network_define(name, bridge, forward, ipv4_config=None, ipv6_config=None, ** conn.close() return False - if (starting is True or autostart is True) and network.isActive() != 1: + if (start or autostart) and network.isActive() != 1: network.create() - if autostart is True and network.autostart() != 1: + if autostart and network.autostart() != 1: network.setAutostart(int(autostart)) - elif autostart is False and network.autostart() == 1: + elif not autostart and network.autostart() == 1: network.setAutostart(int(autostart)) conn.close() @@ -6571,6 +7075,271 @@ def network_define(name, bridge, forward, ipv4_config=None, ipv6_config=None, ** return True +def _remove_empty_xml_node(node): + """ + Remove the nodes with no children, no text and no attribute + """ + for child in node: + if not child.tail and not child.text and not child.items() and not child: + node.remove(child) + else: + _remove_empty_xml_node(child) + return node + + +def network_update( + name, + bridge, + forward, + ipv4_config=None, + ipv6_config=None, + vport=None, + tag=None, + mtu=None, + domain=None, + nat=None, + interfaces=None, + addresses=None, + physical_function=None, + dns=None, + test=False, + **kwargs +): + """ + Update a virtual network if needed. + + :param name: Network name. + :param bridge: Bridge name. + :param forward: Forward mode (bridge, router, nat). + A ``None`` value creates an isolated network with no forwarding at all. + + :param vport: Virtualport type. + The value can also be a dictionary with ``type`` and ``parameters`` keys. + The ``parameters`` value is a dictionary of virtual port parameters. + + .. code-block:: yaml + + - vport: + type: openvswitch + parameters: + interfaceid: 09b11c53-8b5c-4eeb-8f00-d84eaa0aaa4f + + :param tag: Vlan tag. + The value can also be a dictionary with the ``tags`` and optional ``trunk`` keys. + ``trunk`` is a boolean value indicating whether to use VLAN trunking. + ``tags`` is a list of dictionaries with keys ``id`` and ``nativeMode``. + The ``nativeMode`` value can be one of ``tagged`` or ``untagged``. + + .. code-block:: yaml + + - tag: + trunk: True + tags: + - id: 42 + nativeMode: untagged + - id: 47 + + :param ipv4_config: IP v4 configuration. + Dictionary describing the IP v4 setup like IP range and + a possible DHCP configuration. The structure is documented + in net-define-ip_. + + :type ipv4_config: dict or None + + :param ipv6_config: IP v6 configuration. + Dictionary describing the IP v6 setup like IP range and + a possible DHCP configuration. The structure is documented + in net-define-ip_. + + :type ipv6_config: dict or None + + :param connection: libvirt connection URI, overriding defaults. + :param username: username to connect with, overriding defaults. + :param password: password to connect with, overriding defaults. + + :param mtu: size of the Maximum Transmission Unit (MTU) of the network. + (default ``None``) + + :param domain: DNS domain name of the DHCP server. + The value is a dictionary with a mandatory ``name`` property and an optional ``localOnly`` boolean one. + (default ``None``) + + .. code-block:: yaml + + - domain: + name: lab.acme.org + localOnly: True + + :param nat: addresses and ports to route in NAT forward mode. + The value is a dictionary with optional keys ``address`` and ``port``. + Both values are a dictionary with ``start`` and ``end`` values. + (default ``None``) + + .. code-block:: yaml + + - forward: nat + - nat: + address: + start: 1.2.3.4 + end: 1.2.3.10 + port: + start: 500 + end: 1000 + + :param interfaces: whitespace separated list of network interfaces devices that can be used for this network. + (default ``None``) + + .. code-block:: yaml + + - forward: passthrough + - interfaces: "eth10 eth11 eth12" + + :param addresses: whitespace separated list of addreses of PCI devices that can be used for this network in `hostdev` forward mode. + (default ``None``) + + .. code-block:: yaml + + - forward: hostdev + - interfaces: "0000:04:00.1 0000:e3:01.2" + + :param physical_function: device name of the physical interface to use in ``hostdev`` forward mode. + (default ``None``) + + .. code-block:: yaml + + - forward: hostdev + - physical_function: "eth0" + + :param dns: virtual network DNS configuration. + The value is a dictionary described in net-define-dns_. + (default ``None``) + + .. code-block:: yaml + + - dns: + forwarders: + - domain: example.com + addr: 192.168.1.1 + - addr: 8.8.8.8 + - domain: www.example.com + txt: + example.com: "v=spf1 a -all" + _http.tcp.example.com: "name=value,paper=A4" + hosts: + 192.168.1.2: + - mirror.acme.lab + - test.acme.lab + srvs: + - name: ldap + protocol: tcp + domain: ldapserver.example.com + target: . + port: 389 + priority: 1 + weight: 10 + + .. versionadded:: Aluminium + """ + # Get the current definition to compare the two + conn = __get_conn(**kwargs) + needs_update = False + try: + net = conn.networkLookupByName(name) + old_xml = ElementTree.fromstring(net.XMLDesc()) + + # Compute new definition + new_xml = ElementTree.fromstring( + _gen_net_xml( + name, + bridge, + forward, + vport, + tag=tag, + ip_configs=[config for config in [ipv4_config, ipv6_config] if config], + mtu=mtu, + domain=domain, + nat=nat, + interfaces=interfaces, + addresses=addresses, + physical_function=physical_function, + dns=dns, + ) + ) + + elements_to_copy = ["uuid", "mac"] + for to_copy in elements_to_copy: + element = old_xml.find(to_copy) + # mac may not be present (hostdev network for instance) + if element is not None: + new_xml.insert(1, element) + + # Libvirt adds a connection attribute on running networks, remove before comparing + old_xml.attrib.pop("connections", None) + + # Libvirt adds the addresses of the VF devices on running networks with the ph passed + # Those need to be removed before comparing + if old_xml.find("forward/pf") is not None: + forward_node = old_xml.find("forward") + address_nodes = forward_node.findall("address") + for node in address_nodes: + forward_node.remove(node) + + # Remove libvirt auto-added bridge attributes to compare + default_bridge_attribs = {"stp": "on", "delay": "0"} + old_bridge_node = old_xml.find("bridge") + if old_bridge_node is not None: + for key, value in default_bridge_attribs.items(): + if old_bridge_node.get(key, None) == value: + old_bridge_node.attrib.pop(key, None) + + # Libvirt may also add the whole bridge network since the name can be computed + # If the bridge name starts with virbr in a nat, route, open or isolated network + # there is a good change it has been autogenerated... + old_forward = ( + old_xml.find("forward").get("mode") + if old_xml.find("forward") is not None + else None + ) + if ( + old_forward == forward + and forward in ["nat", "route", "open", None] + and bridge is None + and old_bridge_node.get("name", "").startswith("virbr") + ): + old_bridge_node.attrib.pop("name", None) + + # In the ipv4 address, we need to convert netmask to prefix in the old XML + ipv4_nodes = [ + node + for node in old_xml.findall("ip") + if node.get("family", "ipv4") == "ipv4" + ] + for ip_node in ipv4_nodes: + netmask = ip_node.attrib.pop("netmask", None) + if netmask: + address = ipaddress.ip_network( + "{}/{}".format(ip_node.get("address"), netmask), strict=False + ) + ip_node.set("prefix", str(address.prefixlen)) + + # Add default ipv4 family if needed + for doc in [old_xml, new_xml]: + for node in doc.findall("ip"): + if "family" not in node.keys(): + node.set("family", "ipv4") + + # Filter out spaces and empty elements since those would mislead the comparison + _remove_empty_xml_node(xmlutil.strip_spaces(old_xml)) + xmlutil.strip_spaces(new_xml) + + needs_update = xmlutil.to_dict(old_xml, True) != xmlutil.to_dict(new_xml, True) + if needs_update and not test: + conn.networkDefineXML(xmlutil.element_to_str(new_xml)) + finally: + conn.close() + return needs_update + + def list_networks(**kwargs): """ List all virtual networks. @@ -6630,6 +7399,16 @@ def network_info(name=None, **kwargs): lease["type"] = "unknown" return leases + def _net_get_bridge(net): + """ + Get the bridge of the network or None + """ + try: + return net.bridgeName() + except libvirt.libvirtError as err: + # Some network configurations have no bridge + return None + try: nets = [ net for net in conn.listAllNetworks() if name is None or net.name() == name @@ -6637,7 +7416,7 @@ def network_info(name=None, **kwargs): result = { net.name(): { "uuid": net.UUIDString(), - "bridge": net.bridgeName(), + "bridge": _net_get_bridge(net), "autostart": net.autostart(), "active": net.isActive(), "persistent": net.isPersistent(), @@ -7396,37 +8175,12 @@ def pool_update( new_xml.insert(1, element) # Filter out spaces and empty elements like <source/> since those would mislead the comparison - def visit_xml(node, fn): - fn(node) - for child in node: - visit_xml(child, fn) - - def space_stripper(node): - if node.tail is not None: - node.tail = node.tail.strip(" \t\n") - if node.text is not None: - node.text = node.text.strip(" \t\n") - - visit_xml(old_xml, space_stripper) - visit_xml(new_xml, space_stripper) - - def empty_node_remover(node): - for child in node: - if ( - not child.tail - and not child.text - and not child.items() - and not child - ): - node.remove(child) - - visit_xml(old_xml, empty_node_remover) + _remove_empty_xml_node(xmlutil.strip_spaces(old_xml)) + xmlutil.strip_spaces(new_xml) needs_update = xmlutil.to_dict(old_xml, True) != xmlutil.to_dict(new_xml, True) if needs_update and not test: - conn.storagePoolDefineXML( - salt.utils.stringutils.to_str(ElementTree.tostring(new_xml)) - ) + conn.storagePoolDefineXML(xmlutil.element_to_str(new_xml)) finally: conn.close() return needs_update diff --git a/salt/states/virt.py b/salt/states/virt.py index cb24e622e0..6d1fc99ca8 100644 --- a/salt/states/virt.py +++ b/salt/states/virt.py @@ -160,7 +160,8 @@ def _virt_call( :param state: the expected final state of the VM. If None the VM state won't be checked. :return: the salt state return """ - ret = {"name": domain, "changes": {}, "result": True, "comment": ""} + result = True if not __opts__["test"] else None + ret = {"name": domain, "changes": {}, "result": result, "comment": ""} targeted_domains = fnmatch.filter(__salt__["virt.list_domains"](), domain) changed_domains = list() ignored_domains = list() @@ -173,15 +174,17 @@ def _virt_call( domain_state = __salt__["virt.vm_state"](targeted_domain) action_needed = domain_state.get(targeted_domain) != state if action_needed: - response = __salt__["virt.{}".format(function)]( - targeted_domain, - connection=connection, - username=username, - password=password, - **kwargs - ) - if isinstance(response, dict): - response = response["name"] + response = True + if not __opts__["test"]: + response = __salt__["virt.{}".format(function)]( + targeted_domain, + connection=connection, + username=username, + password=password, + **kwargs + ) + if isinstance(response, dict): + response = response["name"] changed_domains.append({"domain": targeted_domain, function: response}) else: noaction_domains.append(targeted_domain) @@ -287,7 +290,6 @@ def defined( arch=None, boot=None, numatune=None, - update=True, boot_dev=None, hypervisor_features=None, clock=None, @@ -295,6 +297,7 @@ def defined( consoles=None, stop_on_reboot=False, live=True, + host_devices=None, ): """ Starts an existing guest, or defines and starts a new VM with specified arguments. @@ -497,10 +500,6 @@ def defined( .. versionadded:: 3000 - :param update: set to ``False`` to prevent updating a defined domain. (Default: ``True``) - - .. deprecated:: sodium - :param boot_dev: Space separated list of devices to boot from sorted by decreasing priority. Values can be ``hd``, ``fd``, ``cdrom`` or ``network``. @@ -594,6 +593,13 @@ def defined( .. versionadded:: Aluminium + :param host_devices: + List of host devices to passthrough to the guest. + The value is a list of device names as provided by the :py:func:`~salt.modules.virt.node_devices` function. + (Default: ``None``) + + .. versionadded:: Aluminium + .. rubric:: Example States Make sure a virtual machine called ``domain_name`` is defined: @@ -640,31 +646,30 @@ def defined( if name in __salt__["virt.list_domains"]( connection=connection, username=username, password=password ): - status = {} - if update: - status = __salt__["virt.update"]( - name, - cpu=cpu, - mem=mem, - disk_profile=disk_profile, - disks=disks, - nic_profile=nic_profile, - interfaces=interfaces, - graphics=graphics, - live=live, - connection=connection, - 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, - ) + status = __salt__["virt.update"]( + name, + cpu=cpu, + mem=mem, + disk_profile=disk_profile, + disks=disks, + nic_profile=nic_profile, + interfaces=interfaces, + graphics=graphics, + live=live, + connection=connection, + 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, + host_devices=host_devices, + ) ret["changes"][name] = status if not status.get("definition"): ret["comment"] = "Domain {} unchanged".format(name) @@ -705,6 +710,7 @@ def defined( hypervisor_features=hypervisor_features, clock=clock, stop_on_reboot=stop_on_reboot, + host_devices=host_devices, ) ret["changes"][name] = {"definition": True} ret["comment"] = "Domain {} defined".format(name) @@ -730,7 +736,6 @@ def running( install=True, pub_key=None, priv_key=None, - update=False, connection=None, username=None, password=None, @@ -744,6 +749,7 @@ def running( serials=None, consoles=None, stop_on_reboot=False, + host_devices=None, ): """ Starts an existing guest, or defines and starts a new VM with specified arguments. @@ -825,10 +831,6 @@ def running( :param seed_cmd: Salt command to execute to seed the image. (Default: ``'seed.apply'``) .. versionadded:: 2019.2.0 - :param update: set to ``True`` to update a defined domain. (Default: ``False``) - - .. versionadded:: 2019.2.0 - .. deprecated:: sodium :param connection: libvirt connection URI, overriding defaults .. versionadded:: 2019.2.0 @@ -961,6 +963,13 @@ def running( clock: timezone: CEST + :param host_devices: + List of host devices to passthrough to the guest. + The value is a list of device names as provided by the :py:func:`~salt.modules.virt.node_devices` function. + (Default: ``None``) + + .. versionadded:: Aluminium + .. rubric:: Example States Make sure an already-defined virtual machine called ``domain_name`` is running: @@ -1004,12 +1013,6 @@ def running( """ merged_disks = disks - if not update: - salt.utils.versions.warn_until( - "Aluminium", - "'update' parameter has been deprecated. Future behavior will be the one of update=True" - "It will be removed in {version}.", - ) ret = defined( name, cpu=cpu, @@ -1027,7 +1030,6 @@ def running( os_type=os_type, arch=arch, boot=boot, - update=update, boot_dev=boot_dev, numatune=numatune, hypervisor_features=hypervisor_features, @@ -1038,6 +1040,7 @@ def running( password=password, serials=serials, consoles=consoles, + host_devices=host_devices, ) result = True if not __opts__["test"] else None @@ -1263,21 +1266,64 @@ def network_defined( connection=None, username=None, password=None, + mtu=None, + domain=None, + nat=None, + interfaces=None, + addresses=None, + physical_function=None, + dns=None, ): """ Defines a new network with specified arguments. + :param name: Network name :param bridge: Bridge name :param forward: Forward mode(bridge, router, nat) + + .. versionchanged:: Aluminium + a ``None`` value creates an isolated network with no forwarding at all + :param vport: Virtualport type (Default: ``'None'``) + The value can also be a dictionary with ``type`` and ``parameters`` keys. + The ``parameters`` value is a dictionary of virtual port parameters. + + .. code-block:: yaml + + - vport: + type: openvswitch + parameters: + interfaceid: 09b11c53-8b5c-4eeb-8f00-d84eaa0aaa4f + + .. versionchanged:: Aluminium + possible dictionary value + :param tag: Vlan tag (Default: ``'None'``) + The value can also be a dictionary with the ``tags`` and optional ``trunk`` keys. + ``trunk`` is a boolean value indicating whether to use VLAN trunking. + ``tags`` is a list of dictionaries with keys ``id`` and ``nativeMode``. + The ``nativeMode`` value can be one of ``tagged`` or ``untagged``. + + .. code-block:: yaml + + - tag: + trunk: True + tags: + - id: 42 + nativeMode: untagged + - id: 47 + + .. versionchanged:: Aluminium + possible dictionary value + :param ipv4_config: - IPv4 network configuration. See the :py:func`virt.network_define - <salt.modules.virt.network_define>` function corresponding parameter documentation + IPv4 network configuration. See the + :py:func:`virt.network_define <salt.modules.virt.network_define>` + function corresponding parameter documentation for more details on this dictionary. (Default: None). :param ipv6_config: - IPv6 network configuration. See the :py:func`virt.network_define + IPv6 network configuration. See the :py:func:`virt.network_define <salt.modules.virt.network_define>` function corresponding parameter documentation for more details on this dictionary. (Default: None). @@ -1285,6 +1331,100 @@ def network_defined( :param connection: libvirt connection URI, overriding defaults :param username: username to connect with, overriding defaults :param password: password to connect with, overriding defaults + :param mtu: size of the Maximum Transmission Unit (MTU) of the network. + (default ``None``) + + .. versionadded:: Aluminium + + :param domain: DNS domain name of the DHCP server. + The value is a dictionary with a mandatory ``name`` property and an optional ``localOnly`` boolean one. + (default ``None``) + + .. code-block:: yaml + + - domain: + name: lab.acme.org + localOnly: True + + .. versionadded:: Aluminium + + :param nat: addresses and ports to route in NAT forward mode. + The value is a dictionary with optional keys ``address`` and ``port``. + Both values are a dictionary with ``start`` and ``end`` values. + (default ``None``) + + .. code-block:: yaml + + - forward: nat + - nat: + address: + start: 1.2.3.4 + end: 1.2.3.10 + port: + start: 500 + end: 1000 + + .. versionadded:: Aluminium + + :param interfaces: whitespace separated list of network interfaces devices that can be used for this network. + (default ``None``) + + .. code-block:: yaml + + - forward: passthrough + - interfaces: "eth10 eth11 eth12" + + .. versionadded:: Aluminium + + :param addresses: whitespace separated list of addreses of PCI devices that can be used for this network in `hostdev` forward mode. + (default ``None``) + + .. code-block:: yaml + + - forward: hostdev + - interfaces: "0000:04:00.1 0000:e3:01.2" + + .. versionadded:: Aluminium + + :param physical_function: device name of the physical interface to use in ``hostdev`` forward mode. + (default ``None``) + + .. code-block:: yaml + + - forward: hostdev + - physical_function: "eth0" + + .. versionadded:: Aluminium + + :param dns: virtual network DNS configuration + The value is a dictionary described in :ref:`net-define-dns`. + (default ``None``) + + .. code-block:: yaml + + - dns: + forwarders: + - domain: example.com + addr: 192.168.1.1 + - addr: 8.8.8.8 + - domain: www.example.com + txt: + example.com: "v=spf1 a -all" + _http.tcp.example.com: "name=value,paper=A4" + hosts: + 192.168.1.2: + - mirror.acme.lab + - test.acme.lab + srvs: + - name: ldap + protocol: tcp + domain: ldapserver.example.com + target: . + port: 389 + priority: 1 + weight: 10 + + .. versionadded:: Aluminium .. versionadded:: sodium @@ -1331,9 +1471,62 @@ def network_defined( name, connection=connection, username=username, password=password ) if info and info[name]: - ret["comment"] = "Network {} exists".format(name) - ret["result"] = True + needs_autostart = ( + info[name]["autostart"] + and not autostart + or not info[name]["autostart"] + and autostart + ) + needs_update = __salt__["virt.network_update"]( + name, + bridge, + forward, + vport=vport, + tag=tag, + ipv4_config=ipv4_config, + ipv6_config=ipv6_config, + mtu=mtu, + domain=domain, + nat=nat, + interfaces=interfaces, + addresses=addresses, + physical_function=physical_function, + dns=dns, + test=True, + connection=connection, + username=username, + password=password, + ) + if needs_update: + if not __opts__["test"]: + __salt__["virt.network_update"]( + name, + bridge, + forward, + vport=vport, + tag=tag, + ipv4_config=ipv4_config, + ipv6_config=ipv6_config, + mtu=mtu, + domain=domain, + nat=nat, + interfaces=interfaces, + addresses=addresses, + physical_function=physical_function, + dns=dns, + test=False, + connection=connection, + username=username, + password=password, + ) + action = ", autostart flag changed" if needs_autostart else "" + ret["changes"][name] = "Network updated{}".format(action) + ret["comment"] = "Network {} updated{}".format(name, action) + else: + ret["comment"] = "Network {} unchanged".format(name) + ret["result"] = True else: + needs_autostart = autostart if not __opts__["test"]: __salt__["virt.network_define"]( name, @@ -1343,14 +1536,35 @@ def network_defined( tag=tag, ipv4_config=ipv4_config, ipv6_config=ipv6_config, - autostart=autostart, + mtu=mtu, + domain=domain, + nat=nat, + interfaces=interfaces, + addresses=addresses, + physical_function=physical_function, + dns=dns, + autostart=False, start=False, connection=connection, username=username, password=password, ) - ret["changes"][name] = "Network defined" - ret["comment"] = "Network {} defined".format(name) + if needs_autostart: + ret["changes"][name] = "Network defined, marked for autostart" + ret["comment"] = "Network {} defined, marked for autostart".format(name) + else: + ret["changes"][name] = "Network defined" + ret["comment"] = "Network {} defined".format(name) + + if needs_autostart: + if not __opts__["test"]: + __salt__["virt.network_set_autostart"]( + name, + state="on" if autostart else "off", + connection=connection, + username=username, + password=password, + ) except libvirt.libvirtError as err: ret["result"] = False ret["comment"] = err.get_error_message() @@ -1370,14 +1584,56 @@ def network_running( connection=None, username=None, password=None, + mtu=None, + domain=None, + nat=None, + interfaces=None, + addresses=None, + physical_function=None, + dns=None, ): """ Defines and starts a new network with specified arguments. + :param name: Network name :param bridge: Bridge name :param forward: Forward mode(bridge, router, nat) + + .. versionchanged:: Aluminium + a ``None`` value creates an isolated network with no forwarding at all + :param vport: Virtualport type (Default: ``'None'``) + The value can also be a dictionary with ``type`` and ``parameters`` keys. + The ``parameters`` value is a dictionary of virtual port parameters. + + .. code-block:: yaml + + - vport: + type: openvswitch + parameters: + interfaceid: 09b11c53-8b5c-4eeb-8f00-d84eaa0aaa4f + + .. versionchanged:: Aluminium + possible dictionary value + :param tag: Vlan tag (Default: ``'None'``) + The value can also be a dictionary with the ``tags`` and optional ``trunk`` keys. + ``trunk`` is a boolean value indicating whether to use VLAN trunking. + ``tags`` is a list of dictionaries with keys ``id`` and ``nativeMode``. + The ``nativeMode`` value can be one of ``tagged`` or ``untagged``. + + .. code-block:: yaml + + - tag: + trunk: True + tags: + - id: 42 + nativeMode: untagged + - id: 47 + + .. versionchanged:: Aluminium + possible dictionary value + :param ipv4_config: IPv4 network configuration. See the :py:func`virt.network_define <salt.modules.virt.network_define>` function corresponding parameter documentation @@ -1402,6 +1658,100 @@ def network_running( :param password: password to connect with, overriding defaults .. versionadded:: 2019.2.0 + :param mtu: size of the Maximum Transmission Unit (MTU) of the network. + (default ``None``) + + .. versionadded:: Aluminium + + :param domain: DNS domain name of the DHCP server. + The value is a dictionary with a mandatory ``name`` property and an optional ``localOnly`` boolean one. + (default ``None``) + + .. code-block:: yaml + + - domain: + name: lab.acme.org + localOnly: True + + .. versionadded:: Aluminium + + :param nat: addresses and ports to route in NAT forward mode. + The value is a dictionary with optional keys ``address`` and ``port``. + Both values are a dictionary with ``start`` and ``end`` values. + (default ``None``) + + .. code-block:: yaml + + - forward: nat + - nat: + address: + start: 1.2.3.4 + end: 1.2.3.10 + port: + start: 500 + end: 1000 + + .. versionadded:: Aluminium + + :param interfaces: whitespace separated list of network interfaces devices that can be used for this network. + (default ``None``) + + .. code-block:: yaml + + - forward: passthrough + - interfaces: "eth10 eth11 eth12" + + .. versionadded:: Aluminium + + :param addresses: whitespace separated list of addreses of PCI devices that can be used for this network in `hostdev` forward mode. + (default ``None``) + + .. code-block:: yaml + + - forward: hostdev + - interfaces: "0000:04:00.1 0000:e3:01.2" + + .. versionadded:: Aluminium + + :param physical_function: device name of the physical interface to use in ``hostdev`` forward mode. + (default ``None``) + + .. code-block:: yaml + + - forward: hostdev + - physical_function: "eth0" + + .. versionadded:: Aluminium + + :param dns: virtual network DNS configuration + The value is a dictionary described in :ref:`net-define-dns`. + (default ``None``) + + .. code-block:: yaml + + - dns: + forwarders: + - domain: example.com + addr: 192.168.1.1 + - addr: 8.8.8.8 + - domain: www.example.com + txt: + host.widgets.com.: "printer=lpr5" + example.com.: "This domain name is reserved for use in documentation" + hosts: + 192.168.1.2: + - mirror.acme.lab + - test.acme.lab + srvs: + - name: ldap + protocol: tcp + domain: ldapserver.example.com + target: . + port: 389 + priority: 1 + weight: 10 + + .. versionadded:: Aluminium .. code-block:: yaml @@ -1442,6 +1792,13 @@ def network_running( tag=tag, ipv4_config=ipv4_config, ipv6_config=ipv6_config, + mtu=mtu, + domain=domain, + nat=nat, + interfaces=interfaces, + addresses=addresses, + physical_function=physical_function, + dns=dns, autostart=autostart, connection=connection, username=username, diff --git a/salt/templates/virt/libvirt_domain.jinja b/salt/templates/virt/libvirt_domain.jinja index 7c4cf0acf3..6772b0db56 100644 --- a/salt/templates/virt/libvirt_domain.jinja +++ b/salt/templates/virt/libvirt_domain.jinja @@ -1,342 +1,336 @@ {%- import 'libvirt_disks.jinja' as libvirt_disks -%} +{%- from 'libvirt_macros.jinja' import opt_attribute as opt_attribute -%} {%- 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> - {%- 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 {{ opt_attribute(mem, 'slots') }} unit='KiB'>{{ to_kib(mem.max) }}</maxMemory> - {%- endif %} - {%- if mem.boot %} - <memory unit='KiB'>{{ to_kib(mem.boot) }}</memory> - {%- endif %} - {%- if mem.current %} - <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">{{ to_kib(mem.hard_limit) }}</hard_limit> - {%- endif %} - {%- if 'soft_limit' in mem and mem.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">{{ to_kib(mem.swap_hard_limit) }}</swap_hard_limit> - {%- endif %} - {%- if 'min_guarantee' in mem and mem.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> + <name>{{ name }}</name> +{%- 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 %} - {%- 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> + </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 }}'{{ opt_attribute(sched, "vcpus") }}{{ opt_attribute(sched, "priority") }}/> + {%- endfor %} + {%- endif %} + {%- if cpu.tuning.iothreadsched %} + {%- for sched in cpu.tuning.iothreadsched %} + <iothreadsched scheduler='{{ sched.scheduler }}'{{ opt_attribute(sched, "iothreads") }}{{ opt_attribute(sched, "priority") }}/> + {%- endfor %} + {%- endif %} + {%- if cpu.tuning.emulatorsched %} + <emulatorsched scheduler='{{ cpu.tuning.emulatorsched.scheduler }}'{{ opt_attribute(cpu.tuning.emulatorsched, "priority") }}/> + {%- 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 %} - <os {{ boot.os_attrib }}> - <type arch='{{ arch }}'>{{ os_type }}</type> - {% if boot %} - {% if 'kernel' in boot %} - <kernel>{{ boot.kernel }}</kernel> - {% endif %} - {% if 'initrd' in boot %} - <initrd>{{ boot.initrd }}</initrd> - {% endif %} - {% if 'cmdline' in boot %} - <cmdline>{{ boot.cmdline }}</cmdline> - {% endif %} - {% if 'loader' in boot %} - <loader readonly='yes' type='pflash'>{{ boot.loader }}</loader> - {% endif %} - {% if 'nvram' in boot %} - <nvram template='{{boot.nvram}}'></nvram> - {% endif %} - {% endif %} - {% for dev in boot_dev %} - <boot dev='{{ dev }}' /> - {% endfor %} - </os> + {%- 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 {{ opt_attribute(mem, 'slots') }} unit='KiB'>{{ to_kib(mem.max) }}</maxMemory> +{%- endif %} +{%- if mem.boot %} + <memory unit='KiB'>{{ to_kib(mem.boot) }}</memory> +{%- endif %} +{%- if mem.current %} + <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">{{ to_kib(mem.hard_limit) }}</hard_limit> + {%- endif %} + {%- if 'soft_limit' in mem and mem.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">{{ to_kib(mem.swap_hard_limit) }}</swap_hard_limit> + {%- endif %} + {%- if 'min_guarantee' in mem and mem.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 }}'{{ opt_attribute(numatune.memory, "nodeset") }}/> + {%- 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"{{ opt_attribute(page, "nodeset" )}}/> + {%- 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 %} + {%- if 'kernel' in boot %} + <kernel>{{ boot.kernel }}</kernel> + {%- endif %} + {%- if 'initrd' in boot %} + <initrd>{{ boot.initrd }}</initrd> + {%- endif %} + {%- if 'cmdline' in boot %} + <cmdline>{{ boot.cmdline }}</cmdline> + {%- endif %} + {%- if 'loader' in boot %} + <loader readonly='yes' type='pflash'>{{ boot.loader }}</loader> + {%- endif %} + {%- if 'nvram' in boot %} + <nvram template='{{boot.nvram}}'></nvram> + {%- endif %} +{%- endif %} +{%- for dev in boot_dev %} + <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 %} + <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> + <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 %} + <disk type='{{ disk.type }}' device='{{ disk.device }}'> + {%- if disk.type == 'file' and 'source_file' in disk -%} + <source file='{{ disk.source_file }}' /> + {%- endif %} + {%- if disk.type == 'block' -%} + <source dev='{{ disk.source_file }}' /> + {%- endif %} + {%- if disk.type == 'volume' and 'pool' in disk -%} + <source pool='{{ disk.pool }}' volume='{{ disk.volume }}' /> + {%- endif %} + {%- if disk.type == 'network' %}{{ libvirt_disks.network_source(disk) }}{%- endif %} + <target dev='{{ disk.target_dev }}' bus='{{ disk.disk_bus }}' /> + {%- if disk.address -%} + <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='{{ disk.io }}'{{ opt_attribute(disk, "iothread") }}/> + {%- endif %} + </disk> +{%- endfor %} +{%- if controller_model %} + <controller type='scsi' index='0' model='{{ controller_model }}' /> +{%- endif %} +{%- for nic in nics %} + <interface type='{{ nic.type }}'> + <source {{ nic.type }}='{{ nic.source }}'/> + {%- if nic.get('mac') -%} + <mac address='{{ nic.mac }}'/> + {%- endif %} + {%- if nic.model %}<model type='{{ nic.model }}'/>{% endif %} + </interface> +{%- endfor %} +{%- if graphics %} + <graphics type='{{ graphics.type }}'{{ opt_attribute(graphics, "port") }} + {%- if graphics.listen.address %} + listen='{{ graphics.listen.address }}' + {%- endif %} + {%- if graphics.type == 'spice' and graphics.tls_port %} + tlsPort='{{ graphics.tls_port }}' + {%- endif %} + autoport='{{ yesno(not graphics.port and not graphics.tls_port) }}'> + <listen type='{{ graphics.listen.type }}'{{ opt_attribute(graphics.listen, "address") }}/> + </graphics> + {%- if graphics.type == "spice" and hypervisor in ["qemu", "kvm"] %} + <channel type='spicevmc'> + <target type='virtio' name='com.redhat.spice.0'/> + </channel> + {%- endif %} {%- endif %} - <on_reboot>{{ on_reboot }}</on_reboot> - <devices> - {% for disk in disks %} - <disk type='{{ disk.type }}' device='{{ disk.device }}'> - {% if disk.type == 'file' and 'source_file' in disk -%} - <source file='{{ disk.source_file }}' /> - {% endif %} - {% if disk.type == 'block' -%} - <source dev='{{ disk.source_file }}' /> - {% endif %} - {% if disk.type == 'volume' and 'pool' in disk -%} - <source pool='{{ disk.pool }}' volume='{{ disk.volume }}' /> - {% endif %} - {%- if disk.type == 'network' %}{{ libvirt_disks.network_source(disk) }}{%- endif %} - <target dev='{{ disk.target_dev }}' bus='{{ disk.disk_bus }}' /> - {% if disk.address -%} - <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='{{ disk.io }}'/> - {% endif %} - </disk> - {% endfor %} - - {% if controller_model %} - <controller type='scsi' index='0' model='{{ controller_model }}' /> - {% endif %} - - {% for nic in nics %} - <interface type='{{ nic.type }}'> - <source {{ nic.type }}='{{ nic.source }}'/> - {% if nic.get('mac') -%} - <mac address='{{ nic.mac }}'/> - {%- endif %} - {% if nic.model %}<model type='{{ nic.model }}'/>{% endif %} - </interface> - {% endfor %} - {% if graphics %} - <graphics type='{{ graphics.type }}' - {% if graphics.listen.address %} - listen='{{ graphics.listen.address }}' - {% endif %} - {% if graphics.port %} - port='{{ graphics.port }}' - {% endif %} - {% if graphics.type == 'spice' and graphics.tls_port %} - tlsPort='{{ graphics.tls_port }}' - {% endif %} - autoport='{{ 'no' if graphics.port or graphics.tls_port else 'yes' }}'> - <listen type='{{ graphics.listen.type }}' - {% if graphics.listen.address %} - address='{{ graphics.listen.address }}' - {% endif %}/> - </graphics> - - {% if graphics.type == "spice" and hypervisor in ["qemu", "kvm"] -%} - <channel type='spicevmc'> - <target type='virtio' name='com.redhat.spice.0'/> - </channel> - {%- endif %} - {% endif %} - - {%- for serial in serials %} - <serial type='{{ serial.type }}'> - {{ libvirt_chardevs.chardev(serial) }} - </serial> - {%- endfor %} - - {%- for console in consoles %} - <console type='{{ console.type }}'> - {{ libvirt_chardevs.chardev(console) }} - </console> - {% endfor %} +{%- for serial in serials %} + <serial type='{{ serial.type }}'> + {{ libvirt_chardevs.chardev(serial) }} + </serial> +{%- 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> + <channel type='unix'> + <target type='virtio' name='org.qemu.guest_agent.0'/> + </channel> {%- endif %} - </devices> - <features> - <acpi /> - <apic /> - <pae /> +{%- for hostdev in hostdevs %} + <hostdev mode='subsystem' type='{{ hostdev["type"] }}'{% if hostdev["type"] == "pci" %} managed='yes'{% endif %}> + <source> + {%- if hostdev["type"] == "usb" %} + <vendor id='{{ hostdev["vendor"] }}'/> + <product id='{{ hostdev["product"] }}'/> + {%- elif hostdev["type"] == "pci" %} + <address + domain='{{ hostdev["domain"] }}' + bus='{{ hostdev["bus"] }}' + slot='{{ hostdev["slot"] }}' + function='{{ hostdev["function"] }}'/> + {%- endif %} + </source> + </hostdev> +{%- endfor %} + </devices> + <features> + <acpi /> + <apic /> + <pae /> {%- if hypervisor_features.get("kvm-hint-dedicated") %} - <kvm> - <hint-dedicated state="on"/> - </kvm> + <kvm> + <hint-dedicated state="on"/> + </kvm> {%- endif %} - </features> + </features> </domain> diff --git a/salt/templates/virt/libvirt_macros.jinja b/salt/templates/virt/libvirt_macros.jinja new file mode 100644 index 0000000000..d2e2fc213d --- /dev/null +++ b/salt/templates/virt/libvirt_macros.jinja @@ -0,0 +1,3 @@ +{%- 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 %} diff --git a/salt/templates/virt/libvirt_network.jinja b/salt/templates/virt/libvirt_network.jinja index 2f11e64559..98bd6567b8 100644 --- a/salt/templates/virt/libvirt_network.jinja +++ b/salt/templates/virt/libvirt_network.jinja @@ -1,20 +1,98 @@ +{%- from 'libvirt_macros.jinja' import opt_attribute as opt_attribute -%} <network> <name>{{ name }}</name> +{%- if bridge %} <bridge name='{{ bridge }}'/> - <forward mode='{{ forward }}'/>{% if vport != None %} - <virtualport type='{{ vport }}'/>{% endif %}{% if tag != None %} - <vlan> - <tag id='{{ tag }}'/> - </vlan>{% endif %} - {% for ip_config in ip_configs %} +{%- endif %} +{%- if mtu %} + <mtu size='{{ mtu }}'/> +{%- endif %} +{%- if domain %} + <domain name='{{ domain.name }}'{{ opt_attribute(domain, "localOnly", yesno) }}/> +{%- endif %} +{%- if forward %} + <forward mode='{{ forward }}'{% if forward == 'hostdev' %} managed='yes'{% endif %}> +{%- endif %} +{%- if nat %} + <nat> + {%- if nat.address %} + <address start='{{ nat.address.start }}' end='{{ nat.address.end }}'/> + {%- endif %} + {%- if nat.port %} + <port start='{{ nat.port.start }}' end='{{ nat.port.end }}'/> + {%- endif %} + </nat> +{%- endif %} +{%- for iface in interfaces %} + <interface dev='{{ iface }}'/> +{%- endfor %} +{%- for addr in addresses %} + <address type='pci' domain='0x{{ addr.domain }}' bus='0x{{ addr.bus }}' slot='0x{{ addr.slot }}' function='0x{{ addr.function }}'/> +{%- endfor %} +{%- if pf %} + <pf dev='{{ pf }}'/> +{%- endif %} +{%- if forward %} + </forward> +{%- endif %} +{%- if vport %} + <virtualport type='{{ vport.type }}'> + {%- if vport.parameters %} + <parameters{%- for atr, val in vport.parameters.items() %} {{ atr }}='{{ val }}' {%- endfor %}/> + {%- endif %} + </virtualport> +{%- endif %} +{%- if vlan %} + <vlan{{ opt_attribute(vlan, "trunk", yesno) }}> + {%- for tag in vlan.tags %} + <tag id='{{ tag.id }}'{{ opt_attribute(tag, "nativeMode") }}/> + {%- endfor %} + </vlan> +{%- endif %} +{%- if dns %} + <dns> + {%- for forwarder in dns.forwarders %} + <forwarder{{ opt_attribute(forwarder, "domain") }}{{ opt_attribute(forwarder, "addr") }}/> + {%- endfor %} + {%- for key in dns.txt.keys()|sort %} + <txt name='{{ key }}' value='{{ dns.txt[key] }}'/> + {%- endfor %} + {%- for ip in dns.hosts.keys()|sort %} + <host ip='{{ ip }}'> + {%- for hostname in dns.hosts[ip] %} + <hostname>{{ hostname }}</hostname> + {%- endfor %} + </host> + {%- endfor %} + {%- for srv in dns.srvs %} + <srv service='{{ srv.name }}' protocol='{{ srv.protocol }}' + {{ opt_attribute(srv, "port") }} + {{ opt_attribute(srv, "target") }} + {{ opt_attribute(srv, "priority") }} + {{ opt_attribute(srv, "weight") }} + {{ opt_attribute(srv, "domain") }}/> + {%- endfor %} + </dns> +{%- endif %} +{%- for ip_config in ip_configs %} <ip family='ipv{{ ip_config.address.version }}' - address='{{ ip_config.address.network_address }}' + address='{{ ip_config.address.hosts()|first }}' prefix='{{ ip_config.address.prefixlen }}'> <dhcp> - {% for range in ip_config.dhcp_ranges %} + {%- for range in ip_config.dhcp_ranges %} <range start='{{ range.start }}' end='{{ range.end }}' /> - {% endfor %} + {%- endfor %} + {%- for ip in ip_config.hosts.keys()|sort %} + {%- set host = ip_config.hosts[ip] %} + <host ip='{{ ip }}'{{ opt_attribute(host, 'mac') }}{{ opt_attribute(host, 'id') }}{{ opt_attribute(host, 'name') }}/> + {%- endfor %} + {%- if ip_config.bootp %} + <bootp file='{{ ip_config.bootp.file }}'{{ opt_attribute(ip_config.bootp, "server") }}/> + {%- endif %} </dhcp> + {%- if ip_config.tftp %} + <tftp root='{{ ip_config.tftp }}'/> + {%- endif %} </ip> - {% endfor %} +{%- endfor %} </network> diff --git a/salt/utils/xmlutil.py b/salt/utils/xmlutil.py index 3e760a9c8b..68e2c1c763 100644 --- a/salt/utils/xmlutil.py +++ b/salt/utils/xmlutil.py @@ -380,3 +380,32 @@ def change_xml(doc, data, mapping): deleted = del_fn(parent_map, node) need_update = need_update or deleted return need_update + + +def strip_spaces(node): + """ + Remove all spaces and line breaks before and after nodes. + This helps comparing XML trees. + + :param node: the XML node to remove blanks from + :return: the node + """ + + if node.tail is not None: + node.tail = node.tail.strip(" \t\n") + if node.text is not None: + node.text = node.text.strip(" \t\n") + try: + for child in node: + strip_spaces(child) + except RecursionError: + raise Exception("Failed to recurse on the node") + + return node + + +def element_to_str(node): + """ + Serialize an XML node into a string + """ + return salt.utils.stringutils.to_str(ElementTree.tostring(node)) diff --git a/tests/pytests/unit/modules/virt/conftest.py b/tests/pytests/unit/modules/virt/conftest.py index d4915dc672..b13b9ddaf1 100644 --- a/tests/pytests/unit/modules/virt/conftest.py +++ b/tests/pytests/unit/modules/virt/conftest.py @@ -43,8 +43,8 @@ class MappedResultMock(MagicMock): super().__init__(side_effect=mapped_results) - def add(self, name): - self._instances[name] = MagicMock() + def add(self, name, value=None): + self._instances[name] = value or MagicMock() @pytest.fixture(autouse=True) @@ -95,7 +95,6 @@ def make_mock_vm(): mocked_conn.listDefinedDomains.return_value = [name] # Configure the mocked domain - domain_mock = virt.libvirt.virDomain() if not isinstance(mocked_conn.lookupByName, MappedResultMock): mocked_conn.lookupByName = MappedResultMock() mocked_conn.lookupByName.add(name) @@ -110,7 +109,7 @@ def make_mock_vm(): # Return state as shutdown domain_mock.info.return_value = [ - 4, + 0 if running else 4, 2048 * 1024, 1024 * 1024, 2, @@ -124,6 +123,8 @@ def make_mock_vm(): domain_mock.setMemoryFlags.return_value = 0 domain_mock.setVcpusFlags.return_value = 0 + domain_mock.connect.return_value = mocked_conn + return domain_mock return _make_mock_vm @@ -336,3 +337,66 @@ def make_capabilities(): </capabilities>""" return _make_capabilities + + +@pytest.fixture +def make_mock_network(): + def _make_mock_net(xml_def): + mocked_conn = virt.libvirt.openAuth.return_value + + doc = ET.fromstring(xml_def) + name = doc.find("name").text + + if not isinstance(mocked_conn.networkLookupByName, MappedResultMock): + mocked_conn.networkLookupByName = MappedResultMock() + mocked_conn.networkLookupByName.add(name) + net_mock = mocked_conn.networkLookupByName(name) + net_mock.XMLDesc.return_value = xml_def + + # libvirt defaults the autostart to unset + net_mock.autostart.return_value = 0 + + # Append the network to listAllNetworks return value + all_nets = mocked_conn.listAllNetworks.return_value + if not isinstance(all_nets, list): + all_nets = [] + all_nets.append(net_mock) + mocked_conn.listAllNetworks.return_value = all_nets + + return net_mock + + return _make_mock_net + + +@pytest.fixture +def make_mock_device(): + """ + Create a mock host device + """ + + def _make_mock_device(xml_def): + mocked_conn = virt.libvirt.openAuth.return_value + if not isinstance(mocked_conn.nodeDeviceLookupByName, MappedResultMock): + mocked_conn.nodeDeviceLookupByName = MappedResultMock() + + doc = ET.fromstring(xml_def) + name = doc.find("./name").text + + mocked_conn.nodeDeviceLookupByName.add(name) + mocked_device = mocked_conn.nodeDeviceLookupByName(name) + mocked_device.name.return_value = name + mocked_device.XMLDesc.return_value = xml_def + mocked_device.listCaps.return_value = [ + cap.get("type") for cap in doc.findall("./capability") + ] + return mocked_device + + return _make_mock_device + + +@pytest.fixture(params=[True, False], ids=["test", "notest"]) +def test(request): + """ + Run the test with both True and False test values + """ + return request.param diff --git a/tests/pytests/unit/modules/virt/test_domain.py b/tests/pytests/unit/modules/virt/test_domain.py index 4765fc1bd7..69bcd641b7 100644 --- a/tests/pytests/unit/modules/virt/test_domain.py +++ b/tests/pytests/unit/modules/virt/test_domain.py @@ -1715,3 +1715,466 @@ def test_gen_xml_spice(): assert "listen" not in root.find("devices/graphics").attrib assert root.find("devices/graphics/listen").attrib["type"] == "none" assert "address" not in root.find("devices/graphics/listen").attrib + + +def test_init_hostdev_usb(make_capabilities, make_mock_device): + """ + Test virt.init with USB host device passed through + """ + make_capabilities() + make_mock_device( + """ + <device> + <name>usb_3_1_3</name> + <path>/sys/devices/pci0000:00/0000:00:1d.6/0000:06:00.0/0000:07:02.0/0000:3e:00.0/usb3/3-1/3-1.3</path> + <devnode type='dev'>/dev/bus/usb/003/004</devnode> + <parent>usb_3_1</parent> + <driver> + <name>usb</name> + </driver> + <capability type='usb_device'> + <bus>3</bus> + <device>4</device> + <product id='0x6006'>AUKEY PC-LM1E Camera</product> + <vendor id='0x0458'>KYE Systems Corp. (Mouse Systems)</vendor> + </capability> + </device> + """ + ) + with patch.dict(virt.os.__dict__, {"chmod": MagicMock(), "makedirs": MagicMock()}): + with patch.dict(virt.__salt__, {"cmd.run": MagicMock()}): + virt.init("test_vm", 2, 2048, host_devices=["usb_3_1_3"], start=False) + define_mock = virt.libvirt.openAuth().defineXML + setxml = ET.fromstring(define_mock.call_args[0][0]) + expected_xml = strip_xml( + """ + <hostdev mode='subsystem' type='usb'> + <source> + <vendor id='0x0458'/> + <product id='0x6006'/> + </source> + </hostdev> + """ + ) + assert expected_xml == strip_xml( + ET.tostring(setxml.find("./devices/hostdev")) + ) + + +def test_init_hostdev_pci(make_capabilities, make_mock_device): + """ + Test virt.init with PCI host device passed through + """ + make_capabilities() + make_mock_device( + """ + <device> + <name>pci_1002_71c4</name> + <parent>pci_8086_27a1</parent> + <capability type='pci'> + <class>0xffffff</class> + <domain>0</domain> + <bus>1</bus> + <slot>0</slot> + <function>0</function> + <product id='0x71c4'>M56GL [Mobility FireGL V5200]</product> + <vendor id='0x1002'>ATI Technologies Inc</vendor> + <numa node='1'/> + </capability> + </device> + """ + ) + with patch.dict(virt.os.__dict__, {"chmod": MagicMock(), "makedirs": MagicMock()}): + with patch.dict(virt.__salt__, {"cmd.run": MagicMock()}): + virt.init("test_vm", 2, 2048, host_devices=["pci_1002_71c4"], start=False) + define_mock = virt.libvirt.openAuth().defineXML + setxml = ET.fromstring(define_mock.call_args[0][0]) + expected_xml = strip_xml( + """ + <hostdev mode='subsystem' type='pci' managed='yes'> + <source> + <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> + </source> + </hostdev> + """ + ) + assert expected_xml == strip_xml( + ET.tostring(setxml.find("./devices/hostdev")) + ) + + +def test_update_hostdev_nochange(make_mock_device, make_mock_vm): + """ + Test the virt.update function with no host device changes + """ + 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> + <on_reboot>restart</on_reboot> + <devices> + <hostdev mode='subsystem' type='pci' managed='yes'> + <source> + <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> + </source> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </hostdev> + <hostdev mode='subsystem' type='usb' managed='no'> + <source> + <vendor id='0x0458'/> + <product id='0x6006'/> + <address bus='3' device='4'/> + </source> + <alias name='hostdev0'/> + <address type='usb' bus='0' port='1'/> + </hostdev> + </devices> + </domain>""" + domain_mock = make_mock_vm(xml_def) + + make_mock_device( + """ + <device> + <name>usb_3_1_3</name> + <path>/sys/devices/pci0000:00/0000:00:1d.6/0000:06:00.0/0000:07:02.0/0000:3e:00.0/usb3/3-1/3-1.3</path> + <devnode type='dev'>/dev/bus/usb/003/004</devnode> + <parent>usb_3_1</parent> + <driver> + <name>usb</name> + </driver> + <capability type='usb_device'> + <bus>3</bus> + <device>4</device> + <product id='0x6006'>AUKEY PC-LM1E Camera</product> + <vendor id='0x0458'>KYE Systems Corp. (Mouse Systems)</vendor> + </capability> + </device> + """ + ) + make_mock_device( + """ + <device> + <name>pci_1002_71c4</name> + <parent>pci_8086_27a1</parent> + <capability type='pci'> + <class>0xffffff</class> + <domain>0</domain> + <bus>1</bus> + <slot>0</slot> + <function>0</function> + <product id='0x71c4'>M56GL [Mobility FireGL V5200]</product> + <vendor id='0x1002'>ATI Technologies Inc</vendor> + <numa node='1'/> + </capability> + </device> + """ + ) + + ret = virt.update("my_vm", host_devices=["pci_1002_71c4", "usb_3_1_3"]) + + assert not ret["definition"] + define_mock = virt.libvirt.openAuth().defineXML + define_mock.assert_not_called() + + +@pytest.mark.parametrize( + "running,live", + [(False, False), (True, False), (True, True)], + ids=["stopped, no live", "running, no live", "running, live"], +) +def test_update_hostdev_changes(running, live, make_mock_device, make_mock_vm, test): + """ + Test the virt.update function with host device changes + """ + 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> + <on_reboot>restart</on_reboot> + <devices> + <hostdev mode='subsystem' type='pci' managed='yes'> + <source> + <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> + </source> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </hostdev> + </devices> + </domain>""" + domain_mock = make_mock_vm(xml_def, running) + + make_mock_device( + """ + <device> + <name>usb_3_1_3</name> + <path>/sys/devices/pci0000:00/0000:00:1d.6/0000:06:00.0/0000:07:02.0/0000:3e:00.0/usb3/3-1/3-1.3</path> + <devnode type='dev'>/dev/bus/usb/003/004</devnode> + <parent>usb_3_1</parent> + <driver> + <name>usb</name> + </driver> + <capability type='usb_device'> + <bus>3</bus> + <device>4</device> + <product id='0x6006'>AUKEY PC-LM1E Camera</product> + <vendor id='0x0458'>KYE Systems Corp. (Mouse Systems)</vendor> + </capability> + </device> + """ + ) + + make_mock_device( + """ + <device> + <name>pci_1002_71c4</name> + <parent>pci_8086_27a1</parent> + <capability type='pci'> + <class>0xffffff</class> + <domain>0</domain> + <bus>1</bus> + <slot>0</slot> + <function>0</function> + <product id='0x71c4'>M56GL [Mobility FireGL V5200]</product> + <vendor id='0x1002'>ATI Technologies Inc</vendor> + <numa node='1'/> + </capability> + </device> + """ + ) + + ret = virt.update("my_vm", host_devices=["usb_3_1_3"], test=test, live=live) + define_mock = virt.libvirt.openAuth().defineXML + assert_called(define_mock, not test) + + # Test that the XML is updated with the proper devices + usb_device_xml = strip_xml( + """ + <hostdev mode="subsystem" type="usb"> + <source> + <vendor id="0x0458" /> + <product id="0x6006" /> + </source> + </hostdev> + """ + ) + if not test: + set_xml = ET.fromstring(define_mock.call_args[0][0]) + actual_hostdevs = [ + ET.tostring(xmlutil.strip_spaces(node)) + for node in set_xml.findall("./devices/hostdev") + ] + assert [usb_device_xml] == actual_hostdevs + + if not test and live: + attach_xml = strip_xml(domain_mock.attachDevice.call_args[0][0]) + assert usb_device_xml == attach_xml + + pci_device_xml = strip_xml( + """ + <hostdev mode='subsystem' type='pci' managed='yes'> + <source> + <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/> + </source> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </hostdev> + """ + ) + detach_xml = strip_xml(domain_mock.detachDevice.call_args[0][0]) + assert pci_device_xml == detach_xml + else: + domain_mock.attachDevice.assert_not_called() + domain_mock.detachDevice.assert_not_called() + + +def test_diff_nics(): + """ + Test virt._diff_nics() + """ + old_nics = ET.fromstring( + """ + <devices> + <interface type='network'> + <mac address='52:54:00:39:02:b1'/> + <source network='default'/> + <model type='virtio'/> + <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='admin'/> + <model type='virtio'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </interface> + <interface type='network'> + <mac address='52:54:00:39:02:b3'/> + <source network='admin'/> + <model type='virtio'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </interface> + </devices> + """ + ).findall("interface") + + new_nics = ET.fromstring( + """ + <devices> + <interface type='network'> + <mac address='52:54:00:39:02:b1'/> + <source network='default'/> + <model type='virtio'/> + </interface> + <interface type='network'> + <mac address='52:54:00:39:02:b2'/> + <source network='default'/> + <model type='virtio'/> + </interface> + <interface type='network'> + <mac address='52:54:00:39:02:b4'/> + <source network='admin'/> + <model type='virtio'/> + </interface> + </devices> + """ + ).findall("interface") + ret = virt._diff_interface_lists(old_nics, new_nics) + assert ["52:54:00:39:02:b1"] == [ + nic.find("mac").get("address") for nic in ret["unchanged"] + ] + assert ["52:54:00:39:02:b2", "52:54:00:39:02:b4"] == [ + nic.find("mac").get("address") for nic in ret["new"] + ] + assert ["52:54:00:39:02:b2", "52:54:00:39:02:b3"] == [ + nic.find("mac").get("address") for nic in ret["deleted"] + ] + + +def test_diff_nics_live_nochange(): + """ + Libvirt alters the NICs of network type when running the guest, test the virt._diff_nics() + function with no change in such a case. + """ + old_nics = ET.fromstring( + """ + <devices> + <interface type='direct'> + <mac address='52:54:00:03:02:15'/> + <source network='test-vepa' portid='8377df4f-7c72-45f3-9ba4-a76306333396' dev='eth1' mode='vepa'/> + <target dev='macvtap0'/> + <model type='virtio'/> + <alias name='net0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x0'/> + </interface> + <interface type='bridge'> + <mac address='52:54:00:ea:2e:89'/> + <source network='default' portid='b97ec5b7-25fd-4697-ae45-06af8cc1a964' bridge='br0'/> + <target dev='vnet0'/> + <model type='virtio'/> + <alias name='net0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </interface> + </devices> + """ + ).findall("interface") + + new_nics = ET.fromstring( + """ + <devices> + <interface type='network'> + <source network='test-vepa'/> + <model type='virtio'/> + </interface> + <interface type='network'> + <source network='default'/> + <model type='virtio'/> + </interface> + </devices> + """ + ) + ret = virt._diff_interface_lists(old_nics, new_nics) + assert ["52:54:00:03:02:15", "52:54:00:ea:2e:89"] == [ + nic.find("mac").get("address") for nic in ret["unchanged"] + ] + + +def test_update_nic_hostdev_nochange(make_mock_network, make_mock_vm, test): + """ + Test the virt.update function with a running host with hostdev nic + """ + xml_def_template = """ + <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> + <on_reboot>restart</on_reboot> + <devices> + {} + </devices> + </domain> + """ + inactive_nic = """ + <interface type='hostdev' managed='yes'> + <mac address='52:54:00:67:b2:08'/> + <driver name='vfio'/> + <source network="test-hostdev"/> + <model type='virtio'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </interface> + """ + running_nic = """ + <interface type='hostdev' managed='yes'> + <mac address='52:54:00:67:b2:08'/> + <driver name='vfio'/> + <source> + <address type='pci' domain='0x0000' bus='0x3d' slot='0x02' function='0x0'/> + </source> + <model type='virtio'/> + <alias name='hostdev0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </interface> + """ + domain_mock = make_mock_vm( + xml_def_template.format(running_nic), + running="running", + inactive_def=xml_def_template.format(inactive_nic), + ) + + make_mock_network( + """ + <network connections='1'> + <name>test-hostdev</name> + <uuid>51d0aaa5-7530-4c60-8498-5bc3ab8c655b</uuid> + <forward mode='hostdev' managed='yes'> + <pf dev='eth0'/> + <address type='pci' domain='0x0000' bus='0x3d' slot='0x02' function='0x0'/> + <address type='pci' domain='0x0000' bus='0x3d' slot='0x02' function='0x1'/> + </forward> + </network> + """ + ) + + ret = virt.update( + "my_vm", + interfaces=[{"name": "eth0", "type": "network", "source": "test-hostdev"}], + test=test, + live=True, + ) + assert not ret.get("definition") + assert not ret.get("interface").get("attached") + assert not ret.get("interface").get("detached") + define_mock = virt.libvirt.openAuth().defineXML + define_mock.assert_not_called() + domain_mock.attachDevice.assert_not_called() + domain_mock.detachDevice.assert_not_called() diff --git a/tests/pytests/unit/modules/virt/test_helpers.py b/tests/pytests/unit/modules/virt/test_helpers.py index 01deb4f9cc..977fbb0583 100644 --- a/tests/pytests/unit/modules/virt/test_helpers.py +++ b/tests/pytests/unit/modules/virt/test_helpers.py @@ -1,3 +1,4 @@ +import salt.utils.xmlutil as xmlutil from salt._compat import ElementTree as ET diff --git a/tests/pytests/unit/modules/virt/test_host.py b/tests/pytests/unit/modules/virt/test_host.py new file mode 100644 index 0000000000..555deb23bb --- /dev/null +++ b/tests/pytests/unit/modules/virt/test_host.py @@ -0,0 +1,219 @@ +import pytest +import salt.modules.virt as virt + +from .conftest import loader_modules_config + + +@pytest.fixture +def configure_loader_modules(): + return loader_modules_config() + + +def test_node_devices(make_mock_device): + """ + Test the virt.node_devices() function + """ + mock_devs = [ + make_mock_device( + """ + <device> + <name>pci_1002_71c4</name> + <parent>pci_8086_27a1</parent> + <capability type='pci'> + <class>0xffffff</class> + <domain>0</domain> + <bus>1</bus> + <slot>0</slot> + <function>0</function> + <product id='0x71c4'>M56GL [Mobility FireGL V5200]</product> + <vendor id='0x1002'>ATI Technologies Inc</vendor> + <numa node='1'/> + </capability> + </device> + """ + ), + # Linux USB hub to be ignored + make_mock_device( + """ + <device> + <name>usb_device_1d6b_1_0000_00_1d_0</name> + <parent>pci_8086_27c8</parent> + <capability type='usb_device'> + <bus>2</bus> + <device>1</device> + <product id='0x0001'>1.1 root hub</product> + <vendor id='0x1d6b'>Linux Foundation</vendor> + </capability> + </device> + """ + ), + # SR-IOV PCI device with multiple capabilities + make_mock_device( + """ + <device> + <name>pci_0000_02_10_7</name> + <parent>pci_0000_00_04_0</parent> + <capability type='pci'> + <domain>0</domain> + <bus>2</bus> + <slot>16</slot> + <function>7</function> + <product id='0x10ca'>82576 Virtual Function</product> + <vendor id='0x8086'>Intel Corporation</vendor> + <capability type='phys_function'> + <address domain='0x0000' bus='0x02' slot='0x00' function='0x1'/> + </capability> + <capability type='virt_functions' maxCount='7'> + <address domain='0x0000' bus='0x02' slot='0x00' function='0x2'/> + <address domain='0x0000' bus='0x02' slot='0x00' function='0x3'/> + <address domain='0x0000' bus='0x02' slot='0x00' function='0x4'/> + <address domain='0x0000' bus='0x02' slot='0x00' function='0x5'/> + </capability> + <iommuGroup number='31'> + <address domain='0x0000' bus='0x02' slot='0x10' function='0x7'/> + </iommuGroup> + <numa node='0'/> + <pci-express> + <link validity='cap' port='0' speed='2.5' width='4'/> + <link validity='sta' width='0'/> + </pci-express> + </capability> + </device> + """ + ), + # PCI bridge to be ignored + make_mock_device( + """ + <device> + <name>pci_0000_00_1c_0</name> + <parent>computer</parent> + <capability type='pci'> + <class>0xffffff</class> + <domain>0</domain> + <bus>0</bus> + <slot>28</slot> + <function>0</function> + <product id='0x8c10'>8 Series/C220 Series Chipset Family PCI Express Root Port #1</product> + <vendor id='0x8086'>Intel Corporation</vendor> + <capability type='pci-bridge'/> + <iommuGroup number='8'> + <address domain='0x0000' bus='0x00' slot='0x1c' function='0x0'/> + </iommuGroup> + <pci-express> + <link validity='cap' port='1' speed='5' width='1'/> + <link validity='sta' speed='2.5' width='1'/> + </pci-express> + </capability> + </device> + """ + ), + # Other device to be ignored + make_mock_device( + """ + <device> + <name>mdev_3627463d_b7f0_4fea_b468_f1da537d301b</name> + <parent>computer</parent> + <capability type='mdev'> + <type id='mtty-1'/> + <iommuGroup number='12'/> + </capability> + </device> + """ + ), + # USB device to be listed + make_mock_device( + """ + <device> + <name>usb_3_1_3</name> + <path>/sys/devices/pci0000:00/0000:00:1d.6/0000:06:00.0/0000:07:02.0/0000:3e:00.0/usb3/3-1/3-1.3</path> + <devnode type='dev'>/dev/bus/usb/003/004</devnode> + <parent>usb_3_1</parent> + <driver> + <name>usb</name> + </driver> + <capability type='usb_device'> + <bus>3</bus> + <device>4</device> + <product id='0x6006'>AUKEY PC-LM1E Camera</product> + <vendor id='0x0458'>KYE Systems Corp. (Mouse Systems)</vendor> + </capability> + </device> + """ + ), + # Network device to be listed + make_mock_device( + """ + <device> + <name>net_eth8_e6_86_48_46_c5_29</name> + <path>/sys/devices/pci0000:3a/0000:3a:00.0/0000:3b:00.0/0000:3c:03.0/0000:3d:02.2/net/eth8</path> + <parent>pci_0000_02_10_7</parent> + <capability type='net'> + <interface>eth8</interface> + <address>e6:86:48:46:c5:29</address> + <link state='down'/> + </capability> + </device> + """ + ), + # Network device to be ignored + make_mock_device( + """ + <device> + <name>net_lo_00_00_00_00_00_00</name> + <path>/sys/devices/virtual/net/lo</path> + <parent>computer</parent> + <capability type='net'> + <interface>lo</interface> + <address>00:00:00:00:00:00</address> + <link state='unknown'/> + </capability> + </device> + """ + ), + ] + virt.libvirt.openAuth().listAllDevices.return_value = mock_devs + + assert [ + { + "name": "pci_1002_71c4", + "caps": "pci", + "vendor_id": "0x1002", + "vendor": "ATI Technologies Inc", + "product_id": "0x71c4", + "product": "M56GL [Mobility FireGL V5200]", + "address": "0000:01:00.0", + "PCI class": "0xffffff", + }, + { + "name": "pci_0000_02_10_7", + "caps": "pci", + "vendor_id": "0x8086", + "vendor": "Intel Corporation", + "product_id": "0x10ca", + "product": "82576 Virtual Function", + "address": "0000:02:10.7", + "physical function": "0000:02:00.1", + "virtual functions": [ + "0000:02:00.2", + "0000:02:00.3", + "0000:02:00.4", + "0000:02:00.5", + ], + }, + { + "name": "usb_3_1_3", + "caps": "usb_device", + "vendor": "KYE Systems Corp. (Mouse Systems)", + "vendor_id": "0x0458", + "product": "AUKEY PC-LM1E Camera", + "product_id": "0x6006", + "address": "003:004", + }, + { + "name": "eth8", + "caps": "net", + "address": "e6:86:48:46:c5:29", + "state": "down", + "device name": "pci_0000_02_10_7", + }, + ] == virt.node_devices() diff --git a/tests/pytests/unit/modules/virt/test_network.py b/tests/pytests/unit/modules/virt/test_network.py new file mode 100644 index 0000000000..ce50311dd7 --- /dev/null +++ b/tests/pytests/unit/modules/virt/test_network.py @@ -0,0 +1,455 @@ +import pytest +import salt.modules.virt as virt +import salt.utils.xmlutil as xmlutil +from salt._compat import ElementTree as ET + +from .conftest import loader_modules_config +from .test_helpers import assert_called, assert_xml_equals, strip_xml + + +@pytest.fixture +def configure_loader_modules(): + return loader_modules_config() + + +def test_gen_xml(): + """ + Test virt._get_net_xml() + """ + xml_data = virt._gen_net_xml("network", "main", "bridge", "openvswitch") + root = ET.fromstring(xml_data) + assert "network" == root.find("name").text + assert "main" == root.find("bridge").attrib["name"] + assert "bridge" == root.find("forward").attrib["mode"] + assert "openvswitch" == root.find("virtualport").attrib["type"] + + +def test_gen_xml_nat(): + """ + Test virt._get_net_xml() in a nat setup + """ + xml_data = virt._gen_net_xml( + "network", + "main", + "nat", + None, + ip_configs=[ + { + "cidr": "192.168.2.0/24", + "dhcp_ranges": [ + {"start": "192.168.2.10", "end": "192.168.2.25"}, + {"start": "192.168.2.110", "end": "192.168.2.125"}, + ], + "hosts": { + "192.168.2.10": { + "mac": "00:16:3e:77:e2:ed", + "name": "foo.example.com", + }, + }, + "bootp": {"file": "pxeboot.img", "server": "192.168.2.1"}, + "tftp": "/path/to/tftp", + }, + { + "cidr": "2001:db8:ca2:2::/64", + "hosts": { + "2001:db8:ca2:2:3::1": {"name": "paul"}, + "2001:db8:ca2:2:3::2": { + "id": "0:3:0:1:0:16:3e:11:22:33", + "name": "ralph", + }, + }, + }, + ], + nat={ + "address": {"start": "1.2.3.4", "end": "1.2.3.10"}, + "port": {"start": 500, "end": 1000}, + }, + domain={"name": "acme.lab", "localOnly": True}, + mtu=9000, + ) + root = ET.fromstring(xml_data) + assert "network" == root.find("name").text + assert "main" == root.find("bridge").attrib["name"] + assert "nat" == root.find("forward").attrib["mode"] + expected_ipv4 = ET.fromstring( + """ + <ip family='ipv4' address='192.168.2.1' prefix='24'> + <dhcp> + <range start='192.168.2.10' end='192.168.2.25'/> + <range start='192.168.2.110' end='192.168.2.125'/> + <host ip='192.168.2.10' mac='00:16:3e:77:e2:ed' name='foo.example.com'/> + <bootp file='pxeboot.img' server='192.168.2.1'/> + </dhcp> + <tftp root='/path/to/tftp'/> + </ip> + """ + ) + assert_xml_equals(expected_ipv4, root.find("./ip[@address='192.168.2.1']")) + + expected_ipv6 = ET.fromstring( + """ + <ip family='ipv6' address='2001:db8:ca2:2::1' prefix='64'> + <dhcp> + <host ip='2001:db8:ca2:2:3::1' name='paul'/> + <host ip='2001:db8:ca2:2:3::2' id='0:3:0:1:0:16:3e:11:22:33' name='ralph'/> + </dhcp> + </ip> + """ + ) + assert_xml_equals(expected_ipv6, root.find("./ip[@address='2001:db8:ca2:2::1']")) + + actual_nat = ET.tostring(xmlutil.strip_spaces(root.find("./forward/nat"))) + expected_nat = strip_xml( + """ + <nat> + <address start='1.2.3.4' end='1.2.3.10'/> + <port start='500' end='1000'/> + </nat> + """ + ) + assert expected_nat == actual_nat + + assert {"name": "acme.lab", "localOnly": "yes"} == root.find("./domain").attrib + assert "9000" == root.find("mtu").get("size") + + +def test_gen_xml_dns(): + """ + Test virt._get_net_xml() with DNS configuration + """ + xml_data = virt._gen_net_xml( + "network", + "main", + "nat", + None, + ip_configs=[ + { + "cidr": "192.168.2.0/24", + "dhcp_ranges": [{"start": "192.168.2.10", "end": "192.168.2.25"}], + } + ], + dns={ + "forwarders": [ + {"domain": "example.com", "addr": "192.168.1.1"}, + {"addr": "8.8.8.8"}, + {"domain": "www.example.com"}, + ], + "txt": { + "host.widgets.com.": "printer=lpr5", + "example.com.": "reserved for doc", + }, + "hosts": {"192.168.1.2": ["mirror.acme.lab", "test.acme.lab"]}, + "srvs": [ + { + "name": "srv1", + "protocol": "tcp", + "domain": "test-domain-name", + "target": ".", + "port": 1024, + "priority": 10, + "weight": 10, + }, + {"name": "srv2", "protocol": "udp"}, + ], + }, + ) + root = ET.fromstring(xml_data) + expected_xml = ET.fromstring( + """ + <dns> + <forwarder domain='example.com' addr='192.168.1.1'/> + <forwarder addr='8.8.8.8'/> + <forwarder domain='www.example.com'/> + <txt name='example.com.' value='reserved for doc'/> + <txt name='host.widgets.com.' value='printer=lpr5'/> + <host ip='192.168.1.2'> + <hostname>mirror.acme.lab</hostname> + <hostname>test.acme.lab</hostname> + </host> + <srv service='srv1' protocol='tcp' port='1024' target='.' priority='10' weight='10' domain='test-domain-name'/> + <srv service='srv2' protocol='udp'/> + </dns> + """ + ) + assert_xml_equals(expected_xml, root.find("./dns")) + + +def test_gen_xml_isolated(): + """ + Test the virt._gen_net_xml() function for an isolated network + """ + xml_data = virt._gen_net_xml("network", "main", None, None) + assert ET.fromstring(xml_data).find("forward") is None + + +def test_gen_xml_passthrough_interfaces(): + """ + Test the virt._gen_net_xml() function for a passthrough forward mode + """ + xml_data = virt._gen_net_xml( + "network", "virbr0", "passthrough", None, interfaces="eth10 eth11 eth12", + ) + root = ET.fromstring(xml_data) + assert "passthrough" == root.find("forward").get("mode") + assert ["eth10", "eth11", "eth12"] == [ + n.get("dev") for n in root.findall("forward/interface") + ] + + +def test_gen_xml_hostdev_addresses(): + """ + Test the virt._gen_net_xml() function for a hostdev forward mode with PCI addresses + """ + xml_data = virt._gen_net_xml( + "network", "virbr0", "hostdev", None, addresses="0000:04:00.1 0000:e3:01.2", + ) + root = ET.fromstring(xml_data) + expected_forward = ET.fromstring( + """ + <forward mode='hostdev' managed='yes'> + <address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x1'/> + <address type='pci' domain='0x0000' bus='0xe3' slot='0x01' function='0x2'/> + </forward> + """ + ) + assert_xml_equals(expected_forward, root.find("./forward")) + + +def test_gen_xml_hostdev_pf(): + """ + Test the virt._gen_net_xml() function for a hostdev forward mode with physical function + """ + xml_data = virt._gen_net_xml( + "network", "virbr0", "hostdev", None, physical_function="eth0" + ) + root = ET.fromstring(xml_data) + expected_forward = strip_xml( + """ + <forward mode='hostdev' managed='yes'> + <pf dev='eth0'/> + </forward> + """ + ) + actual_forward = ET.tostring(xmlutil.strip_spaces(root.find("./forward"))) + assert expected_forward == actual_forward + + +def test_gen_xml_openvswitch(): + """ + Test the virt._gen_net_xml() function for an openvswitch setup with virtualport and vlan + """ + xml_data = virt._gen_net_xml( + "network", + "ovsbr0", + "bridge", + { + "type": "openvswitch", + "parameters": {"interfaceid": "09b11c53-8b5c-4eeb-8f00-d84eaa0aaa4f"}, + }, + tag={ + "trunk": True, + "tags": [{"id": 42, "nativeMode": "untagged"}, {"id": 47}], + }, + ) + expected_xml = ET.fromstring( + """ + <network> + <name>network</name> + <bridge name='ovsbr0'/> + <forward mode='bridge'/> + <virtualport type='openvswitch'> + <parameters interfaceid='09b11c53-8b5c-4eeb-8f00-d84eaa0aaa4f'/> + </virtualport> + <vlan trunk='yes'> + <tag id='42' nativeMode='untagged'/> + <tag id='47'/> + </vlan> + </network> + """ + ) + assert_xml_equals(expected_xml, ET.fromstring(xml_data)) + + +@pytest.mark.parametrize( + "autostart, start", [(True, True), (False, True), (False, False)], +) +def test_define(make_mock_network, autostart, start): + """ + Test the virt.defined function + """ + # We create a network mock to fake the autostart flag at start + # and allow checking everything went fine. This doesn't mess up with the network define part + mock_network = make_mock_network("<network><name>default</name></network>") + assert virt.network_define( + "default", + "test-br0", + "nat", + ipv4_config={ + "cidr": "192.168.124.0/24", + "dhcp_ranges": [{"start": "192.168.124.2", "end": "192.168.124.254"}], + }, + autostart=autostart, + start=start, + ) + + expected_xml = strip_xml( + """ + <network> + <name>default</name> + <bridge name='test-br0'/> + <forward mode='nat'/> + <ip family='ipv4' address='192.168.124.1' prefix='24'> + <dhcp> + <range start='192.168.124.2' end='192.168.124.254'/> + </dhcp> + </ip> + </network> + """ + ) + define_mock = virt.libvirt.openAuth().networkDefineXML + assert expected_xml == strip_xml(define_mock.call_args[0][0]) + + if autostart: + mock_network.setAutostart.assert_called_with(1) + else: + mock_network.setAutostart.assert_not_called() + + assert_called(mock_network.create, autostart or start) + + +def test_update_nat_nochange(make_mock_network): + """ + Test updating a NAT network without changes + """ + net_mock = make_mock_network( + """ + <network> + <name>default</name> + <uuid>d6c95a31-16a2-473a-b8cd-7ad2fe2dd855</uuid> + <forward mode='nat'> + <nat> + <port start='1024' end='65535'/> + </nat> + </forward> + <bridge name='virbr0' stp='on' delay='0'/> + <mac address='52:54:00:cd:49:6b'/> + <domain name='my.lab' localOnly='yes'/> + <ip address='192.168.122.1' netmask='255.255.255.0'> + <dhcp> + <range start='192.168.122.2' end='192.168.122.254'/> + <host mac='52:54:00:46:4d:9e' name='mirror' ip='192.168.122.136'/> + <bootp file='pxelinux.0' server='192.168.122.110'/> + </dhcp> + </ip> + </network> + """ + ) + assert not virt.network_update( + "default", + None, + "nat", + ipv4_config={ + "cidr": "192.168.122.0/24", + "dhcp_ranges": [{"start": "192.168.122.2", "end": "192.168.122.254"}], + "hosts": { + "192.168.122.136": {"mac": "52:54:00:46:4d:9e", "name": "mirror"}, + }, + "bootp": {"file": "pxelinux.0", "server": "192.168.122.110"}, + }, + domain={"name": "my.lab", "localOnly": True}, + nat={"port": {"start": 1024, "end": "65535"}}, + ) + define_mock = virt.libvirt.openAuth().networkDefineXML + define_mock.assert_not_called() + + +@pytest.mark.parametrize( + "test, netmask", + [(True, "netmask='255.255.255.0'"), (True, "prefix='24'"), (False, "prefix='24'")], +) +def test_update_nat_change(make_mock_network, test, netmask): + """ + Test updating a NAT network with changes + """ + net_mock = make_mock_network( + """ + <network> + <name>default</name> + <uuid>d6c95a31-16a2-473a-b8cd-7ad2fe2dd855</uuid> + <forward mode='nat'/> + <bridge name='virbr0' stp='on' delay='0'/> + <mac address='52:54:00:cd:49:6b'/> + <domain name='my.lab' localOnly='yes'/> + <ip address='192.168.122.1' {}> + <dhcp> + <range start='192.168.122.2' end='192.168.122.254'/> + </dhcp> + </ip> + </network> + """.format( + netmask + ) + ) + assert virt.network_update( + "default", + "test-br0", + "nat", + ipv4_config={ + "cidr": "192.168.124.0/24", + "dhcp_ranges": [{"start": "192.168.124.2", "end": "192.168.124.254"}], + }, + test=test, + ) + define_mock = virt.libvirt.openAuth().networkDefineXML + assert_called(define_mock, not test) + + if not test: + # Test the passed new XML + expected_xml = strip_xml( + """ + <network> + <name>default</name> + <mac address='52:54:00:cd:49:6b'/> + <uuid>d6c95a31-16a2-473a-b8cd-7ad2fe2dd855</uuid> + <bridge name='test-br0'/> + <forward mode='nat'/> + <ip family='ipv4' address='192.168.124.1' prefix='24'> + <dhcp> + <range start='192.168.124.2' end='192.168.124.254'/> + </dhcp> + </ip> + </network> + """ + ) + assert expected_xml == strip_xml(define_mock.call_args[0][0]) + + +@pytest.mark.parametrize("change", [True, False], ids=["changed", "unchanged"]) +def test_update_hostdev_pf(make_mock_network, change): + """ + Test updating a hostdev network without changes + """ + net_mock = make_mock_network( + """ + <network connections='1'> + <name>test-hostdev</name> + <uuid>51d0aaa5-7530-4c60-8498-5bc3ab8c655b</uuid> + <forward mode='hostdev' managed='yes'> + <pf dev='eth0'/> + <address type='pci' domain='0x0000' bus='0x3d' slot='0x02' function='0x0'/> + <address type='pci' domain='0x0000' bus='0x3d' slot='0x02' function='0x1'/> + </forward> + </network> + """ + ) + assert change == virt.network_update( + "test-hostdev", + None, + "hostdev", + physical_function="eth0" if not change else "eth1", + ) + define_mock = virt.libvirt.openAuth().networkDefineXML + if change: + define_mock.assert_called() + else: + define_mock.assert_not_called() diff --git a/tests/pytests/unit/states/virt/__init__.py b/tests/pytests/unit/states/virt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/pytests/unit/states/virt/conftest.py b/tests/pytests/unit/states/virt/conftest.py new file mode 100644 index 0000000000..cc975fddbf --- /dev/null +++ b/tests/pytests/unit/states/virt/conftest.py @@ -0,0 +1,36 @@ +import pytest +import salt.states.virt as virt +from tests.support.mock import MagicMock + + +class LibvirtMock(MagicMock): # pylint: disable=too-many-ancestors + """ + Libvirt library mock + """ + + class libvirtError(Exception): + """ + libvirtError mock + """ + + def __init__(self, msg): + super().__init__(msg) + self.msg = msg + + def get_error_message(self): + return self.msg + + +@pytest.fixture(autouse=True) +def setup_loader(): + setup_loader_modules = {virt: {"libvirt": LibvirtMock()}} + with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: + yield loader_mock + + +@pytest.fixture(params=[True, False], ids=["test", "notest"]) +def test(request): + """ + Run the test with both True and False test values + """ + return request.param diff --git a/tests/pytests/unit/states/virt/test_domain.py b/tests/pytests/unit/states/virt/test_domain.py new file mode 100644 index 0000000000..a4ae8c0694 --- /dev/null +++ b/tests/pytests/unit/states/virt/test_domain.py @@ -0,0 +1,840 @@ +import pytest +import salt.states.virt as virt +from salt.exceptions import CommandExecutionError +from tests.support.mock import MagicMock, patch + +from .test_helpers import domain_update_call + + +def test_defined_no_change(test): + """ + defined state test, no change required case. + """ + with patch.dict(virt.__opts__, {"test": test}): + init_mock = MagicMock(return_value=True) + update_mock = MagicMock(return_value={"definition": False}) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm"]), + "virt.update": update_mock, + "virt.init": init_mock, + }, + ): + assert { + "name": "myvm", + "changes": {"myvm": {"definition": False}}, + "result": True, + "comment": "Domain myvm unchanged", + } == virt.defined("myvm") + init_mock.assert_not_called() + assert [domain_update_call("myvm", test=test)] == update_mock.call_args_list + + +def test_defined_new_with_connection(test): + """ + defined state test, new guest with connection details passed case. + """ + with patch.dict(virt.__opts__, {"test": test}): + init_mock = MagicMock(return_value=True) + update_mock = MagicMock(side_effect=CommandExecutionError("not found")) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=[]), + "virt.init": init_mock, + "virt.update": update_mock, + }, + ): + disks = [ + { + "name": "system", + "size": 8192, + "overlay_image": True, + "pool": "default", + "image": "/path/to/image.qcow2", + }, + {"name": "data", "size": 16834}, + ] + ifaces = [ + {"name": "eth0", "mac": "01:23:45:67:89:AB"}, + {"name": "eth1", "type": "network", "source": "admin"}, + ] + graphics = { + "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"}, + ] + assert { + "name": "myvm", + "result": True if not test else None, + "changes": {"myvm": {"definition": True}}, + "comment": "Domain myvm defined", + } == virt.defined( + "myvm", + cpu=2, + mem=2048, + boot_dev="cdrom hd", + os_type="linux", + arch="i686", + vm_type="qemu", + disk_profile="prod", + disks=disks, + nic_profile="prod", + interfaces=ifaces, + graphics=graphics, + seed=False, + 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, + host_devices=["pci_0000_00_17_0"], + ) + if not test: + init_mock.assert_called_with( + "myvm", + cpu=2, + mem=2048, + boot_dev="cdrom hd", + os_type="linux", + arch="i686", + disk="prod", + disks=disks, + nic="prod", + interfaces=ifaces, + graphics=graphics, + 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, + host_devices=["pci_0000_00_17_0"], + ) + else: + init_mock.assert_not_called() + update_mock.assert_not_called() + + +def test_defined_update(test): + """ + defined state test, with change required case. + """ + with patch.dict(virt.__opts__, {"test": test}): + init_mock = MagicMock(return_value=True) + update_mock = MagicMock(return_value={"definition": True, "cpu": True}) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm"]), + "virt.update": update_mock, + "virt.init": init_mock, + }, + ): + boot = { + "kernel": "/root/f8-i386-vmlinuz", + "initrd": "/root/f8-i386-initrd", + "cmdline": "console=ttyS0 ks=http://example.com/f8-i386/os/", + } + assert { + "name": "myvm", + "changes": {"myvm": {"definition": True, "cpu": True}}, + "result": True if not test else None, + "comment": "Domain myvm updated", + } == virt.defined("myvm", cpu=2, boot=boot,) + init_mock.assert_not_called() + assert [ + domain_update_call("myvm", cpu=2, test=test, boot=boot) + ] == update_mock.call_args_list + + +def test_defined_update_error(test): + """ + defined state test, with error during the update. + """ + with patch.dict(virt.__opts__, {"test": test}): + init_mock = MagicMock(return_value=True) + update_mock = MagicMock( + return_value={"definition": True, "cpu": False, "errors": ["some error"]} + ) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm"]), + "virt.update": update_mock, + "virt.init": init_mock, + }, + ): + assert { + "name": "myvm", + "changes": { + "myvm": { + "definition": True, + "cpu": False, + "errors": ["some error"], + } + }, + "result": True if not test else None, + "comment": "Domain myvm updated with live update(s) failures", + } == virt.defined("myvm", cpu=2, boot_dev="cdrom hd") + init_mock.assert_not_called() + update_mock.assert_called_with( + "myvm", + cpu=2, + boot_dev="cdrom hd", + mem=None, + disk_profile=None, + disks=None, + nic_profile=None, + interfaces=None, + graphics=None, + live=True, + connection=None, + username=None, + password=None, + boot=None, + numatune=None, + test=test, + hypervisor_features=None, + clock=None, + serials=None, + consoles=None, + stop_on_reboot=False, + host_devices=None, + ) + + +def test_defined_update_definition_error(test): + """ + defined state test, with definition update failure + """ + with patch.dict(virt.__opts__, {"test": test}): + init_mock = MagicMock(return_value=True) + update_mock = MagicMock( + side_effect=[virt.libvirt.libvirtError("error message")] + ) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm"]), + "virt.update": update_mock, + "virt.init": init_mock, + }, + ): + assert { + "name": "myvm", + "changes": {}, + "result": False, + "comment": "error message", + } == virt.defined("myvm", cpu=2) + init_mock.assert_not_called() + assert [ + domain_update_call("myvm", cpu=2, test=test) + ] == update_mock.call_args_list + + +@pytest.mark.parametrize("running", ["running", "shutdown"]) +def test_running_no_change(test, running): + """ + running state test, no change required case. + """ + with patch.dict(virt.__opts__, {"test": test}): + update_mock = MagicMock(return_value={"definition": False}) + start_mock = MagicMock(return_value=0) + with patch.dict( + virt.__salt__, + { + "virt.vm_state": MagicMock(return_value={"myvm": running}), + "virt.start": start_mock, + "virt.update": MagicMock(return_value={"definition": False}), + "virt.list_domains": MagicMock(return_value=["myvm"]), + }, + ): + changes = {"definition": False} + comment = "Domain myvm exists and is running" + if running == "shutdown": + changes["started"] = True + comment = "Domain myvm started" + assert { + "name": "myvm", + "result": True, + "changes": {"myvm": changes}, + "comment": comment, + } == virt.running("myvm") + if running == "shutdown" and not test: + start_mock.assert_called() + else: + start_mock.assert_not_called() + + +def test_running_define(test): + """ + running state test, defining and start a guest the old way + """ + with patch.dict(virt.__opts__, {"test": test}): + init_mock = MagicMock(return_value=True) + start_mock = MagicMock(return_value=0) + with patch.dict( + virt.__salt__, + { + "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), + "virt.init": init_mock, + "virt.start": start_mock, + "virt.list_domains": MagicMock(return_value=[]), + }, + ): + disks = [ + { + "name": "system", + "size": 8192, + "overlay_image": True, + "pool": "default", + "image": "/path/to/image.qcow2", + }, + {"name": "data", "size": 16834}, + ] + ifaces = [ + {"name": "eth0", "mac": "01:23:45:67:89:AB"}, + {"name": "eth1", "type": "network", "source": "admin"}, + ] + graphics = { + "type": "spice", + "listen": {"type": "address", "address": "192.168.0.1"}, + } + + assert { + "name": "myvm", + "result": True if not test else None, + "changes": {"myvm": {"definition": True, "started": True}}, + "comment": "Domain myvm defined and started", + } == virt.running( + "myvm", + cpu=2, + mem=2048, + os_type="linux", + arch="i686", + vm_type="qemu", + disk_profile="prod", + disks=disks, + nic_profile="prod", + interfaces=ifaces, + graphics=graphics, + seed=False, + install=False, + pub_key="/path/to/key.pub", + priv_key="/path/to/key", + boot_dev="network hd", + stop_on_reboot=True, + host_devices=["pci_0000_00_17_0"], + connection="someconnection", + username="libvirtuser", + password="supersecret", + ) + if not test: + init_mock.assert_called_with( + "myvm", + cpu=2, + mem=2048, + os_type="linux", + arch="i686", + disk="prod", + disks=disks, + nic="prod", + interfaces=ifaces, + graphics=graphics, + 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, + host_devices=["pci_0000_00_17_0"], + ) + start_mock.assert_called_with( + "myvm", + connection="someconnection", + username="libvirtuser", + password="supersecret", + ) + else: + init_mock.assert_not_called() + start_mock.assert_not_called() + + +def test_running_start_error(): + """ + running state test, start an existing guest raising an error + """ + with patch.dict(virt.__opts__, {"test": False}): + with patch.dict( + virt.__salt__, + { + "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), + "virt.update": MagicMock(return_value={"definition": False}), + "virt.start": MagicMock( + side_effect=[virt.libvirt.libvirtError("libvirt error msg")] + ), + "virt.list_domains": MagicMock(return_value=["myvm"]), + }, + ): + assert { + "name": "myvm", + "changes": {"myvm": {"definition": False}}, + "result": False, + "comment": "libvirt error msg", + } == virt.running("myvm") + + +@pytest.mark.parametrize("running", ["running", "shutdown"]) +def test_running_update(test, running): + """ + running state test, update an existing guest + """ + with patch.dict(virt.__opts__, {"test": test}): + start_mock = MagicMock(return_value=0) + with patch.dict( + virt.__salt__, + { + "virt.vm_state": MagicMock(return_value={"myvm": running}), + "virt.update": MagicMock( + return_value={"definition": True, "cpu": True} + ), + "virt.start": start_mock, + "virt.list_domains": MagicMock(return_value=["myvm"]), + }, + ): + changes = {"myvm": {"definition": True, "cpu": True}} + if running == "shutdown": + changes["myvm"]["started"] = True + assert { + "name": "myvm", + "changes": changes, + "result": True if not test else None, + "comment": "Domain myvm updated" + if running == "running" + else "Domain myvm updated and started", + } == virt.running("myvm", cpu=2) + if running == "shutdown" and not test: + start_mock.assert_called() + else: + start_mock.assert_not_called() + + +def test_running_definition_error(): + """ + running state test, update an existing guest raising an error when setting the XML + """ + with patch.dict(virt.__opts__, {"test": False}): + with patch.dict( + virt.__salt__, + { + "virt.vm_state": MagicMock(return_value={"myvm": "running"}), + "virt.update": MagicMock( + side_effect=[virt.libvirt.libvirtError("error message")] + ), + "virt.list_domains": MagicMock(return_value=["myvm"]), + }, + ): + assert { + "name": "myvm", + "changes": {}, + "result": False, + "comment": "error message", + } == virt.running("myvm", cpu=3) + + +def test_running_update_error(): + """ + running state test, update an existing guest raising an error + """ + with patch.dict(virt.__opts__, {"test": False}): + update_mock = MagicMock( + return_value={"definition": True, "cpu": False, "errors": ["some error"]} + ) + with patch.dict( + virt.__salt__, + { + "virt.vm_state": MagicMock(return_value={"myvm": "running"}), + "virt.update": update_mock, + "virt.list_domains": MagicMock(return_value=["myvm"]), + }, + ): + assert { + "name": "myvm", + "changes": { + "myvm": { + "definition": True, + "cpu": False, + "errors": ["some error"], + } + }, + "result": True, + "comment": "Domain myvm updated with live update(s) failures", + } == virt.running("myvm", cpu=2) + update_mock.assert_called_with( + "myvm", + cpu=2, + mem=None, + disk_profile=None, + disks=None, + nic_profile=None, + interfaces=None, + graphics=None, + live=True, + connection=None, + 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, + host_devices=None, + ) + + +@pytest.mark.parametrize("running", ["running", "shutdown"]) +def test_stopped(test, running): + """ + stopped state test, running guest + """ + with patch.dict(virt.__opts__, {"test": test}): + shutdown_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), + "virt.vm_state": MagicMock(return_value={"myvm": running}), + "virt.shutdown": shutdown_mock, + }, + ): + changes = {} + comment = "No changes had happened" + if running == "running": + changes = {"stopped": [{"domain": "myvm", "shutdown": True}]} + comment = "Machine has been shut down" + assert { + "name": "myvm", + "changes": changes, + "comment": comment, + "result": True if not test or running == "shutdown" else None, + } == virt.stopped( + "myvm", connection="myconnection", username="user", password="secret", + ) + if not test and running == "running": + shutdown_mock.assert_called_with( + "myvm", + connection="myconnection", + username="user", + password="secret", + ) + else: + shutdown_mock.assert_not_called() + + +def test_stopped_error(): + """ + stopped state test, error while stopping guest + """ + with patch.dict(virt.__opts__, {"test": False}): + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), + "virt.vm_state": MagicMock(return_value={"myvm": "running"}), + "virt.shutdown": MagicMock( + side_effect=virt.libvirt.libvirtError("Some error") + ), + }, + ): + assert { + "name": "myvm", + "changes": {"ignored": [{"domain": "myvm", "issue": "Some error"}]}, + "result": False, + "comment": "No changes had happened", + } == virt.stopped("myvm") + + +def test_stopped_not_existing(test): + """ + stopped state test, non existing guest + """ + with patch.dict(virt.__opts__, {"test": test}): + shutdown_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, {"virt.list_domains": MagicMock(return_value=[])}, + ): + assert { + "name": "myvm", + "changes": {}, + "comment": "No changes had happened", + "result": False, + } == virt.stopped("myvm") + + +@pytest.mark.parametrize("running", ["running", "shutdown"]) +def test_powered_off(test, running): + """ + powered_off state test + """ + with patch.dict(virt.__opts__, {"test": test}): + stop_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), + "virt.vm_state": MagicMock(return_value={"myvm": running}), + "virt.stop": stop_mock, + }, + ): + changes = {} + comment = "No changes had happened" + if running == "running": + changes = {"unpowered": [{"domain": "myvm", "stop": True}]} + comment = "Machine has been powered off" + assert { + "name": "myvm", + "result": True if not test or running == "shutdown" else None, + "changes": changes, + "comment": comment, + } == virt.powered_off( + "myvm", connection="myconnection", username="user", password="secret", + ) + if not test and running == "running": + stop_mock.assert_called_with( + "myvm", + connection="myconnection", + username="user", + password="secret", + ) + else: + stop_mock.assert_not_called() + + +def test_powered_off_error(): + """ + powered_off state test, error case + """ + with patch.dict(virt.__opts__, {"test": False}): + stop_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), + "virt.vm_state": MagicMock(return_value={"myvm": "running"}), + "virt.stop": MagicMock( + side_effect=virt.libvirt.libvirtError("Some error") + ), + }, + ): + assert { + "name": "myvm", + "result": False, + "changes": {"ignored": [{"domain": "myvm", "issue": "Some error"}]}, + "comment": "No changes had happened", + } == virt.powered_off("myvm") + + +def test_powered_off_not_existing(): + """ + powered_off state test cases. + """ + ret = {"name": "myvm", "changes": {}, "result": True} + with patch.dict(virt.__opts__, {"test": False}): + with patch.dict( + virt.__salt__, {"virt.list_domains": MagicMock(return_value=[])} + ): # pylint: disable=no-member + ret.update( + {"changes": {}, "result": False, "comment": "No changes had happened"} + ) + assert { + "name": "myvm", + "changes": {}, + "result": False, + "comment": "No changes had happened", + } == virt.powered_off("myvm") + + +def test_snapshot(test): + """ + snapshot state test + """ + with patch.dict(virt.__opts__, {"test": test}): + snapshot_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), + "virt.snapshot": snapshot_mock, + }, + ): + assert { + "name": "myvm", + "result": True if not test else None, + "changes": {"saved": [{"domain": "myvm", "snapshot": True}]}, + "comment": "Snapshot has been taken", + } == virt.snapshot( + "myvm", + suffix="snap", + connection="myconnection", + username="user", + password="secret", + ) + if not test: + snapshot_mock.assert_called_with( + "myvm", + suffix="snap", + connection="myconnection", + username="user", + password="secret", + ) + else: + snapshot_mock.assert_not_called() + + +def test_snapshot_error(): + """ + snapshot state test, error case + """ + with patch.dict(virt.__opts__, {"test": False}): + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), + "virt.snapshot": MagicMock( + side_effect=virt.libvirt.libvirtError("Some error") + ), + }, + ): + assert { + "name": "myvm", + "result": False, + "changes": {"ignored": [{"domain": "myvm", "issue": "Some error"}]}, + "comment": "No changes had happened", + } == virt.snapshot("myvm") + + +def test_snapshot_not_existing(test): + """ + snapshot state test, guest not existing. + """ + with patch.dict(virt.__opts__, {"test": test}): + with patch.dict( + virt.__salt__, {"virt.list_domains": MagicMock(return_value=[])} + ): + assert { + "name": "myvm", + "changes": {}, + "result": False, + "comment": "No changes had happened", + } == virt.snapshot("myvm") + + +def test_rebooted(test): + """ + rebooted state test + """ + with patch.dict(virt.__opts__, {"test": test}): + reboot_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), + "virt.reboot": reboot_mock, + }, + ): + assert { + "name": "myvm", + "result": True if not test else None, + "changes": {"rebooted": [{"domain": "myvm", "reboot": True}]}, + "comment": "Machine has been rebooted", + } == virt.rebooted( + "myvm", connection="myconnection", username="user", password="secret", + ) + if not test: + reboot_mock.assert_called_with( + "myvm", + connection="myconnection", + username="user", + password="secret", + ) + else: + reboot_mock.assert_not_called() + + +def test_rebooted_error(): + """ + rebooted state test, error case. + """ + with patch.dict(virt.__opts__, {"test": False}): + reboot_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), + "virt.reboot": MagicMock( + side_effect=virt.libvirt.libvirtError("Some error") + ), + }, + ): + assert { + "name": "myvm", + "result": False, + "changes": {"ignored": [{"domain": "myvm", "issue": "Some error"}]}, + "comment": "No changes had happened", + } == virt.rebooted("myvm") + + +def test_rebooted_not_existing(test): + """ + rebooted state test cases. + """ + with patch.dict(virt.__opts__, {"test": test}): + with patch.dict( + virt.__salt__, {"virt.list_domains": MagicMock(return_value=[])} + ): + assert { + "name": "myvm", + "changes": {}, + "result": False, + "comment": "No changes had happened", + } == virt.rebooted("myvm") diff --git a/tests/pytests/unit/states/virt/test_helpers.py b/tests/pytests/unit/states/virt/test_helpers.py new file mode 100644 index 0000000000..b8e2cb06e2 --- /dev/null +++ b/tests/pytests/unit/states/virt/test_helpers.py @@ -0,0 +1,99 @@ +from tests.support.mock import call + + +def network_update_call( + name, + bridge, + forward, + vport=None, + tag=None, + ipv4_config=None, + ipv6_config=None, + connection=None, + username=None, + password=None, + mtu=None, + domain=None, + nat=None, + interfaces=None, + addresses=None, + physical_function=None, + dns=None, + test=False, +): + """ + Create a call object with the missing default parameters from virt.network_update() + """ + return call( + name, + bridge, + forward, + vport=vport, + tag=tag, + ipv4_config=ipv4_config, + ipv6_config=ipv6_config, + mtu=mtu, + domain=domain, + nat=nat, + interfaces=interfaces, + addresses=addresses, + physical_function=physical_function, + dns=dns, + test=test, + connection=connection, + username=username, + password=password, + ) + + +def domain_update_call( + name, + cpu=None, + mem=None, + disk_profile=None, + disks=None, + nic_profile=None, + interfaces=None, + graphics=None, + connection=None, + username=None, + password=None, + boot=None, + numatune=None, + boot_dev=None, + hypervisor_features=None, + clock=None, + serials=None, + consoles=None, + stop_on_reboot=False, + live=True, + host_devices=None, + test=False, +): + """ + Create a call object with the missing default parameters from virt.update() + """ + return call( + name, + cpu=cpu, + mem=mem, + disk_profile=disk_profile, + disks=disks, + nic_profile=nic_profile, + interfaces=interfaces, + graphics=graphics, + live=live, + connection=connection, + username=username, + password=password, + boot=boot, + numatune=numatune, + serials=serials, + consoles=consoles, + test=test, + boot_dev=boot_dev, + hypervisor_features=hypervisor_features, + clock=clock, + stop_on_reboot=stop_on_reboot, + host_devices=host_devices, + ) diff --git a/tests/pytests/unit/states/virt/test_network.py b/tests/pytests/unit/states/virt/test_network.py new file mode 100644 index 0000000000..668eee0c64 --- /dev/null +++ b/tests/pytests/unit/states/virt/test_network.py @@ -0,0 +1,476 @@ +import salt.states.virt as virt +from tests.support.mock import MagicMock, patch + +from .test_helpers import network_update_call + + +def test_network_defined_not_existing(test): + """ + network_defined state tests if the network doesn't exist yet. + """ + with patch.dict(virt.__opts__, {"test": test}): + define_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.network_info": MagicMock( + side_effect=[{}, {"mynet": {"active": False}}] + ), + "virt.network_define": define_mock, + }, + ): + assert { + "name": "mynet", + "changes": {"mynet": "Network defined"}, + "result": None if test else True, + "comment": "Network mynet defined", + } == virt.network_defined( + "mynet", + "br2", + "bridge", + vport="openvswitch", + tag=180, + ipv4_config={ + "cidr": "192.168.2.0/24", + "dhcp_ranges": [ + {"start": "192.168.2.10", "end": "192.168.2.25"}, + {"start": "192.168.2.110", "end": "192.168.2.125"}, + ], + }, + ipv6_config={ + "cidr": "2001:db8:ca2:2::1/64", + "dhcp_ranges": [ + {"start": "2001:db8:ca2:1::10", "end": "2001:db8:ca2::1f"}, + ], + }, + mtu=9000, + domain={"name": "acme.lab"}, + nat={"ports": {"start": 1024, "end": 2048}}, + interfaces="eth0 eth1", + addresses="0000:01:02.4 0000:01:02.5", + physical_function="eth4", + dns={ + "hosts": { + "192.168.2.10": {"name": "web", "mac": "de:ad:be:ef:00:00"} + } + }, + autostart=False, + connection="myconnection", + username="user", + password="secret", + ) + if not test: + define_mock.assert_called_with( + "mynet", + "br2", + "bridge", + vport="openvswitch", + tag=180, + autostart=False, + start=False, + ipv4_config={ + "cidr": "192.168.2.0/24", + "dhcp_ranges": [ + {"start": "192.168.2.10", "end": "192.168.2.25"}, + {"start": "192.168.2.110", "end": "192.168.2.125"}, + ], + }, + ipv6_config={ + "cidr": "2001:db8:ca2:2::1/64", + "dhcp_ranges": [ + {"start": "2001:db8:ca2:1::10", "end": "2001:db8:ca2::1f"}, + ], + }, + mtu=9000, + domain={"name": "acme.lab"}, + nat={"ports": {"start": 1024, "end": 2048}}, + interfaces="eth0 eth1", + addresses="0000:01:02.4 0000:01:02.5", + physical_function="eth4", + dns={ + "hosts": { + "192.168.2.10": {"name": "web", "mac": "de:ad:be:ef:00:00"} + } + }, + connection="myconnection", + username="user", + password="secret", + ) + else: + define_mock.assert_not_called() + + +def test_network_defined_no_change(test): + """ + network_defined state tests if the network doesn't need update. + """ + with patch.dict(virt.__opts__, {"test": test}): + define_mock = MagicMock(return_value=True) + update_mock = MagicMock(return_value=False) + with patch.dict( + virt.__salt__, + { + "virt.network_info": MagicMock( + return_value={"mynet": {"active": True, "autostart": True}} + ), + "virt.network_define": define_mock, + "virt.network_update": update_mock, + }, + ): + assert { + "name": "mynet", + "changes": {}, + "result": True, + "comment": "Network mynet unchanged", + } == virt.network_defined("mynet", "br2", "bridge") + define_mock.assert_not_called() + assert [ + network_update_call("mynet", "br2", "bridge", test=True) + ] == update_mock.call_args_list + + +def test_network_defined_change(test): + """ + network_defined state tests if the network needs update. + """ + with patch.dict(virt.__opts__, {"test": test}): + define_mock = MagicMock(return_value=True) + update_mock = MagicMock(return_value=True) + autostart_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.network_info": MagicMock( + return_value={"mynet": {"active": True, "autostart": True}} + ), + "virt.network_define": define_mock, + "virt.network_update": update_mock, + "virt.network_set_autostart": autostart_mock, + }, + ): + assert { + "name": "mynet", + "changes": {"mynet": "Network updated, autostart flag changed"}, + "result": None if test else True, + "comment": "Network mynet updated, autostart flag changed", + } == virt.network_defined( + "mynet", + "br2", + "bridge", + vport="openvswitch", + tag=180, + ipv4_config={ + "cidr": "192.168.2.0/24", + "dhcp_ranges": [ + {"start": "192.168.2.10", "end": "192.168.2.25"}, + {"start": "192.168.2.110", "end": "192.168.2.125"}, + ], + }, + ipv6_config={ + "cidr": "2001:db8:ca2:2::1/64", + "dhcp_ranges": [ + {"start": "2001:db8:ca2:1::10", "end": "2001:db8:ca2::1f"}, + ], + }, + mtu=9000, + domain={"name": "acme.lab"}, + nat={"ports": {"start": 1024, "end": 2048}}, + interfaces="eth0 eth1", + addresses="0000:01:02.4 0000:01:02.5", + physical_function="eth4", + dns={ + "hosts": { + "192.168.2.10": {"name": "web", "mac": "de:ad:be:ef:00:00"} + } + }, + autostart=False, + connection="myconnection", + username="user", + password="secret", + ) + define_mock.assert_not_called() + expected_update_kwargs = { + "vport": "openvswitch", + "tag": 180, + "ipv4_config": { + "cidr": "192.168.2.0/24", + "dhcp_ranges": [ + {"start": "192.168.2.10", "end": "192.168.2.25"}, + {"start": "192.168.2.110", "end": "192.168.2.125"}, + ], + }, + "ipv6_config": { + "cidr": "2001:db8:ca2:2::1/64", + "dhcp_ranges": [ + {"start": "2001:db8:ca2:1::10", "end": "2001:db8:ca2::1f"}, + ], + }, + "mtu": 9000, + "domain": {"name": "acme.lab"}, + "nat": {"ports": {"start": 1024, "end": 2048}}, + "interfaces": "eth0 eth1", + "addresses": "0000:01:02.4 0000:01:02.5", + "physical_function": "eth4", + "dns": { + "hosts": { + "192.168.2.10": {"name": "web", "mac": "de:ad:be:ef:00:00"} + } + }, + "connection": "myconnection", + "username": "user", + "password": "secret", + } + calls = [ + network_update_call( + "mynet", "br2", "bridge", **expected_update_kwargs, test=True + ) + ] + if test: + assert calls == update_mock.call_args_list + autostart_mock.assert_not_called() + else: + calls.append( + network_update_call( + "mynet", "br2", "bridge", **expected_update_kwargs, test=False + ) + ) + assert calls == update_mock.call_args_list + autostart_mock.assert_called_with( + "mynet", + state="off", + connection="myconnection", + username="user", + password="secret", + ) + + +def test_network_defined_error(test): + """ + network_defined state tests if an error is triggered by libvirt. + """ + with patch.dict(virt.__opts__, {"test": test}): + define_mock = MagicMock(return_value=True) + with patch.dict( + virt.__salt__, + { + "virt.network_info": MagicMock( + side_effect=virt.libvirt.libvirtError("Some error") + ) + }, + ): + assert { + "name": "mynet", + "changes": {}, + "result": False, + "comment": "Some error", + } == virt.network_defined("mynet", "br2", "bridge") + define_mock.assert_not_called() + + +def test_network_running_not_existing(test): + """ + network_running state test cases, non-existing network case. + """ + with patch.dict(virt.__opts__, {"test": test}): + define_mock = MagicMock(return_value=True) + start_mock = MagicMock(return_value=True) + # Non-existing network case + with patch.dict( + virt.__salt__, + { + "virt.network_info": MagicMock( + side_effect=[{}, {"mynet": {"active": False}}] + ), + "virt.network_define": define_mock, + "virt.network_start": start_mock, + }, + ): + assert { + "name": "mynet", + "changes": {"mynet": "Network defined and started"}, + "comment": "Network mynet defined and started", + "result": None if test else True, + } == virt.network_running( + "mynet", + "br2", + "bridge", + vport="openvswitch", + tag=180, + ipv4_config={ + "cidr": "192.168.2.0/24", + "dhcp_ranges": [ + {"start": "192.168.2.10", "end": "192.168.2.25"}, + {"start": "192.168.2.110", "end": "192.168.2.125"}, + ], + }, + ipv6_config={ + "cidr": "2001:db8:ca2:2::1/64", + "dhcp_ranges": [ + {"start": "2001:db8:ca2:1::10", "end": "2001:db8:ca2::1f"}, + ], + }, + mtu=9000, + domain={"name": "acme.lab"}, + nat={"ports": {"start": 1024, "end": 2048}}, + interfaces="eth0 eth1", + addresses="0000:01:02.4 0000:01:02.5", + physical_function="eth4", + dns={ + "hosts": { + "192.168.2.10": {"name": "web", "mac": "de:ad:be:ef:00:00"} + } + }, + autostart=False, + connection="myconnection", + username="user", + password="secret", + ) + if not test: + define_mock.assert_called_with( + "mynet", + "br2", + "bridge", + vport="openvswitch", + tag=180, + autostart=False, + start=False, + ipv4_config={ + "cidr": "192.168.2.0/24", + "dhcp_ranges": [ + {"start": "192.168.2.10", "end": "192.168.2.25"}, + {"start": "192.168.2.110", "end": "192.168.2.125"}, + ], + }, + ipv6_config={ + "cidr": "2001:db8:ca2:2::1/64", + "dhcp_ranges": [ + {"start": "2001:db8:ca2:1::10", "end": "2001:db8:ca2::1f"}, + ], + }, + mtu=9000, + domain={"name": "acme.lab"}, + nat={"ports": {"start": 1024, "end": 2048}}, + interfaces="eth0 eth1", + addresses="0000:01:02.4 0000:01:02.5", + physical_function="eth4", + dns={ + "hosts": { + "192.168.2.10": {"name": "web", "mac": "de:ad:be:ef:00:00"} + } + }, + connection="myconnection", + username="user", + password="secret", + ) + start_mock.assert_called_with( + "mynet", + connection="myconnection", + username="user", + password="secret", + ) + else: + define_mock.assert_not_called() + start_mock.assert_not_called() + + +def test_network_running_nochange(test): + """ + network_running state test cases, no change case case. + """ + with patch.dict(virt.__opts__, {"test": test}): + define_mock = MagicMock(return_value=True) + update_mock = MagicMock(return_value=False) + with patch.dict( + virt.__salt__, + { + "virt.network_info": MagicMock( + return_value={"mynet": {"active": True, "autostart": True}} + ), + "virt.network_define": define_mock, + "virt.network_update": update_mock, + }, + ): + assert { + "name": "mynet", + "changes": {}, + "comment": "Network mynet unchanged and is running", + "result": None if test else True, + } == virt.network_running("mynet", "br2", "bridge") + assert [ + network_update_call("mynet", "br2", "bridge", test=True) + ] == update_mock.call_args_list + + +def test_network_running_stopped(test): + """ + network_running state test cases, network stopped case. + """ + with patch.dict(virt.__opts__, {"test": test}): + define_mock = MagicMock(return_value=True) + start_mock = MagicMock(return_value=True) + update_mock = MagicMock(return_value=False) + with patch.dict( + virt.__salt__, + { # pylint: disable=no-member + "virt.network_info": MagicMock( + return_value={"mynet": {"active": False, "autostart": True}} + ), + "virt.network_start": start_mock, + "virt.network_define": define_mock, + "virt.network_update": update_mock, + }, + ): + assert { + "name": "mynet", + "changes": {"mynet": "Network started"}, + "comment": "Network mynet unchanged and started", + "result": None if test else True, + } == virt.network_running( + "mynet", + "br2", + "bridge", + connection="myconnection", + username="user", + password="secret", + ) + assert [ + network_update_call( + "mynet", + "br2", + "bridge", + connection="myconnection", + username="user", + password="secret", + test=True, + ) + ] == update_mock.call_args_list + if not test: + start_mock.assert_called_with( + "mynet", + connection="myconnection", + username="user", + password="secret", + ) + else: + start_mock.assert_not_called() + + +def test_network_running_error(test): + """ + network_running state test cases, libvirt error case. + """ + with patch.dict(virt.__opts__, {"test": test}): + with patch.dict( + virt.__salt__, + { + "virt.network_info": MagicMock( + side_effect=virt.libvirt.libvirtError("Some error") + ), + }, + ): + assert { + "name": "mynet", + "changes": {}, + "comment": "Some error", + "result": False, + } == virt.network_running("mynet", "br2", "bridge") diff --git a/tests/pytests/unit/utils/test_xmlutil.py b/tests/pytests/unit/utils/test_xmlutil.py index 2bcaff3a17..aed3e42e06 100644 --- a/tests/pytests/unit/utils/test_xmlutil.py +++ b/tests/pytests/unit/utils/test_xmlutil.py @@ -208,3 +208,17 @@ def test_change_xml_template_list(xml_doc): assert ["1024", "512"] == [ n.get("size") for n in xml_doc.findall("memtune/hugepages/page") ] + + +def test_strip_spaces(): + xml_str = """<domain> + <name>test01</name> + <memory unit="MiB" >1024</memory> + </domain> + """ + expected_str = ( + b'<domain><name>test01</name><memory unit="MiB">1024</memory></domain>' + ) + + node = ET.fromstring(xml_str) + assert expected_str == ET.tostring(xml.strip_spaces(node)) diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index 59fa6b676e..4d9ea2501a 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -104,7 +104,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): mock_domain.name.return_value = name return mock_domain - def assertEqualUnit(self, actual, expected, unit="KiB"): + def assert_equal_unit(self, actual, expected, unit="KiB"): self.assertEqual(actual.get("unit"), unit) self.assertEqual(actual.text, str(expected)) @@ -537,7 +537,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): "swap_hard_limit": "1g", "min_guarantee": "256m", "hugepages": [ - {"nodeset": "", "size": "128m"}, + {"size": "128m"}, {"nodeset": "0", "size": "256m"}, {"nodeset": "1", "size": "512m"}, ], @@ -555,14 +555,14 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): "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.assert_equal_unit(root.find("memory"), 512 * 1024) + self.assert_equal_unit(root.find("currentMemory"), 256 * 1024) + self.assert_equal_unit(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.assert_equal_unit(root.find("memtune/hard_limit"), 1024 * 1024) + self.assert_equal_unit(root.find("memtune/soft_limit"), 512 * 1024) + self.assert_equal_unit(root.find("memtune/swap_hard_limit"), 1024 ** 2) + self.assert_equal_unit(root.find("memtune/min_guarantee"), 256 * 1024) self.assertEqual( [ {"nodeset": page.get("nodeset"), "size": page.get("size")} @@ -1772,70 +1772,6 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): ], ) - def test_diff_nics(self): - """ - Test virt._diff_nics() - """ - old_nics = ET.fromstring( - """ - <devices> - <interface type='network'> - <mac address='52:54:00:39:02:b1'/> - <source network='default'/> - <model type='virtio'/> - <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='admin'/> - <model type='virtio'/> - <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> - </interface> - <interface type='network'> - <mac address='52:54:00:39:02:b3'/> - <source network='admin'/> - <model type='virtio'/> - <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> - </interface> - </devices> - """ - ).findall("interface") - - new_nics = ET.fromstring( - """ - <devices> - <interface type='network'> - <mac address='52:54:00:39:02:b1'/> - <source network='default'/> - <model type='virtio'/> - </interface> - <interface type='network'> - <mac address='52:54:00:39:02:b2'/> - <source network='default'/> - <model type='virtio'/> - </interface> - <interface type='network'> - <mac address='52:54:00:39:02:b4'/> - <source network='admin'/> - <model type='virtio'/> - </interface> - </devices> - """ - ).findall("interface") - ret = virt._diff_interface_lists(old_nics, new_nics) - self.assertEqual( - [nic.find("mac").get("address") for nic in ret["unchanged"]], - ["52:54:00:39:02:b1"], - ) - self.assertEqual( - [nic.find("mac").get("address") for nic in ret["new"]], - ["52:54:00:39:02:b2", "52:54:00:39:02:b4"], - ) - self.assertEqual( - [nic.find("mac").get("address") for nic in ret["deleted"]], - ["52:54:00:39:02:b2", "52:54:00:39:02:b3"], - ) - def test_init(self): """ Test init() function @@ -4901,59 +4837,6 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual(expected, caps) - def test_network(self): - """ - Test virt._get_net_xml() - """ - xml_data = virt._gen_net_xml("network", "main", "bridge", "openvswitch") - root = ET.fromstring(xml_data) - self.assertEqual(root.find("name").text, "network") - self.assertEqual(root.find("bridge").attrib["name"], "main") - self.assertEqual(root.find("forward").attrib["mode"], "bridge") - self.assertEqual(root.find("virtualport").attrib["type"], "openvswitch") - - def test_network_nat(self): - """ - Test virt._get_net_xml() in a nat setup - """ - xml_data = virt._gen_net_xml( - "network", - "main", - "nat", - None, - ip_configs=[ - { - "cidr": "192.168.2.0/24", - "dhcp_ranges": [ - {"start": "192.168.2.10", "end": "192.168.2.25"}, - {"start": "192.168.2.110", "end": "192.168.2.125"}, - ], - } - ], - ) - root = ET.fromstring(xml_data) - self.assertEqual(root.find("name").text, "network") - self.assertEqual(root.find("bridge").attrib["name"], "main") - self.assertEqual(root.find("forward").attrib["mode"], "nat") - self.assertEqual( - root.find("./ip[@address='192.168.2.0']").attrib["prefix"], "24" - ) - self.assertEqual( - root.find("./ip[@address='192.168.2.0']").attrib["family"], "ipv4" - ) - self.assertEqual( - root.find( - "./ip[@address='192.168.2.0']/dhcp/range[@start='192.168.2.10']" - ).attrib["end"], - "192.168.2.25", - ) - self.assertEqual( - root.find( - "./ip[@address='192.168.2.0']/dhcp/range[@start='192.168.2.110']" - ).attrib["end"], - "192.168.2.125", - ) - def test_domain_capabilities(self): """ Test the virt.domain_capabilities parsing diff --git a/tests/unit/states/test_virt.py b/tests/unit/states/test_virt.py index dadc6dd08e..2ab73f8af4 100644 --- a/tests/unit/states/test_virt.py +++ b/tests/unit/states/test_virt.py @@ -7,7 +7,7 @@ import tempfile import salt.states.virt as virt import salt.utils.files -from salt.exceptions import CommandExecutionError, SaltInvocationError +from salt.exceptions import SaltInvocationError from tests.support.mixins import LoaderModuleMockMixin from tests.support.mock import MagicMock, mock_open, patch from tests.support.runtests import RUNTIME_VARS @@ -263,1707 +263,6 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): ret, ) - def test_defined(self): - """ - defined state test cases. - """ - ret = { - "name": "myvm", - "changes": {}, - "result": True, - "comment": "myvm is running", - } - with patch.dict(virt.__opts__, {"test": False}): - # no change test - init_mock = MagicMock(return_value=True) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm"]), - "virt.update": MagicMock(return_value={"definition": False}), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": False}}, - "comment": "Domain myvm unchanged", - } - ) - self.assertDictEqual(virt.defined("myvm"), ret) - - # Test defining a guest with connection details - init_mock.reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=[]), - "virt.init": init_mock, - "virt.update": MagicMock( - side_effect=CommandExecutionError("not found") - ), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True}}, - "comment": "Domain myvm defined", - } - ) - disks = [ - { - "name": "system", - "size": 8192, - "overlay_image": True, - "pool": "default", - "image": "/path/to/image.qcow2", - }, - {"name": "data", "size": 16834}, - ] - ifaces = [ - {"name": "eth0", "mac": "01:23:45:67:89:AB"}, - {"name": "eth1", "type": "network", "source": "admin"}, - ] - graphics = { - "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", - cpu=2, - mem=2048, - boot_dev="cdrom hd", - os_type="linux", - arch="i686", - vm_type="qemu", - disk_profile="prod", - disks=disks, - nic_profile="prod", - interfaces=ifaces, - graphics=graphics, - seed=False, - 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, - ) - init_mock.assert_called_with( - "myvm", - cpu=2, - mem=2048, - boot_dev="cdrom hd", - os_type="linux", - arch="i686", - disk="prod", - disks=disks, - nic="prod", - interfaces=ifaces, - graphics=graphics, - 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 - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm"]), - "virt.update": MagicMock( - return_value={"definition": True, "cpu": True} - ), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "cpu": True}}, - "result": True, - "comment": "Domain myvm updated", - } - ) - self.assertDictEqual(virt.defined("myvm", cpu=2), ret) - - # Working update case when running with boot params - boot = { - "kernel": "/root/f8-i386-vmlinuz", - "initrd": "/root/f8-i386-initrd", - "cmdline": "console=ttyS0 ks=http://example.com/f8-i386/os/", - } - - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm"]), - "virt.update": MagicMock( - return_value={"definition": True, "cpu": True} - ), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "cpu": True}}, - "result": True, - "comment": "Domain myvm updated", - } - ) - self.assertDictEqual(virt.defined("myvm", boot=boot), ret) - - # Working update case when stopped - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm"]), - "virt.update": MagicMock(return_value={"definition": True}), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True}}, - "result": True, - "comment": "Domain myvm updated", - } - ) - self.assertDictEqual(virt.defined("myvm", cpu=2), ret) - - # Failed live update case - update_mock = MagicMock( - return_value={ - "definition": True, - "cpu": False, - "errors": ["some error"], - } - ) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm"]), - "virt.update": update_mock, - }, - ): - ret.update( - { - "changes": { - "myvm": { - "definition": True, - "cpu": False, - "errors": ["some error"], - } - }, - "result": True, - "comment": "Domain myvm updated with live update(s) failures", - } - ) - self.assertDictEqual( - virt.defined("myvm", cpu=2, boot_dev="cdrom hd"), ret - ) - update_mock.assert_called_with( - "myvm", - cpu=2, - boot_dev="cdrom hd", - mem=None, - disk_profile=None, - disks=None, - nic_profile=None, - interfaces=None, - graphics=None, - live=True, - connection=None, - username=None, - password=None, - boot=None, - numatune=None, - test=False, - hypervisor_features=None, - clock=None, - serials=None, - consoles=None, - stop_on_reboot=False, - ) - - # Failed definition update case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm"]), - "virt.update": MagicMock( - side_effect=[self.mock_libvirt.libvirtError("error message")] - ), - }, - ): - ret.update({"changes": {}, "result": False, "comment": "error message"}) - self.assertDictEqual(virt.defined("myvm", cpu=2), ret) - - # Test dry-run mode - with patch.dict(virt.__opts__, {"test": True}): - # Guest defined case - init_mock = MagicMock(return_value=True) - update_mock = MagicMock(side_effect=CommandExecutionError("not found")) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=[]), - "virt.init": init_mock, - "virt.update": update_mock, - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True}}, - "result": None, - "comment": "Domain myvm defined", - } - ) - disks = [ - { - "name": "system", - "size": 8192, - "overlay_image": True, - "pool": "default", - "image": "/path/to/image.qcow2", - }, - {"name": "data", "size": 16834}, - ] - ifaces = [ - {"name": "eth0", "mac": "01:23:45:67:89:AB"}, - {"name": "eth1", "type": "network", "source": "admin"}, - ] - graphics = { - "type": "spice", - "listen": {"type": "address", "address": "192.168.0.1"}, - } - self.assertDictEqual( - virt.defined( - "myvm", - cpu=2, - mem=2048, - os_type="linux", - arch="i686", - vm_type="qemu", - disk_profile="prod", - disks=disks, - nic_profile="prod", - interfaces=ifaces, - graphics=graphics, - seed=False, - install=False, - pub_key="/path/to/key.pub", - priv_key="/path/to/key", - stop_on_reboot=False, - connection="someconnection", - username="libvirtuser", - password="supersecret", - ), - ret, - ) - init_mock.assert_not_called() - update_mock.assert_not_called() - - # Guest update case - update_mock = MagicMock(return_value={"definition": True}) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm"]), - "virt.update": update_mock, - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True}}, - "result": None, - "comment": "Domain myvm updated", - } - ) - self.assertDictEqual(virt.defined("myvm", cpu=2), ret) - update_mock.assert_called_with( - "myvm", - cpu=2, - mem=None, - disk_profile=None, - disks=None, - nic_profile=None, - interfaces=None, - graphics=None, - live=True, - connection=None, - 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, - ) - - # No changes case - update_mock = MagicMock(return_value={"definition": False}) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm"]), - "virt.update": update_mock, - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": False}}, - "result": True, - "comment": "Domain myvm unchanged", - } - ) - self.assertDictEqual(virt.defined("myvm"), ret) - update_mock.assert_called_with( - "myvm", - cpu=None, - mem=None, - disk_profile=None, - disks=None, - nic_profile=None, - interfaces=None, - graphics=None, - live=True, - connection=None, - 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, - ) - - def test_running(self): - """ - running state test cases. - """ - ret = { - "name": "myvm", - "changes": {}, - "result": True, - "comment": "myvm is running", - } - with patch.dict(virt.__opts__, {"test": False}): - # Test starting an existing guest without changing it - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), - "virt.start": MagicMock(return_value=0), - "virt.update": MagicMock(return_value={"definition": False}), - "virt.list_domains": MagicMock(return_value=["myvm"]), - }, - ): - ret.update( - { - "changes": {"myvm": {"started": True}}, - "comment": "Domain myvm started", - } - ) - self.assertDictEqual(virt.running("myvm"), ret) - - # Test defining and starting a guest the old way - init_mock = MagicMock(return_value=True) - start_mock = MagicMock(return_value=0) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), - "virt.init": init_mock, - "virt.start": start_mock, - "virt.list_domains": MagicMock(return_value=[]), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "started": True}}, - "comment": "Domain myvm defined and started", - } - ) - self.assertDictEqual( - virt.running( - "myvm", - cpu=2, - mem=2048, - disks=[{"name": "system", "image": "/path/to/img.qcow2"}], - ), - ret, - ) - init_mock.assert_called_with( - "myvm", - cpu=2, - mem=2048, - os_type=None, - arch=None, - boot=None, - numatune=None, - disk=None, - disks=[{"name": "system", "image": "/path/to/img.qcow2"}], - nic=None, - interfaces=None, - graphics=None, - hypervisor=None, - start=False, - seed=True, - install=True, - 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 - ) - - # Test defining and starting a guest the new way with connection details - init_mock.reset_mock() - start_mock.reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), - "virt.init": init_mock, - "virt.start": start_mock, - "virt.list_domains": MagicMock(return_value=[]), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "started": True}}, - "comment": "Domain myvm defined and started", - } - ) - disks = [ - { - "name": "system", - "size": 8192, - "overlay_image": True, - "pool": "default", - "image": "/path/to/image.qcow2", - }, - {"name": "data", "size": 16834}, - ] - ifaces = [ - {"name": "eth0", "mac": "01:23:45:67:89:AB"}, - {"name": "eth1", "type": "network", "source": "admin"}, - ] - graphics = { - "type": "spice", - "listen": {"type": "address", "address": "192.168.0.1"}, - } - self.assertDictEqual( - virt.running( - "myvm", - cpu=2, - mem=2048, - os_type="linux", - arch="i686", - vm_type="qemu", - disk_profile="prod", - disks=disks, - nic_profile="prod", - interfaces=ifaces, - graphics=graphics, - seed=False, - install=False, - pub_key="/path/to/key.pub", - priv_key="/path/to/key", - boot_dev="network hd", - stop_on_reboot=True, - connection="someconnection", - username="libvirtuser", - password="supersecret", - ), - ret, - ) - init_mock.assert_called_with( - "myvm", - cpu=2, - mem=2048, - os_type="linux", - arch="i686", - disk="prod", - disks=disks, - nic="prod", - interfaces=ifaces, - graphics=graphics, - 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", - connection="someconnection", - username="libvirtuser", - password="supersecret", - ) - - # Test with existing guest, but start raising an error - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), - "virt.update": MagicMock(return_value={"definition": False}), - "virt.start": MagicMock( - side_effect=[ - self.mock_libvirt.libvirtError("libvirt error msg") - ] - ), - "virt.list_domains": MagicMock(return_value=["myvm"]), - }, - ): - ret.update( - { - "changes": {"myvm": {}}, - "result": False, - "comment": "libvirt error msg", - } - ) - self.assertDictEqual(virt.running("myvm"), ret) - - # Working update case when running - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.update": MagicMock( - return_value={"definition": True, "cpu": True} - ), - "virt.list_domains": MagicMock(return_value=["myvm"]), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "cpu": True}}, - "result": True, - "comment": "Domain myvm updated", - } - ) - self.assertDictEqual(virt.running("myvm", cpu=2, update=True), ret) - - # Working update case when running with boot params - boot = { - "kernel": "/root/f8-i386-vmlinuz", - "initrd": "/root/f8-i386-initrd", - "cmdline": "console=ttyS0 ks=http://example.com/f8-i386/os/", - } - - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.update": MagicMock( - return_value={"definition": True, "cpu": True} - ), - "virt.list_domains": MagicMock(return_value=["myvm"]), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "cpu": True}}, - "result": True, - "comment": "Domain myvm updated", - } - ) - self.assertDictEqual(virt.running("myvm", boot=boot, update=True), ret) - - # Working update case when stopped - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), - "virt.start": MagicMock(return_value=0), - "virt.update": MagicMock(return_value={"definition": True}), - "virt.list_domains": MagicMock(return_value=["myvm"]), - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "started": True}}, - "result": True, - "comment": "Domain myvm updated and started", - } - ) - self.assertDictEqual(virt.running("myvm", cpu=2, update=True), ret) - - # Failed live update case - update_mock = MagicMock( - return_value={ - "definition": True, - "cpu": False, - "errors": ["some error"], - } - ) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.update": update_mock, - "virt.list_domains": MagicMock(return_value=["myvm"]), - }, - ): - ret.update( - { - "changes": { - "myvm": { - "definition": True, - "cpu": False, - "errors": ["some error"], - } - }, - "result": True, - "comment": "Domain myvm updated with live update(s) failures", - } - ) - self.assertDictEqual(virt.running("myvm", cpu=2, update=True), ret) - update_mock.assert_called_with( - "myvm", - cpu=2, - mem=None, - disk_profile=None, - disks=None, - nic_profile=None, - interfaces=None, - graphics=None, - live=True, - connection=None, - 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, - ) - - # Failed definition update case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.update": MagicMock( - side_effect=[self.mock_libvirt.libvirtError("error message")] - ), - "virt.list_domains": MagicMock(return_value=["myvm"]), - }, - ): - ret.update({"changes": {}, "result": False, "comment": "error message"}) - self.assertDictEqual(virt.running("myvm", cpu=2, update=True), ret) - - # Test dry-run mode - with patch.dict(virt.__opts__, {"test": True}): - # Guest defined case - init_mock = MagicMock(return_value=True) - start_mock = MagicMock(return_value=0) - list_mock = MagicMock(return_value=[]) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), - "virt.init": init_mock, - "virt.start": start_mock, - "virt.list_domains": list_mock, - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "started": True}}, - "result": None, - "comment": "Domain myvm defined and started", - } - ) - disks = [ - { - "name": "system", - "size": 8192, - "overlay_image": True, - "pool": "default", - "image": "/path/to/image.qcow2", - }, - {"name": "data", "size": 16834}, - ] - ifaces = [ - {"name": "eth0", "mac": "01:23:45:67:89:AB"}, - {"name": "eth1", "type": "network", "source": "admin"}, - ] - graphics = { - "type": "spice", - "listen": {"type": "address", "address": "192.168.0.1"}, - } - self.assertDictEqual( - virt.running( - "myvm", - cpu=2, - mem=2048, - os_type="linux", - arch="i686", - vm_type="qemu", - disk_profile="prod", - disks=disks, - nic_profile="prod", - interfaces=ifaces, - graphics=graphics, - seed=False, - install=False, - pub_key="/path/to/key.pub", - priv_key="/path/to/key", - stop_on_reboot=True, - connection="someconnection", - username="libvirtuser", - password="supersecret", - ), - ret, - ) - init_mock.assert_not_called() - start_mock.assert_not_called() - - # Guest update case - update_mock = MagicMock(return_value={"definition": True}) - start_mock = MagicMock(return_value=0) - list_mock = MagicMock(return_value=["myvm"]) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "stopped"}), - "virt.start": start_mock, - "virt.update": update_mock, - "virt.list_domains": list_mock, - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": True, "started": True}}, - "result": None, - "comment": "Domain myvm updated and started", - } - ) - self.assertDictEqual(virt.running("myvm", cpu=2, update=True), ret) - update_mock.assert_called_with( - "myvm", - cpu=2, - mem=None, - disk_profile=None, - disks=None, - nic_profile=None, - interfaces=None, - graphics=None, - live=True, - connection=None, - 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() - - # No changes case - update_mock = MagicMock(return_value={"definition": False}) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.update": update_mock, - "virt.list_domains": list_mock, - }, - ): - ret.update( - { - "changes": {"myvm": {"definition": False}}, - "result": True, - "comment": "Domain myvm exists and is running", - } - ) - self.assertDictEqual(virt.running("myvm", update=True), ret) - update_mock.assert_called_with( - "myvm", - cpu=None, - mem=None, - disk_profile=None, - disks=None, - nic_profile=None, - interfaces=None, - graphics=None, - live=True, - connection=None, - 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, - ) - - def test_stopped(self): - """ - stopped state test cases. - """ - ret = {"name": "myvm", "changes": {}, "result": True} - - shutdown_mock = MagicMock(return_value=True) - - # Normal case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.shutdown": shutdown_mock, - }, - ): - ret.update( - { - "changes": {"stopped": [{"domain": "myvm", "shutdown": True}]}, - "comment": "Machine has been shut down", - } - ) - self.assertDictEqual(virt.stopped("myvm"), ret) - shutdown_mock.assert_called_with( - "myvm", connection=None, username=None, password=None - ) - - # Normal case with user-provided connection parameters - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.shutdown": shutdown_mock, - }, - ): - self.assertDictEqual( - virt.stopped( - "myvm", - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - shutdown_mock.assert_called_with( - "myvm", connection="myconnection", username="user", password="secret" - ) - - # Case where an error occurred during the shutdown - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.shutdown": MagicMock( - side_effect=self.mock_libvirt.libvirtError("Some error") - ), - }, - ): - ret.update( - { - "changes": {"ignored": [{"domain": "myvm", "issue": "Some error"}]}, - "result": False, - "comment": "No changes had happened", - } - ) - self.assertDictEqual(virt.stopped("myvm"), ret) - - # Case there the domain doesn't exist - with patch.dict( - virt.__salt__, {"virt.list_domains": MagicMock(return_value=[])} - ): # pylint: disable=no-member - ret.update( - {"changes": {}, "result": False, "comment": "No changes had happened"} - ) - self.assertDictEqual(virt.stopped("myvm"), ret) - - # Case where the domain is already stopped - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.vm_state": MagicMock(return_value={"myvm": "shutdown"}), - }, - ): - ret.update( - {"changes": {}, "result": True, "comment": "No changes had happened"} - ) - self.assertDictEqual(virt.stopped("myvm"), ret) - - def test_powered_off(self): - """ - powered_off state test cases. - """ - ret = {"name": "myvm", "changes": {}, "result": True} - - stop_mock = MagicMock(return_value=True) - - # Normal case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.stop": stop_mock, - }, - ): - ret.update( - { - "changes": {"unpowered": [{"domain": "myvm", "stop": True}]}, - "comment": "Machine has been powered off", - } - ) - self.assertDictEqual(virt.powered_off("myvm"), ret) - stop_mock.assert_called_with( - "myvm", connection=None, username=None, password=None - ) - - # Normal case with user-provided connection parameters - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.stop": stop_mock, - }, - ): - self.assertDictEqual( - virt.powered_off( - "myvm", - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - stop_mock.assert_called_with( - "myvm", connection="myconnection", username="user", password="secret" - ) - - # Case where an error occurred during the poweroff - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.vm_state": MagicMock(return_value={"myvm": "running"}), - "virt.stop": MagicMock( - side_effect=self.mock_libvirt.libvirtError("Some error") - ), - }, - ): - ret.update( - { - "changes": {"ignored": [{"domain": "myvm", "issue": "Some error"}]}, - "result": False, - "comment": "No changes had happened", - } - ) - self.assertDictEqual(virt.powered_off("myvm"), ret) - - # Case there the domain doesn't exist - with patch.dict( - virt.__salt__, {"virt.list_domains": MagicMock(return_value=[])} - ): # pylint: disable=no-member - ret.update( - {"changes": {}, "result": False, "comment": "No changes had happened"} - ) - self.assertDictEqual(virt.powered_off("myvm"), ret) - - # Case where the domain is already stopped - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.vm_state": MagicMock(return_value={"myvm": "shutdown"}), - }, - ): - ret.update( - {"changes": {}, "result": True, "comment": "No changes had happened"} - ) - self.assertDictEqual(virt.powered_off("myvm"), ret) - - def test_snapshot(self): - """ - snapshot state test cases. - """ - ret = {"name": "myvm", "changes": {}, "result": True} - - snapshot_mock = MagicMock(return_value=True) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.snapshot": snapshot_mock, - }, - ): - ret.update( - { - "changes": {"saved": [{"domain": "myvm", "snapshot": True}]}, - "comment": "Snapshot has been taken", - } - ) - self.assertDictEqual(virt.snapshot("myvm"), ret) - snapshot_mock.assert_called_with( - "myvm", suffix=None, connection=None, username=None, password=None - ) - - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.snapshot": snapshot_mock, - }, - ): - self.assertDictEqual( - virt.snapshot( - "myvm", - suffix="snap", - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - snapshot_mock.assert_called_with( - "myvm", - suffix="snap", - connection="myconnection", - username="user", - password="secret", - ) - - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.snapshot": MagicMock( - side_effect=self.mock_libvirt.libvirtError("Some error") - ), - }, - ): - ret.update( - { - "changes": {"ignored": [{"domain": "myvm", "issue": "Some error"}]}, - "result": False, - "comment": "No changes had happened", - } - ) - self.assertDictEqual(virt.snapshot("myvm"), ret) - - with patch.dict( - virt.__salt__, {"virt.list_domains": MagicMock(return_value=[])} - ): # pylint: disable=no-member - ret.update( - {"changes": {}, "result": False, "comment": "No changes had happened"} - ) - self.assertDictEqual(virt.snapshot("myvm"), ret) - - def test_rebooted(self): - """ - rebooted state test cases. - """ - ret = {"name": "myvm", "changes": {}, "result": True} - - reboot_mock = MagicMock(return_value=True) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.reboot": reboot_mock, - }, - ): - ret.update( - { - "changes": {"rebooted": [{"domain": "myvm", "reboot": True}]}, - "comment": "Machine has been rebooted", - } - ) - self.assertDictEqual(virt.rebooted("myvm"), ret) - reboot_mock.assert_called_with( - "myvm", connection=None, username=None, password=None - ) - - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.reboot": reboot_mock, - }, - ): - self.assertDictEqual( - virt.rebooted( - "myvm", - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - reboot_mock.assert_called_with( - "myvm", connection="myconnection", username="user", password="secret" - ) - - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.list_domains": MagicMock(return_value=["myvm", "vm1"]), - "virt.reboot": MagicMock( - side_effect=self.mock_libvirt.libvirtError("Some error") - ), - }, - ): - ret.update( - { - "changes": {"ignored": [{"domain": "myvm", "issue": "Some error"}]}, - "result": False, - "comment": "No changes had happened", - } - ) - self.assertDictEqual(virt.rebooted("myvm"), ret) - - with patch.dict( - virt.__salt__, {"virt.list_domains": MagicMock(return_value=[])} - ): # pylint: disable=no-member - ret.update( - {"changes": {}, "result": False, "comment": "No changes had happened"} - ) - self.assertDictEqual(virt.rebooted("myvm"), ret) - - def test_network_defined(self): - """ - network_defined state test cases. - """ - ret = {"name": "mynet", "changes": {}, "result": True, "comment": ""} - with patch.dict(virt.__opts__, {"test": False}): - define_mock = MagicMock(return_value=True) - # Non-existing network case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - side_effect=[{}, {"mynet": {"active": False}}] - ), - "virt.network_define": define_mock, - }, - ): - ret.update( - { - "changes": {"mynet": "Network defined"}, - "comment": "Network mynet defined", - } - ) - self.assertDictEqual( - virt.network_defined( - "mynet", - "br2", - "bridge", - vport="openvswitch", - tag=180, - ipv4_config={ - "cidr": "192.168.2.0/24", - "dhcp_ranges": [ - {"start": "192.168.2.10", "end": "192.168.2.25"}, - {"start": "192.168.2.110", "end": "192.168.2.125"}, - ], - }, - ipv6_config={ - "cidr": "2001:db8:ca2:2::1/64", - "dhcp_ranges": [ - { - "start": "2001:db8:ca2:1::10", - "end": "2001:db8:ca2::1f", - }, - ], - }, - autostart=False, - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - define_mock.assert_called_with( - "mynet", - "br2", - "bridge", - vport="openvswitch", - tag=180, - autostart=False, - start=False, - ipv4_config={ - "cidr": "192.168.2.0/24", - "dhcp_ranges": [ - {"start": "192.168.2.10", "end": "192.168.2.25"}, - {"start": "192.168.2.110", "end": "192.168.2.125"}, - ], - }, - ipv6_config={ - "cidr": "2001:db8:ca2:2::1/64", - "dhcp_ranges": [ - {"start": "2001:db8:ca2:1::10", "end": "2001:db8:ca2::1f"}, - ], - }, - connection="myconnection", - username="user", - password="secret", - ) - - # Case where there is nothing to be done - define_mock.reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - return_value={"mynet": {"active": True}} - ), - "virt.network_define": define_mock, - }, - ): - ret.update({"changes": {}, "comment": "Network mynet exists"}) - self.assertDictEqual( - virt.network_defined("mynet", "br2", "bridge"), ret - ) - - # Error case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock(return_value={}), - "virt.network_define": MagicMock( - side_effect=self.mock_libvirt.libvirtError("Some error") - ), - }, - ): - ret.update({"changes": {}, "comment": "Some error", "result": False}) - self.assertDictEqual( - virt.network_defined("mynet", "br2", "bridge"), ret - ) - - # Test cases with __opt__['test'] set to True - with patch.dict(virt.__opts__, {"test": True}): - ret.update({"result": None}) - - # Non-existing network case - define_mock.reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock(return_value={}), - "virt.network_define": define_mock, - }, - ): - ret.update( - { - "changes": {"mynet": "Network defined"}, - "comment": "Network mynet defined", - } - ) - self.assertDictEqual( - virt.network_defined( - "mynet", - "br2", - "bridge", - vport="openvswitch", - tag=180, - ipv4_config={ - "cidr": "192.168.2.0/24", - "dhcp_ranges": [ - {"start": "192.168.2.10", "end": "192.168.2.25"}, - {"start": "192.168.2.110", "end": "192.168.2.125"}, - ], - }, - ipv6_config={ - "cidr": "2001:db8:ca2:2::1/64", - "dhcp_ranges": [ - { - "start": "2001:db8:ca2:1::10", - "end": "2001:db8:ca2::1f", - }, - ], - }, - autostart=False, - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - define_mock.assert_not_called() - - # Case where there is nothing to be done - define_mock.reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - return_value={"mynet": {"active": True}} - ), - "virt.network_define": define_mock, - }, - ): - ret.update( - {"changes": {}, "comment": "Network mynet exists", "result": True} - ) - self.assertDictEqual( - virt.network_defined("mynet", "br2", "bridge"), ret - ) - - # Error case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - side_effect=self.mock_libvirt.libvirtError("Some error") - ) - }, - ): - ret.update({"changes": {}, "comment": "Some error", "result": False}) - self.assertDictEqual( - virt.network_defined("mynet", "br2", "bridge"), ret - ) - - def test_network_running(self): - """ - network_running state test cases. - """ - ret = {"name": "mynet", "changes": {}, "result": True, "comment": ""} - with patch.dict(virt.__opts__, {"test": False}): - define_mock = MagicMock(return_value=True) - start_mock = MagicMock(return_value=True) - # Non-existing network case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - side_effect=[{}, {"mynet": {"active": False}}] - ), - "virt.network_define": define_mock, - "virt.network_start": start_mock, - }, - ): - ret.update( - { - "changes": {"mynet": "Network defined and started"}, - "comment": "Network mynet defined and started", - } - ) - self.assertDictEqual( - virt.network_running( - "mynet", - "br2", - "bridge", - vport="openvswitch", - tag=180, - ipv4_config={ - "cidr": "192.168.2.0/24", - "dhcp_ranges": [ - {"start": "192.168.2.10", "end": "192.168.2.25"}, - {"start": "192.168.2.110", "end": "192.168.2.125"}, - ], - }, - ipv6_config={ - "cidr": "2001:db8:ca2:2::1/64", - "dhcp_ranges": [ - { - "start": "2001:db8:ca2:1::10", - "end": "2001:db8:ca2::1f", - }, - ], - }, - autostart=False, - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - define_mock.assert_called_with( - "mynet", - "br2", - "bridge", - vport="openvswitch", - tag=180, - autostart=False, - start=False, - ipv4_config={ - "cidr": "192.168.2.0/24", - "dhcp_ranges": [ - {"start": "192.168.2.10", "end": "192.168.2.25"}, - {"start": "192.168.2.110", "end": "192.168.2.125"}, - ], - }, - ipv6_config={ - "cidr": "2001:db8:ca2:2::1/64", - "dhcp_ranges": [ - {"start": "2001:db8:ca2:1::10", "end": "2001:db8:ca2::1f"}, - ], - }, - connection="myconnection", - username="user", - password="secret", - ) - start_mock.assert_called_with( - "mynet", - connection="myconnection", - username="user", - password="secret", - ) - - # Case where there is nothing to be done - define_mock.reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - return_value={"mynet": {"active": True}} - ), - "virt.network_define": define_mock, - }, - ): - ret.update( - {"changes": {}, "comment": "Network mynet exists and is running"} - ) - self.assertDictEqual( - virt.network_running("mynet", "br2", "bridge"), ret - ) - - # Network existing and stopped case - start_mock = MagicMock(return_value=True) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - return_value={"mynet": {"active": False}} - ), - "virt.network_start": start_mock, - "virt.network_define": define_mock, - }, - ): - ret.update( - { - "changes": {"mynet": "Network started"}, - "comment": "Network mynet exists and started", - } - ) - self.assertDictEqual( - virt.network_running( - "mynet", - "br2", - "bridge", - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - start_mock.assert_called_with( - "mynet", - connection="myconnection", - username="user", - password="secret", - ) - - # Error case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock(return_value={}), - "virt.network_define": MagicMock( - side_effect=self.mock_libvirt.libvirtError("Some error") - ), - }, - ): - ret.update({"changes": {}, "comment": "Some error", "result": False}) - self.assertDictEqual( - virt.network_running("mynet", "br2", "bridge"), ret - ) - - # Test cases with __opt__['test'] set to True - with patch.dict(virt.__opts__, {"test": True}): - ret.update({"result": None}) - - # Non-existing network case - define_mock.reset_mock() - start_mock.reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock(return_value={}), - "virt.network_define": define_mock, - "virt.network_start": start_mock, - }, - ): - ret.update( - { - "changes": {"mynet": "Network defined and started"}, - "comment": "Network mynet defined and started", - } - ) - self.assertDictEqual( - virt.network_running( - "mynet", - "br2", - "bridge", - vport="openvswitch", - tag=180, - ipv4_config={ - "cidr": "192.168.2.0/24", - "dhcp_ranges": [ - {"start": "192.168.2.10", "end": "192.168.2.25"}, - {"start": "192.168.2.110", "end": "192.168.2.125"}, - ], - }, - ipv6_config={ - "cidr": "2001:db8:ca2:2::1/64", - "dhcp_ranges": [ - { - "start": "2001:db8:ca2:1::10", - "end": "2001:db8:ca2::1f", - }, - ], - }, - autostart=False, - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - define_mock.assert_not_called() - start_mock.assert_not_called() - - # Case where there is nothing to be done - define_mock.reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - return_value={"mynet": {"active": True}} - ), - "virt.network_define": define_mock, - }, - ): - ret.update( - {"changes": {}, "comment": "Network mynet exists and is running"} - ) - self.assertDictEqual( - virt.network_running("mynet", "br2", "bridge"), ret - ) - - # Network existing and stopped case - start_mock = MagicMock(return_value=True) - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - return_value={"mynet": {"active": False}} - ), - "virt.network_start": start_mock, - "virt.network_define": define_mock, - }, - ): - ret.update( - { - "changes": {"mynet": "Network started"}, - "comment": "Network mynet exists and started", - } - ) - self.assertDictEqual( - virt.network_running( - "mynet", - "br2", - "bridge", - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - start_mock.assert_not_called() - - # Error case - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.network_info": MagicMock( - side_effect=self.mock_libvirt.libvirtError("Some error") - ) - }, - ): - ret.update({"changes": {}, "comment": "Some error", "result": False}) - self.assertDictEqual( - virt.network_running("mynet", "br2", "bridge"), ret - ) - def test_pool_defined(self): """ pool_defined state test cases. -- 2.30.1
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