Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-15:Update
salt.21868
open-suse-3002.2-virt-network-311.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File open-suse-3002.2-virt-network-311.patch of Package salt.21868
From ff3273ffb5be499d14a0023b8b9f8baed133807b Mon Sep 17 00:00:00 2001 From: Cedric Bosdonnat <cbosdonnat@suse.com> Date: Tue, 12 Jan 2021 11:28:24 +0100 Subject: [PATCH] Open suse 3002.2 virt network (#311) * Bump to `pytest-salt-factories >= 0.120.0` * Switch to using the pytest-salt-factories loader mock support * 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: extract the live update code from the update function In order to help reusing the device changes computing code and avoid getting a giant virt.update(), move the live update code of it into a specific internal function. * 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 Co-authored-by: Pedro Algarvio <pedro@algarvio.me> --- changelog/59143.added | 1 + requirements/pytest.txt | 2 +- requirements/static/ci/py3.5/darwin.txt | 2 +- requirements/static/ci/py3.5/freebsd.txt | 2 +- requirements/static/ci/py3.5/linux.txt | 2 +- requirements/static/ci/py3.5/windows.txt | 2 +- requirements/static/ci/py3.6/darwin.txt | 2 +- requirements/static/ci/py3.6/freebsd.txt | 2 +- requirements/static/ci/py3.6/linux.txt | 2 +- requirements/static/ci/py3.6/windows.txt | 2 +- requirements/static/ci/py3.7/darwin.txt | 2 +- requirements/static/ci/py3.7/freebsd.txt | 2 +- requirements/static/ci/py3.7/linux.txt | 2 +- requirements/static/ci/py3.7/windows.txt | 2 +- requirements/static/ci/py3.8/darwin.txt | 2 +- requirements/static/ci/py3.8/freebsd.txt | 2 +- requirements/static/ci/py3.8/linux.txt | 2 +- requirements/static/ci/py3.9/darwin.txt | 2 +- requirements/static/ci/py3.9/freebsd.txt | 2 +- requirements/static/ci/py3.9/linux.txt | 2 +- salt/modules/virt.py | 1260 +++++++++--- 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/conftest.py | 2 +- tests/pytests/functional/modules/test_opkg.py | 8 +- tests/pytests/unit/beacons/test_sensehat.py | 8 +- tests/pytests/unit/beacons/test_status.py | 8 +- .../pytests/unit/modules/test_alternatives.py | 8 +- .../pytests/unit/modules/test_ansiblegate.py | 13 +- tests/pytests/unit/modules/test_archive.py | 8 +- .../pytests/unit/modules/test_azurearm_dns.py | 8 +- tests/pytests/unit/modules/test_nilrt_ip.py | 8 +- tests/pytests/unit/modules/test_opkg.py | 8 +- .../pytests/unit/modules/test_restartcheck.py | 8 +- .../unit/modules/test_slackware_service.py | 12 +- tests/pytests/unit/modules/test_swarm.py | 10 +- tests/pytests/unit/modules/test_tls.py | 12 +- tests/pytests/unit/modules/virt/conftest.py | 88 +- .../pytests/unit/modules/virt/test_domain.py | 473 ++++- .../pytests/unit/modules/virt/test_helpers.py | 25 + tests/pytests/unit/modules/virt/test_host.py | 219 +++ .../pytests/unit/modules/virt/test_network.py | 450 +++++ tests/pytests/unit/output/test_highstate.py | 8 +- .../pytests/unit/states/test_alternatives.py | 8 +- tests/pytests/unit/states/test_ini_manage.py | 24 +- 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_linux_sysctl.py | 173 -- tests/unit/modules/test_virt.py | 137 +- tests/unit/states/test_virt.py | 1703 +---------------- 57 files changed, 4689 insertions(+), 2757 deletions(-) create mode 100644 changelog/59143.added 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 delete mode 100644 tests/unit/modules/test_linux_sysctl.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/requirements/pytest.txt b/requirements/pytest.txt index 96faa73c27..77d60767d1 100644 --- a/requirements/pytest.txt +++ b/requirements/pytest.txt @@ -2,6 +2,6 @@ mock >= 3.0.0 # PyTest pytest >= 6.1.0 pytest-salt -pytest-salt-factories >= 0.93.0 +pytest-salt-factories >= 0.120.0 pytest-tempdir >= 2019.10.12 pytest-helpers-namespace >= 2019.1.8 diff --git a/requirements/static/ci/py3.5/darwin.txt b/requirements/static/ci/py3.5/darwin.txt index acfb43b542..efaac38353 100644 --- a/requirements/static/ci/py3.5/darwin.txt +++ b/requirements/static/ci/py3.5/darwin.txt @@ -89,7 +89,7 @@ pyopenssl==19.0.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.5/freebsd.txt b/requirements/static/ci/py3.5/freebsd.txt index 868cea5220..d4faa715c9 100644 --- a/requirements/static/ci/py3.5/freebsd.txt +++ b/requirements/static/ci/py3.5/freebsd.txt @@ -91,7 +91,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.5/linux.txt b/requirements/static/ci/py3.5/linux.txt index c6b57bf491..6b64d844db 100644 --- a/requirements/static/ci/py3.5/linux.txt +++ b/requirements/static/ci/py3.5/linux.txt @@ -184,7 +184,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.5/windows.txt b/requirements/static/ci/py3.5/windows.txt index 8646edac12..3de8e54de0 100644 --- a/requirements/static/ci/py3.5/windows.txt +++ b/requirements/static/ci/py3.5/windows.txt @@ -82,7 +82,7 @@ pymysql==0.9.3 pyopenssl==19.0.0 pyparsing==2.4.5 # via packaging pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.6/darwin.txt b/requirements/static/ci/py3.6/darwin.txt index 223ae11a0a..cf560de09d 100644 --- a/requirements/static/ci/py3.6/darwin.txt +++ b/requirements/static/ci/py3.6/darwin.txt @@ -94,7 +94,7 @@ pyopenssl==19.0.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.6/freebsd.txt b/requirements/static/ci/py3.6/freebsd.txt index 6493dd4c8f..13a7678376 100644 --- a/requirements/static/ci/py3.6/freebsd.txt +++ b/requirements/static/ci/py3.6/freebsd.txt @@ -96,7 +96,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.6/linux.txt b/requirements/static/ci/py3.6/linux.txt index 3317837a35..55800bfa25 100644 --- a/requirements/static/ci/py3.6/linux.txt +++ b/requirements/static/ci/py3.6/linux.txt @@ -188,7 +188,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.6/windows.txt b/requirements/static/ci/py3.6/windows.txt index eae87eadb1..325e6ec969 100644 --- a/requirements/static/ci/py3.6/windows.txt +++ b/requirements/static/ci/py3.6/windows.txt @@ -81,7 +81,7 @@ pymysql==0.9.3 pyopenssl==19.0.0 pyparsing==2.4.5 # via packaging pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.7/darwin.txt b/requirements/static/ci/py3.7/darwin.txt index d7c43ab796..8411522975 100644 --- a/requirements/static/ci/py3.7/darwin.txt +++ b/requirements/static/ci/py3.7/darwin.txt @@ -92,7 +92,7 @@ pyopenssl==19.0.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.7/freebsd.txt b/requirements/static/ci/py3.7/freebsd.txt index 8c7a7df48b..98c4c85dfe 100644 --- a/requirements/static/ci/py3.7/freebsd.txt +++ b/requirements/static/ci/py3.7/freebsd.txt @@ -94,7 +94,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.7/linux.txt b/requirements/static/ci/py3.7/linux.txt index 9c6a5139b2..c3490e6ba6 100644 --- a/requirements/static/ci/py3.7/linux.txt +++ b/requirements/static/ci/py3.7/linux.txt @@ -186,7 +186,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.7/windows.txt b/requirements/static/ci/py3.7/windows.txt index 7ca5bc9b49..53b5db2734 100644 --- a/requirements/static/ci/py3.7/windows.txt +++ b/requirements/static/ci/py3.7/windows.txt @@ -79,7 +79,7 @@ pymysql==0.9.3 pyopenssl==19.0.0 pyparsing==2.4.5 # via packaging pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.8/darwin.txt b/requirements/static/ci/py3.8/darwin.txt index f410432e54..541fd4c2d6 100644 --- a/requirements/static/ci/py3.8/darwin.txt +++ b/requirements/static/ci/py3.8/darwin.txt @@ -91,7 +91,7 @@ pyopenssl==19.0.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.8/freebsd.txt b/requirements/static/ci/py3.8/freebsd.txt index d0c20f466c..6030e259d1 100644 --- a/requirements/static/ci/py3.8/freebsd.txt +++ b/requirements/static/ci/py3.8/freebsd.txt @@ -93,7 +93,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.8/linux.txt b/requirements/static/ci/py3.8/linux.txt index 9ae7e8957e..da66159c3e 100644 --- a/requirements/static/ci/py3.8/linux.txt +++ b/requirements/static/ci/py3.8/linux.txt @@ -186,7 +186,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.9/darwin.txt b/requirements/static/ci/py3.9/darwin.txt index 3e6b92586d..50a3c95995 100644 --- a/requirements/static/ci/py3.9/darwin.txt +++ b/requirements/static/ci/py3.9/darwin.txt @@ -91,7 +91,7 @@ pyopenssl==19.0.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.9/freebsd.txt b/requirements/static/ci/py3.9/freebsd.txt index 48da272966..08e5e3c51e 100644 --- a/requirements/static/ci/py3.9/freebsd.txt +++ b/requirements/static/ci/py3.9/freebsd.txt @@ -93,7 +93,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/requirements/static/ci/py3.9/linux.txt b/requirements/static/ci/py3.9/linux.txt index ae6683ea03..d11c63ce7a 100644 --- a/requirements/static/ci/py3.9/linux.txt +++ b/requirements/static/ci/py3.9/linux.txt @@ -186,7 +186,7 @@ pyopenssl==19.1.0 pyparsing==2.4.5 # via junos-eznc, packaging pyserial==3.4 # via junos-eznc, netmiko pytest-helpers-namespace==2019.1.8 -pytest-salt-factories==0.93.0 +pytest-salt-factories==0.120.0 pytest-salt==2020.1.27 pytest-tempdir==2019.10.12 pytest==6.1.1 diff --git a/salt/modules/virt.py b/salt/modules/virt.py index b852f8175d..9f61983e8d 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -428,7 +428,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") @@ -814,6 +815,7 @@ def _gen_xml( serials=None, consoles=None, stop_on_reboot=False, + host_devices=None, **kwargs ): """ @@ -953,7 +955,8 @@ def _gen_xml( "disk_bus": disk["model"], "format": disk.get("format", "raw"), "index": str(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"): @@ -1001,6 +1004,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" @@ -1044,23 +1085,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: @@ -1813,6 +1906,7 @@ def init( serials=None, consoles=None, stop_on_reboot=False, + host_devices=None, **kwargs ): """ @@ -2143,6 +2237,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 @@ -2485,9 +2586,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: @@ -2706,6 +2815,7 @@ def init( serials, consoles, stop_on_reboot, + host_devices, **kwargs ) log.debug("New virtual machine definition: %s", vm_xml) @@ -2764,10 +2874,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 @@ -2819,6 +2939,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 @@ -2919,6 +3065,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 @@ -3014,6 +3170,218 @@ def _diff_console_list(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 + """ + if current is None or new is None: + return False + return abs(current - new) / current < 1e-03 + + +def _compute_device_changes(old_xml, new_xml, to_skip): + """ + Compute the device changes between two domain XML definitions. + """ + devices_node = old_xml.find("devices") + changes = {} + for dev_type in to_skip: + changes[dev_type] = {} + if not to_skip[dev_type]: + old = devices_node.findall(dev_type) + new = new_xml.findall("devices/{}".format(dev_type)) + changes[dev_type] = globals()["_diff_{}_lists".format(dev_type)](old, new) + 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. + """ + status = {} + errors = [] + + if not domain.isActive(): + return status, errors + + # Do the live changes now that we know the definition has been properly set + # From that point on, failures are not blocking to try to live update as much + # as possible. + commands = [] + if cpu and (isinstance(cpu, int) or isinstance(cpu, dict) and cpu.get("maximum")): + new_cpu = cpu.get("maximum") if isinstance(cpu, dict) else cpu + if old_cpu != new_cpu and new_cpu is not None: + commands.append( + { + "device": "cpu", + "cmd": "setVcpusFlags", + "args": [new_cpu, libvirt.VIR_DOMAIN_AFFECT_LIVE], + } + ) + if mem: + if isinstance(mem, dict): + # setMemoryFlags takes memory amount in KiB + new_mem = ( + int(_handle_unit(mem.get("current")) / 1024) + if "current" in mem + else None + ) + elif isinstance(mem, int): + new_mem = int(mem * 1024) + + if not _almost_equal(old_mem, new_mem) and new_mem is not None: + commands.append( + { + "device": "mem", + "cmd": "setMemoryFlags", + "args": [new_mem, libvirt.VIR_DOMAIN_AFFECT_LIVE], + } + ) + + # Compute the changes with the live definition + 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 = [] + new_disks = [] + for new_disk in changes["disk"].get("new", []): + device = new_disk.get("device", "disk") + if device not in ["cdrom", "floppy"]: + new_disks.append(new_disk) + continue + + target_dev = new_disk.find("target").get("dev") + matching = [ + old_disk + for old_disk in changes["disk"].get("deleted", []) + if old_disk.get("device", "disk") == device + and old_disk.find("target").get("dev") == target_dev + ] + if not matching: + new_disks.append(new_disk) + else: + # libvirt needs to keep the XML exactly as it was before + updated_disk = matching[0] + changes["disk"]["deleted"].remove(updated_disk) + removable_changes.append(updated_disk) + source_node = updated_disk.find("source") + new_source_node = new_disk.find("source") + source_file = ( + new_source_node.get("file") if new_source_node is not None else None + ) + + updated_disk.set("type", "file") + # Detaching device + if source_node is not None: + updated_disk.remove(source_node) + + # Attaching device + if source_file: + ElementTree.SubElement( + updated_disk, "source", attrib={"file": source_file} + ) + + changes["disk"]["new"] = new_disks + + for dev_type in ["disk", "interface", "hostdev"]: + for added in changes[dev_type].get("new", []): + commands.append( + { + "device": dev_type, + "cmd": "attachDevice", + "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(removed_def)], + } + ) + + for updated_disk in removable_changes: + commands.append( + { + "device": "disk", + "cmd": "updateDeviceFlags", + "args": [xmlutil.element_to_str(updated_disk)], + } + ) + + for cmd in commands: + try: + ret = 0 if test else getattr(domain, cmd["cmd"])(*cmd["args"]) + device_type = cmd["device"] + if device_type in ["cpu", "mem"]: + status[device_type] = not ret + else: + actions = { + "attachDevice": "attached", + "detachDevice": "detached", + "updateDeviceFlags": "updated", + } + device_status = status.setdefault(device_type, {}) + cmd_status = device_status.setdefault(actions[cmd["cmd"]], []) + cmd_status.append(cmd["args"][0]) + + except libvirt.libvirtError as err: + errors.append(str(err)) + + return status, errors + + def update( name, cpu=0, @@ -3033,6 +3401,7 @@ def update( serials=None, consoles=None, stop_on_reboot=False, + host_devices=None, **kwargs ): """ @@ -3220,6 +3589,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 @@ -3254,7 +3630,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 @@ -3283,6 +3659,7 @@ def update( serial=serials, consoles=consoles, stop_on_reboot=stop_on_reboot, + host_devices=host_devices, **kwargs ) ) @@ -3326,11 +3703,6 @@ def update( old_mem = int(_get_with_unit(desc.find("memory")) / 1024) old_cpu = int(desc.find("./vcpu").text) - def _almost_equal(current, new): - if current is None or new is None: - return False - return abs(current - new) / current < 1e-03 - def _yesno_attribute(path, xpath, attr_name, ignored=None): return xmlutil.attribute( path, xpath, attr_name, ignored, lambda v: "yes" if v else "no" @@ -3669,26 +4041,24 @@ def update( # Update the XML definition with the new disks and diff changes devices_node = desc.find("devices") - parameters = { - "disk": ["disks", "disk_profile"], - "interface": ["interfaces", "nic_profile"], - "graphics": ["graphics"], - "serial": ["serial"], - "console": ["console"], + func_locals = locals() + + def _skip_update(names): + return all(func_locals.get(n) is None for n in names) + + to_skip = { + "disk": _skip_update(["disks", "disk_profile"]), + "interface": _skip_update(["interfaces", "nic_profile"]), + "graphics": _skip_update(["graphics"]), + "serial": _skip_update(["serial"]), + "console": _skip_update(["console"]), + "hostdev": _skip_update(["host_devices"]), } - changes = {} - for dev_type in parameters: - changes[dev_type] = {} - func_locals = locals() - if [ - param - for param in parameters[dev_type] - if func_locals.get(param, None) is not None - ]: + changes = _compute_device_changes(desc, new_desc, to_skip) + for dev_type in changes: + if not to_skip[dev_type]: old = devices_node.findall(dev_type) - new = new_desc.findall("devices/{}".format(dev_type)) - changes[dev_type] = globals()["_diff_{}_lists".format(dev_type)](old, new) - if changes[dev_type]["deleted"] or changes[dev_type]["new"]: + if changes[dev_type].get("deleted") or changes[dev_type].get("new"): for item in old: devices_node.remove(item) devices_node.extend(changes[dev_type]["sorted"]) @@ -3713,151 +4083,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 - # Do the live changes now that we know the definition has been properly set - # From that point on, failures are not blocking to try to live update as much - # as possible. - commands = [] - removable_changes = [] - if domain.isActive() and live: - if cpu and ( - isinstance(cpu, int) or isinstance(cpu, dict) and cpu.get("maximum") - ): - new_cpu = cpu.get("maximum") if isinstance(cpu, dict) else cpu - if old_cpu != new_cpu and new_cpu is not None: - commands.append( - { - "device": "cpu", - "cmd": "setVcpusFlags", - "args": [new_cpu, libvirt.VIR_DOMAIN_AFFECT_LIVE], - } - ) - if mem: - if isinstance(mem, dict): - # setMemoryFlags takes memory amount in KiB - new_mem = ( - int(_handle_unit(mem.get("current")) / 1024) - if "current" in mem - else None - ) - elif isinstance(mem, int): - new_mem = int(mem * 1024) - - if not _almost_equal(old_mem, new_mem) and new_mem is not None: - commands.append( - { - "device": "mem", - "cmd": "setMemoryFlags", - "args": [new_mem, libvirt.VIR_DOMAIN_AFFECT_LIVE], - } - ) - - # Look for removable device source changes - new_disks = [] - for new_disk in changes["disk"].get("new", []): - device = new_disk.get("device", "disk") - if device not in ["cdrom", "floppy"]: - new_disks.append(new_disk) - continue - - target_dev = new_disk.find("target").get("dev") - matching = [ - old_disk - for old_disk in changes["disk"].get("deleted", []) - if old_disk.get("device", "disk") == device - and old_disk.find("target").get("dev") == target_dev - ] - if not matching: - new_disks.append(new_disk) - else: - # libvirt needs to keep the XML exactly as it was before - updated_disk = matching[0] - changes["disk"]["deleted"].remove(updated_disk) - removable_changes.append(updated_disk) - source_node = updated_disk.find("source") - new_source_node = new_disk.find("source") - source_file = ( - new_source_node.get("file") - if new_source_node is not None - else None - ) - - updated_disk.set("type", "file") - # Detaching device - if source_node is not None: - updated_disk.remove(source_node) - - # Attaching device - if source_file: - ElementTree.SubElement(updated_disk, "source", file=source_file) - - changes["disk"]["new"] = new_disks - - for dev_type in ["disk", "interface"]: - for added in changes[dev_type].get("new", []): - commands.append( - { - "device": dev_type, - "cmd": "attachDevice", - "args": [ - salt.utils.stringutils.to_str( - ElementTree.tostring(added) - ) - ], - } - ) - - for removed in changes[dev_type].get("deleted", []): - commands.append( - { - "device": dev_type, - "cmd": "detachDevice", - "args": [ - salt.utils.stringutils.to_str( - ElementTree.tostring(removed) - ) - ], - } - ) - - for updated_disk in removable_changes: - commands.append( - { - "device": "disk", - "cmd": "updateDeviceFlags", - "args": [ - salt.utils.stringutils.to_str( - ElementTree.tostring(updated_disk) - ) - ], - } - ) - - for cmd in commands: - try: - ret = getattr(domain, cmd["cmd"])(*cmd["args"]) if not test else 0 - device_type = cmd["device"] - if device_type in ["cpu", "mem"]: - status[device_type] = not bool(ret) - else: - actions = { - "attachDevice": "attached", - "detachDevice": "detached", - "updateDeviceFlags": "updated", - } - status[device_type][actions[cmd["cmd"]]].append(cmd["args"][0]) - - except libvirt.libvirtError as err: - if "errors" not in status: - status["errors"] = [] - status["errors"].append(str(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: + status_errors = status.setdefault("errors", []) + status_errors += errors conn.close() return status @@ -4107,6 +4348,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 @@ -5791,9 +6147,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} @@ -6464,10 +6818,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: @@ -6482,11 +6834,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): @@ -6532,18 +6880,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_. @@ -6551,7 +6951,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_. @@ -6559,13 +6959,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: @@ -6573,7 +7068,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`` and ``weight`` optional properties. CLI Example: @@ -6586,8 +7121,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, @@ -6596,6 +7129,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) @@ -6615,12 +7155,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() @@ -6628,6 +7168,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") + 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. @@ -6687,6 +7492,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 @@ -6694,7 +7509,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(), @@ -7453,37 +8268,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 784cdca73c..c677c9ad84 100644 --- a/salt/states/virt.py +++ b/salt/states/virt.py @@ -161,7 +161,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() @@ -174,15 +175,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) @@ -288,7 +291,6 @@ def defined( arch=None, boot=None, numatune=None, - update=True, boot_dev=None, hypervisor_features=None, clock=None, @@ -296,6 +298,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. @@ -498,10 +501,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``. @@ -595,6 +594,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: @@ -641,31 +647,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) @@ -706,6 +711,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) @@ -731,7 +737,6 @@ def running( install=True, pub_key=None, priv_key=None, - update=False, connection=None, username=None, password=None, @@ -745,6 +750,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. @@ -826,10 +832,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 @@ -962,6 +964,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: @@ -1005,12 +1014,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, @@ -1028,7 +1031,6 @@ def running( os_type=os_type, arch=arch, boot=boot, - update=update, boot_dev=boot_dev, numatune=numatune, hypervisor_features=hypervisor_features, @@ -1039,6 +1041,7 @@ def running( password=password, serials=serials, consoles=consoles, + host_devices=host_devices, ) result = True if not __opts__["test"] else None @@ -1264,21 +1267,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). @@ -1286,6 +1332,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 @@ -1332,9 +1472,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, @@ -1344,14 +1537,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() @@ -1371,14 +1585,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 @@ -1403,6 +1659,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 @@ -1443,6 +1793,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 6ac3e867b9..4603dfd8de 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" %} + <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" -%} - <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..ab14408712 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 name='{{ 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 5c187ca7e5..c91c3f6275 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/conftest.py b/tests/conftest.py index 6922f626f8..27df1f272d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,10 +27,10 @@ import _pytest.logging import _pytest.skipping import psutil import pytest +import salt._logging.impl import salt.config import salt.loader import salt.log.mixins -import salt.log.setup import salt.utils.files import salt.utils.path import salt.utils.platform diff --git a/tests/pytests/functional/modules/test_opkg.py b/tests/pytests/functional/modules/test_opkg.py index 4e1d2f9c20..8b5a690de8 100644 --- a/tests/pytests/functional/modules/test_opkg.py +++ b/tests/pytests/functional/modules/test_opkg.py @@ -8,14 +8,12 @@ from tests.support.mock import patch pytestmark = pytest.mark.skip_if_binaries_missing("stat", "md5sum", "uname") -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = { +@pytest.fixture +def configure_loader_modules(): + return { opkg: {"__salt__": {"cmd.shell": cmd.shell, "cmd.run_stdout": cmd.run_stdout}}, cmd: {}, } - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock def test_conf_d_path_does_not_exist_not_created_by_restart_check(tmp_path): diff --git a/tests/pytests/unit/beacons/test_sensehat.py b/tests/pytests/unit/beacons/test_sensehat.py index 501cb1c69b..b4b964b443 100644 --- a/tests/pytests/unit/beacons/test_sensehat.py +++ b/tests/pytests/unit/beacons/test_sensehat.py @@ -3,9 +3,9 @@ import salt.beacons.sensehat as sensehat from tests.support.mock import MagicMock -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = { +@pytest.fixture +def configure_loader_modules(): + return { sensehat: { "__salt__": { "sensehat.get_humidity": MagicMock(return_value=80), @@ -14,8 +14,6 @@ def setup_loader(): }, } } - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock def test_non_list_config(): diff --git a/tests/pytests/unit/beacons/test_status.py b/tests/pytests/unit/beacons/test_status.py index bb32253c3e..6c010ddd80 100644 --- a/tests/pytests/unit/beacons/test_status.py +++ b/tests/pytests/unit/beacons/test_status.py @@ -10,16 +10,14 @@ import salt.modules.status as status_module from salt.beacons import status -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = { +@pytest.fixture +def configure_loader_modules(): + return { status: { "__salt__": pytest.helpers.salt_loader_module_functions(status_module) }, status_module: {"__grains__": {"kernel": "Linux"}, "__salt__": {}}, } - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock def test_empty_config(): diff --git a/tests/pytests/unit/modules/test_alternatives.py b/tests/pytests/unit/modules/test_alternatives.py index 49c6c5e415..aa05c3f0f4 100644 --- a/tests/pytests/unit/modules/test_alternatives.py +++ b/tests/pytests/unit/modules/test_alternatives.py @@ -4,11 +4,9 @@ from tests.support.helpers import TstSuiteLoggingHandler from tests.support.mock import MagicMock, patch -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {alternatives: {}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock +@pytest.fixture +def configure_loader_modules(): + return {alternatives: {}} def test_display(): diff --git a/tests/pytests/unit/modules/test_ansiblegate.py b/tests/pytests/unit/modules/test_ansiblegate.py index ca5a6ab1ef..42c0968a6e 100644 --- a/tests/pytests/unit/modules/test_ansiblegate.py +++ b/tests/pytests/unit/modules/test_ansiblegate.py @@ -1,4 +1,3 @@ -# # Author: Bo Maryniuk <bo@suse.de> @@ -17,6 +16,11 @@ pytestmark = pytest.mark.skipif( ) +@pytest.fixture +def configure_loader_modules(): + return {ansible: {}} + + @pytest.fixture def resolver(): _resolver = ansible.AnsibleModuleResolver({}) @@ -28,13 +32,6 @@ def resolver(): return _resolver -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {ansible: {}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock - - def test_ansible_module_help(resolver): """ Test help extraction from the module diff --git a/tests/pytests/unit/modules/test_archive.py b/tests/pytests/unit/modules/test_archive.py index c2a7f24d1d..a4dfca8c84 100644 --- a/tests/pytests/unit/modules/test_archive.py +++ b/tests/pytests/unit/modules/test_archive.py @@ -18,11 +18,9 @@ class ZipFileMock(MagicMock): return self._files -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {archive: {"__grains__": {"id": 0}}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock +@pytest.fixture +def configure_loader_modules(): + return {archive: {"__grains__": {"id": 0}}} def test_tar(): diff --git a/tests/pytests/unit/modules/test_azurearm_dns.py b/tests/pytests/unit/modules/test_azurearm_dns.py index de096915a1..d1f42a60d7 100644 --- a/tests/pytests/unit/modules/test_azurearm_dns.py +++ b/tests/pytests/unit/modules/test_azurearm_dns.py @@ -109,8 +109,8 @@ def credentials(): } -@pytest.fixture(autouse=True) -def setup_loader(): +@pytest.fixture +def configure_loader_modules(): """ setup loader modules and override the azurearm.get_client utility """ @@ -120,11 +120,9 @@ def setup_loader(): minion_config, utils=utils, whitelist=["azurearm_dns", "config"] ) utils["azurearm.get_client"] = AzureClientMock() - setup_loader_modules = { + return { azurearm_dns: {"__utils__": utils, "__salt__": funcs}, } - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock def test_record_set_create_or_update(credentials): diff --git a/tests/pytests/unit/modules/test_nilrt_ip.py b/tests/pytests/unit/modules/test_nilrt_ip.py index adf08531dd..3e4bd414e9 100644 --- a/tests/pytests/unit/modules/test_nilrt_ip.py +++ b/tests/pytests/unit/modules/test_nilrt_ip.py @@ -5,11 +5,9 @@ import salt.modules.nilrt_ip as nilrt_ip from tests.support.mock import patch -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {nilrt_ip: {}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock +@pytest.fixture +def configure_loader_modules(): + return {nilrt_ip: {}} @pytest.fixture diff --git a/tests/pytests/unit/modules/test_opkg.py b/tests/pytests/unit/modules/test_opkg.py index e5817eef38..7fd12015e5 100644 --- a/tests/pytests/unit/modules/test_opkg.py +++ b/tests/pytests/unit/modules/test_opkg.py @@ -3,11 +3,9 @@ import salt.modules.opkg as opkg from tests.support.mock import patch -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {opkg: {}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock +@pytest.fixture +def configure_loader_modules(): + return {opkg: {}} def test_when_os_is_NILinuxRT_and_creation_of_RESTART_CHECK_STATE_PATH_fails_virtual_should_be_False(): diff --git a/tests/pytests/unit/modules/test_restartcheck.py b/tests/pytests/unit/modules/test_restartcheck.py index b0c55dd0fe..8b4dc01bca 100644 --- a/tests/pytests/unit/modules/test_restartcheck.py +++ b/tests/pytests/unit/modules/test_restartcheck.py @@ -8,11 +8,9 @@ import salt.modules.systemd_service as service from tests.support.mock import create_autospec, patch -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {restartcheck: {}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock +@pytest.fixture +def configure_loader_modules(): + return {restartcheck: {}} def test_when_timestamp_file_does_not_exist_then_file_changed_nilrt_should_be_True(): diff --git a/tests/pytests/unit/modules/test_slackware_service.py b/tests/pytests/unit/modules/test_slackware_service.py index 047582e668..2fe38c5232 100644 --- a/tests/pytests/unit/modules/test_slackware_service.py +++ b/tests/pytests/unit/modules/test_slackware_service.py @@ -8,6 +8,11 @@ import salt.modules.slackware_service as slackware_service from tests.support.mock import MagicMock, patch +@pytest.fixture +def configure_loader_modules(): + return {slackware_service: {}} + + @pytest.fixture def mocked_rcd(): glob_output = [ @@ -39,13 +44,6 @@ def mocked_rcd(): return glob_mock, os_path_exists_mock, os_access_mock -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {slackware_service: {}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock - - def test_get_all_rc_services_minus_system_and_config_files(mocked_rcd): """ In Slackware, the services are started, stopped, enabled or disabled diff --git a/tests/pytests/unit/modules/test_swarm.py b/tests/pytests/unit/modules/test_swarm.py index e474f89f62..6259d0bd17 100644 --- a/tests/pytests/unit/modules/test_swarm.py +++ b/tests/pytests/unit/modules/test_swarm.py @@ -16,15 +16,13 @@ pytestmark = pytest.mark.skipif( ) -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {swarm: {"__context__": {}}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock +@pytest.fixture +def configure_loader_modules(): + return {swarm: {"__context__": {}}} @pytest.fixture -def fake_context_client(): +def fake_context_client(setup_loader_mock): fake_swarm_client = MagicMock() patch_context = patch.dict( swarm.__context__, {"client": fake_swarm_client, "server_name": "test swarm"} diff --git a/tests/pytests/unit/modules/test_tls.py b/tests/pytests/unit/modules/test_tls.py index d7e79d91ad..a1db1930ee 100644 --- a/tests/pytests/unit/modules/test_tls.py +++ b/tests/pytests/unit/modules/test_tls.py @@ -5,6 +5,11 @@ import salt.modules.tls as tls from tests.support.mock import MagicMock, patch +@pytest.fixture +def configure_loader_modules(): + return {tls: {}} + + @pytest.fixture(scope="module") def tls_test_data(): return { @@ -23,13 +28,6 @@ def tls_test_data(): } -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {tls: {}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock - - def test_create_ca_permissions_on_cert_and_key(tmpdir, tls_test_data): ca_name = "test_ca" certp = tmpdir.join(ca_name).join("{}_ca_cert.crt".format(ca_name)).strpath diff --git a/tests/pytests/unit/modules/virt/conftest.py b/tests/pytests/unit/modules/virt/conftest.py index ec56bdff24..3bacd734a7 100644 --- a/tests/pytests/unit/modules/virt/conftest.py +++ b/tests/pytests/unit/modules/virt/conftest.py @@ -43,32 +43,29 @@ 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) -def setup_loader(request): +def loader_modules_config(): # Create libvirt mock and connection mock mock_libvirt = LibvirtMock() mock_conn = MagicMock() mock_conn.getStoragePoolCapabilities.return_value = "<storagepoolCapabilities/>" mock_libvirt.openAuth.return_value = mock_conn - setup_loader_modules = { + return { virt: { "libvirt": mock_libvirt, "__salt__": {"config.get": config.get, "config.option": config.option}, }, config: {}, } - with pytest.helpers.loader_mock(request, setup_loader_modules) as loader_mock: - yield loader_mock @pytest.fixture def make_mock_vm(): - def _make_mock_vm(xml_def): + def _make_mock_vm(xml_def, running=False, inactive_def=None): mocked_conn = virt.libvirt.openAuth.return_value doc = ET.fromstring(xml_def) @@ -81,17 +78,21 @@ 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) domain_mock = mocked_conn.lookupByName(name) - domain_mock.XMLDesc.return_value = xml_def + + domain_mock.XMLDesc = MappedResultMock() + domain_mock.XMLDesc.add(0, xml_def) + domain_mock.XMLDesc.add( + virt.libvirt.VIR_DOMAIN_XML_INACTIVE, inactive_def or xml_def + ) domain_mock.OSType.return_value = os_type # Return state as shutdown domain_mock.info.return_value = [ - 4, + 0 if running else 4, 2048 * 1024, 1024 * 1024, 2, @@ -103,6 +104,8 @@ def make_mock_vm(): domain_mock.attachDevice.return_value = 0 domain_mock.detachDevice.return_value = 0 + domain_mock.connect.return_value = mocked_conn + return domain_mock return _make_mock_vm @@ -315,3 +318,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 347c3bcd88..0bde881403 100644 --- a/tests/pytests/unit/modules/virt/test_domain.py +++ b/tests/pytests/unit/modules/virt/test_domain.py @@ -1,8 +1,16 @@ +import pytest import salt.modules.virt as virt +import salt.utils.xmlutil as xmlutil from salt._compat import ElementTree as ET from tests.support.mock import MagicMock, patch -from .test_helpers import append_to_XMLDesc +from .conftest import loader_modules_config +from .test_helpers import append_to_XMLDesc, assert_called, strip_xml + + +@pytest.fixture +def configure_loader_modules(): + return loader_modules_config() def test_update_xen_disk_volumes(make_mock_vm, make_mock_storage_pool): @@ -589,3 +597,466 @@ def test_init_stop_on_reboot(make_capabilities): define_mock = virt.libvirt.openAuth().defineXML setxml = ET.fromstring(define_mock.call_args[0][0]) assert "destroy" == setxml.find("./on_reboot").text + + +def test_init_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 f64aee2821..5410f45603 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 @@ -9,3 +10,27 @@ def append_to_XMLDesc(mocked, fragment): xml_fragment = ET.fromstring(fragment) xml_doc.append(xml_fragment) mocked.XMLDesc.return_value = ET.tostring(xml_doc) + + +def assert_xml_equals(expected, actual): + """ + Assert that two ElementTree nodes are equal + """ + assert xmlutil.to_dict(xmlutil.strip_spaces(expected), True) == xmlutil.to_dict( + xmlutil.strip_spaces(actual), True + ) + + +def strip_xml(xml_str): + """ + Remove all spaces and formatting from an XML string + """ + return ET.tostring(xmlutil.strip_spaces(ET.fromstring(xml_str))) + + +def assert_called(mock, condition): + """ + Assert that the mock has been called if not in test mode, and vice-versa. + I know it's a simple XOR, but makes the tests easier to read + """ + assert not condition and not mock.called or condition and mock.called 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..e7e544c580 --- /dev/null +++ b/tests/pytests/unit/modules/virt/test_network.py @@ -0,0 +1,450 @@ +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 name='srv1' protocol='tcp' port='1024' target='.' priority='10' weight='10' domain='test-domain-name'/> + <srv name='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", [True, False]) +def test_update_nat_change(make_mock_network, test): + """ + 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' netmask='255.255.255.0'> + <dhcp> + <range start='192.168.122.2' end='192.168.122.254'/> + </dhcp> + </ip> + </network> + """ + ) + 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/output/test_highstate.py b/tests/pytests/unit/output/test_highstate.py index 8336208bae..53eaf6fde7 100644 --- a/tests/pytests/unit/output/test_highstate.py +++ b/tests/pytests/unit/output/test_highstate.py @@ -2,11 +2,9 @@ import pytest import salt.output.highstate as highstate -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {highstate: {"__opts__": {"strip_colors": True}}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock +@pytest.fixture +def configure_loader_modules(): + return {highstate: {"__opts__": {"strip_colors": True}}} @pytest.mark.parametrize("data", [None, {"return": None}, {"return": {"data": None}}]) diff --git a/tests/pytests/unit/states/test_alternatives.py b/tests/pytests/unit/states/test_alternatives.py index 7bdcdb97cb..de0bc509b9 100644 --- a/tests/pytests/unit/states/test_alternatives.py +++ b/tests/pytests/unit/states/test_alternatives.py @@ -7,11 +7,9 @@ import salt.states.alternatives as alternatives from tests.support.mock import MagicMock, patch -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = {alternatives: {}} - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock +@pytest.fixture +def configure_loader_modules(): + return {alternatives: {}} # 'install' function tests: 1 diff --git a/tests/pytests/unit/states/test_ini_manage.py b/tests/pytests/unit/states/test_ini_manage.py index b0030793da..2b44e2ffd6 100644 --- a/tests/pytests/unit/states/test_ini_manage.py +++ b/tests/pytests/unit/states/test_ini_manage.py @@ -9,17 +9,8 @@ from tests.support.mock import patch @pytest.fixture -def sections(): - sections = OrderedDict() - sections["general"] = OrderedDict() - sections["general"]["hostname"] = "myserver.com" - sections["general"]["port"] = "1234" - return sections - - -@pytest.fixture(autouse=True) -def setup_loader(): - setup_loader_modules = { +def configure_loader_modules(): + return { ini_manage: { "__salt__": { "ini.get_ini": mod_ini_manage.get_ini, @@ -29,8 +20,15 @@ def setup_loader(): }, mod_ini_manage: {"__opts__": {"test": False}}, } - with pytest.helpers.loader_mock(setup_loader_modules) as loader_mock: - yield loader_mock + + +@pytest.fixture +def sections(): + sections = OrderedDict() + sections["general"] = OrderedDict() + sections["general"]["hostname"] = "myserver.com" + sections["general"]["port"] = "1234" + return sections def test_options_present(tmpdir, sections): 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_linux_sysctl.py b/tests/unit/modules/test_linux_sysctl.py deleted file mode 100644 index 7f463bb7ab..0000000000 --- a/tests/unit/modules/test_linux_sysctl.py +++ /dev/null @@ -1,173 +0,0 @@ -""" - :codeauthor: jmoney <justin@saltstack.com> -""" - - -import salt.modules.linux_sysctl as linux_sysctl -import salt.modules.systemd_service as systemd -from salt.exceptions import CommandExecutionError -from tests.support.mixins import LoaderModuleMockMixin -from tests.support.mock import MagicMock, mock_open, patch -from tests.support.unit import TestCase - - -class LinuxSysctlTestCase(TestCase, LoaderModuleMockMixin): - """ - TestCase for salt.modules.linux_sysctl module - """ - - def setup_loader_modules(self): - return {linux_sysctl: {}, systemd: {}} - - def test_get(self): - """ - Tests the return of get function - """ - mock_cmd = MagicMock(return_value=1) - with patch.dict(linux_sysctl.__salt__, {"cmd.run": mock_cmd}): - self.assertEqual(linux_sysctl.get("net.ipv4.ip_forward"), 1) - - def test_assign_proc_sys_failed(self): - """ - Tests if /proc/sys/<kernel-subsystem> exists or not - """ - with patch("os.path.exists", MagicMock(return_value=False)): - cmd = { - "pid": 1337, - "retcode": 0, - "stderr": "", - "stdout": "net.ipv4.ip_forward = 1", - } - mock_cmd = MagicMock(return_value=cmd) - with patch.dict(linux_sysctl.__salt__, {"cmd.run_all": mock_cmd}): - self.assertRaises( - CommandExecutionError, linux_sysctl.assign, "net.ipv4.ip_forward", 1 - ) - - def test_assign_cmd_failed(self): - """ - Tests if the assignment was successful or not - """ - with patch("os.path.exists", MagicMock(return_value=True)): - cmd = { - "pid": 1337, - "retcode": 0, - "stderr": 'sysctl: setting key "net.ipv4.ip_forward": Invalid argument', - "stdout": "net.ipv4.ip_forward = backward", - } - mock_cmd = MagicMock(return_value=cmd) - with patch.dict(linux_sysctl.__salt__, {"cmd.run_all": mock_cmd}): - self.assertRaises( - CommandExecutionError, - linux_sysctl.assign, - "net.ipv4.ip_forward", - "backward", - ) - - def test_assign_success(self): - """ - Tests the return of successful assign function - """ - with patch("os.path.exists", MagicMock(return_value=True)): - cmd = { - "pid": 1337, - "retcode": 0, - "stderr": "", - "stdout": "net.ipv4.ip_forward = 1", - } - ret = {"net.ipv4.ip_forward": "1"} - mock_cmd = MagicMock(return_value=cmd) - with patch.dict(linux_sysctl.__salt__, {"cmd.run_all": mock_cmd}): - self.assertEqual(linux_sysctl.assign("net.ipv4.ip_forward", 1), ret) - - def test_persist_no_conf_failure(self): - """ - Tests adding of config file failure - """ - asn_cmd = { - "pid": 1337, - "retcode": 0, - "stderr": "sysctl: permission denied", - "stdout": "", - } - mock_asn_cmd = MagicMock(return_value=asn_cmd) - cmd = "sysctl -w net.ipv4.ip_forward=1" - mock_cmd = MagicMock(return_value=cmd) - with patch.dict( - linux_sysctl.__salt__, - {"cmd.run_stdout": mock_cmd, "cmd.run_all": mock_asn_cmd}, - ): - with patch("salt.utils.files.fopen", mock_open()) as m_open: - self.assertRaises( - CommandExecutionError, - linux_sysctl.persist, - "net.ipv4.ip_forward", - 1, - config=None, - ) - - def test_persist_no_conf_success(self): - """ - Tests successful add of config file when previously not one - """ - config = "/etc/sysctl.conf" - with patch("os.path.isfile", MagicMock(return_value=False)), patch( - "os.path.exists", MagicMock(return_value=True) - ): - asn_cmd = { - "pid": 1337, - "retcode": 0, - "stderr": "", - "stdout": "net.ipv4.ip_forward = 1", - } - mock_asn_cmd = MagicMock(return_value=asn_cmd) - - sys_cmd = "systemd 208\n+PAM +LIBWRAP" - mock_sys_cmd = MagicMock(return_value=sys_cmd) - - with patch("salt.utils.files.fopen", mock_open()) as m_open, patch.dict( - linux_sysctl.__context__, {"salt.utils.systemd.version": 232} - ), patch.dict( - linux_sysctl.__salt__, - {"cmd.run_stdout": mock_sys_cmd, "cmd.run_all": mock_asn_cmd}, - ), patch.dict( - systemd.__context__, - {"salt.utils.systemd.booted": True, "salt.utils.systemd.version": 232}, - ): - linux_sysctl.persist("net.ipv4.ip_forward", 1, config=config) - writes = m_open.write_calls() - assert writes == ["#\n# Kernel sysctl configuration\n#\n"], writes - - def test_persist_read_conf_success(self): - """ - Tests sysctl.conf read success - """ - with patch("os.path.isfile", MagicMock(return_value=True)), patch( - "os.path.exists", MagicMock(return_value=True) - ): - asn_cmd = { - "pid": 1337, - "retcode": 0, - "stderr": "", - "stdout": "net.ipv4.ip_forward = 1", - } - mock_asn_cmd = MagicMock(return_value=asn_cmd) - - sys_cmd = "systemd 208\n+PAM +LIBWRAP" - mock_sys_cmd = MagicMock(return_value=sys_cmd) - - with patch("salt.utils.files.fopen", mock_open()): - with patch.dict( - linux_sysctl.__context__, {"salt.utils.systemd.version": 232} - ): - with patch.dict( - linux_sysctl.__salt__, - {"cmd.run_stdout": mock_sys_cmd, "cmd.run_all": mock_asn_cmd}, - ): - with patch.dict( - systemd.__context__, {"salt.utils.systemd.booted": True} - ): - self.assertEqual( - linux_sysctl.persist("net.ipv4.ip_forward", 1), - "Updated", - ) diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index 91dee2098d..f717513944 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -598,7 +598,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"}, ], @@ -1881,70 +1881,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 @@ -3160,7 +3096,12 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): "source_file": None, "model": "ide", }, - {"name": "added", "size": 2048, "iothreads": True}, + { + "name": "added", + "size": 2048, + "io": "threads", + "iothread_id": 2, + }, ], ) added_disk_path = os.path.join( @@ -3196,6 +3137,9 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual( "threads", setxml.find("devices/disk[3]/driver").get("io") ) + self.assertEqual( + "2", setxml.find("devices/disk[3]/driver").get("iothread") + ) # Update nics case yaml_config = """ @@ -3245,7 +3189,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): devattach_mock.reset_mock() devdetach_mock.reset_mock() ret = virt.update("my_vm", nic_profile=None, interfaces=[]) - self.assertEqual([], ret["interface"]["attached"]) + self.assertFalse(ret["interface"].get("attached")) self.assertEqual(2, len(ret["interface"]["detached"])) devattach_mock.assert_not_called() devdetach_mock.assert_called() @@ -3254,7 +3198,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): devattach_mock.reset_mock() devdetach_mock.reset_mock() ret = virt.update("my_vm", disk_profile=None, disks=[]) - self.assertEqual([], ret["disk"]["attached"]) + self.assertFalse(ret["disk"].get("attached")) self.assertEqual(3, len(ret["disk"]["detached"])) devattach_mock.assert_not_called() devdetach_mock.assert_called() @@ -3540,8 +3484,8 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): ) self.assertTrue(ret["definition"]) - self.assertFalse(ret["disk"]["attached"]) - self.assertFalse(ret["disk"]["detached"]) + self.assertFalse(ret["disk"].get("attached")) + self.assertFalse(ret["disk"].get("detached")) self.assertEqual( [ { @@ -6119,59 +6063,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.29.2
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor