Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:tulip
salt
opensuse-3000.2-virt-backports-236-257.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File opensuse-3000.2-virt-backports-236-257.patch of Package salt
From 895231d5605e63882de015d7b99cea36e9174d66 Mon Sep 17 00:00:00 2001 From: Cedric Bosdonnat <cbosdonnat@suse.com> Date: Tue, 1 Sep 2020 10:38:39 +0200 Subject: [PATCH] Opensuse 3000.2 virt backports (#236) (#257) * Revert "virt._get_domain: don't raise an exception if there is no VM" This reverts commit 9d433776fc252ab872b331cfa7fc940c42452f83. * Revert "openSUSE-3000 virt-defined-states (#222)" This reverts commit 900fdc4d5bcfdac933f9a411d38b92ef47dd1ef5. * Revert "Add virt.all_capabilities" This reverts commit 96e8307b531f7e51f5587f66a51dc52ad246c8c1. * Blacken salt -- virt only * virt._get_domain: don't raise an exception if there is no VM Raising an exception if there is no VM in _get_domain makes sense if looking for some VMs, but not when listing all VMs. * fixed bug on update existing boot parameters * add support to boot vm with UEFI * virt: don't depend on ElementTree.tostring to compare XML fragments ElementTree.tostring() implementation starts to keep the attribute order defined by the user in python 3.8. This can lead to equal elements to be considered different. Use xmlutil.to_dict(element, True) to compare the elements. This also uncovered a wrong behavior of pool_update when only changing the password. * openSUSE-3000 virt-defined-states (#222) * Create virt.pool_defined state out of virt.pool_running Users may want to use states to ensure a virtual storage pool is defined and not enforce it to be running. Extract the code that performs the pool definition / update from virt.pool_running state into a virt.pool_defined. Obviously the virt.pool_running state calls the virt.pool_defined one. In such a case no additionnal test is needed for virt.pool_defined since this is already tested with virt.pool_running. * Add virt.update test parameter In order to allow running dry-runs of virt.update module add a test parameter. This will later be used by the virt states. * Extract virt.defined state from virt.running In order to ensure a virtual guest is defined independently of its status, extract the corresponding code from the virt.running state. This commit also handles the __opts__['test'] for the running state. Since the update call only performs changes if needed, deprecate the update parameter. * Extract virt.network_defined from virt.network_running Just like domains and storage pools, users may want to ensure a network is defined without influencing it's status. Extract the code from network_running state defining the network into a network_defined state. While at it, support __opt__['test'] == True in these states. Updating the network definition in the pool_defined state will come in a future PR. * Fix virt.update to handle None mem and cpu virt.running state now may call virt.update with None mem and cpu parameters. This was not handled in _gen_xml(). Also add some more tests cases matching this for virt.update. * Add support to virt for libvirt loader * Add bhyve compatibility to virt * Fix test_get_hypervisor: mock bhyve * Add virt.all_capabilities In order to get all possible capabilities from a host, the user has to call virt.capabilities, and then loop over the guests and domains before calling virt.domain_capabilities for each of them. This commit embeds all this logic to get them all in a single virt.all_capabilities call. * Virt init disks support (PR#56666) * Fix pylint warning in test_virt.py after blackening * Add pool parameter to virt.define_vol_xml_str * Remove useless default values for disks and vm_name in _disk_profile * virt._gen_vol_xml: move all esx-specifics outside In the near future gen_vol_xml will be able to handle many volume types, not only for ESX volumes. For this, clean up the function from all the ESX-specifics code and move them to the caller code. The volume key and target path values have also been removed since those are read-only elements that should not be provided for volume creation as per https://libvirt.org/formatstorage.html#StorageVol * virt: add more properties to generate volume XML In order to generate almost all the volumes that libvirt can handle, add the type, backingStore, permissions and allocation parameters to the virt._gen_vol_xml() function. Also make the format parameter optional since libvirt has default values depending on the storage backend. * virt.define_vol_xml_str variant using an existing libvirt connection In order to avoid connection multiple times when reusing this function in the virt module, create _define_vol_xml_str not caring about the connection opening and closing. * Add virt.volume_define function In the same vein than pool_define and network_define, expose a volume_define function to let users create a volume without needing to know all of libvirt's XML details. * Add virt.volume_upload function When using volumes the user can just copy the template disk image into the target path. In such a case, the image needs to be uploaded into the volume. * virt.capabilities refactoring Extract the libvirt-handling code from virt.capabilities into a virt._capabilities function accepting an opened libvirt connection. This allows reusing the code in other functions with easy connection handling. * Extract virt.pool_capabilities logic for use with a libvirt connection Te virt.pool_capabilities function computes a lot of interesting values on the virtual storage pool types. Extract the logic of it into virt._pool_capabilities for reuse. * Share libvirt connection in virt.init Since the next commits will introduce more uses of the libvirt connection in virt.init(), start sharing it now. * Add disk volumes support in virt.init In order to support creating VMs with disk on more storage pools like iSCSI, disks, LVM, RBD, etc, virt.init needs to handle disks not only as files, but also as libvirt volumes. * fix libvirtError use libvirtError is not defined, libvirt.libvirtError should be used instead. * virt: let libvirt generate MAC addresses There is no need to generate MAC addresses in the virt module if the user hasn't provided any. This only makes it harder to make the difference between a real mac address change from the user and a new generated one. Now the mac address is not written in the domain XML definition if not provided by the user. This avoids unnecessary changes when applying virt.running. * virt.update handle disk volumes * virt.volume_infos: output backing store as well Since it could be useful to know whether a volume has a backing store, output the path and format of the backing store if any is defined. * virt.volume_infos: output disk format Since the format of a volume may be of interest and could help to tell if two volumes are similar, output this information in the virt.volume_infos function. * Add virt.volume_defined state In order to help creating storage volumes in virtual storage pools from states, add a virt.volume_defined state. * Add volume support to virt._get_disks If a virtual machine has disks of volume types, they will now be reported by the virt._get_disk() function and all the user-exposed functions using it like virt.get_disks(), virt.vm_info() and virt.full_info(). * Add volume disks support to virt.purge() virt.purge will now remove not only the file disks, but also the disk volumes. * Handle RBD volumes as network disks in VM definition libvirt doesn't support attaching RBD storage pool volumes to a VM. For instance, a disk with such a source is invalid: <source pool='rbd-pool' volume='rbd-volume'/> And needs to be replaced with: <source protocol="rbd" name="rbd-pool-name/rbd-volume"> <host name="hostname" port="7000"/> <auth username='myuser'> <secret type='ceph' usage='mypassid'/> </auth> </source> This means that we need to fetch the connection data from the pool definition and convert the volume disk definition into a network one when creating the VM. This also means that when purging the VM we need to search for the pool by looking in every pool's XML to delete the volume. * virt: fix VM creating with disk volume Since volumes in a virtual storage pool of type 'disk' are partitions, then need to be named after the disk name with sequential numbers rather than using the VM and disk names. Also, the format passed by the user is the one used when creating the volume. However in the VM definition for logical and disk volumes, the format should be set to raw. * virt: handle cdrom remote images Libvirt allows to use network images for cdroms. Use them if the image is a remote URL for a cdrom device. * virt.update properly handle removable devices Live attaching / detaching removable devices results in failures. To change the source of those, we need to call updateDeviceFlags instead. * improve boot parameter documentation (PR#57086) * Remove buggy start parameter from virt.pool_running docstring As mentioned by issue #57275, the start parameter in virt.pool_running documentation is not implemented at all. Remove it from the doc. * virt: fix removable_changes definition place * virt: show the proper pool name in get_disks From the user point of view the internal RBD pool name doesn't make sense when listing the disks of a VM. We need then to resolve it to the libvirt pool name if possible. Move the corresponding code from purge to get_disks. * virt: fix updating VM with backing store disks libvirt adds the index attribute in the XML definition when there is a backing store file for a disk. We need to elude it before comparing the sources to avoid trying to recreate disks in such cases. * virt: default cdrom model to ide cdrom devices can't use virtio. Set the default bus to ide for cdroms. * virt.get_disk: output the full URL for cdroms with remote image virt._gen_xml converts the cdroms with remote URL into a network device, but to be coherent with the user input the virt.get_disk() function needs to reassemble the URL in thoses cases. * virt.pool_delete: remove also secret Some pool type require a secret for the authentication on the remote source. Remove the secrets that were added by pool_defined but leave the others in place. * virt.init: cdrom format default to raw cdrom sources can't be of format qcow2. Force raw as the default if needed when creating VM with source_file set for the cdrom. * virt.init: fix disk target names Fixes issue #57477. * virt.update: handle changing cdrom images for devices with remote source When a DVD device on a VM has a remote source, virt.update needs to be able to handle detaching it and attaching a file image live. * virt.init: fix the name of volumes reused in disk-types pools Only compute the partition name if no source_file was provided by the user for a pool of disk type. Co-authored-by: Blacken Salt <jenkins@saltstack.com> Co-authored-by: Firefly <leevn2011@hotmail.com> Co-authored-by: Jeroen Schutrup <jeroenschutrup@hotmail.nl> Co-authored-by: ch3ll <megan.wilhite@gmail.com> Co-authored-by: Blacken Salt <jenkins@saltstack.com> Co-authored-by: Firefly <leevn2011@hotmail.com> Co-authored-by: Jeroen Schutrup <jeroenschutrup@hotmail.nl> Co-authored-by: ch3ll <megan.wilhite@gmail.com> --- changelog/57005.added | 1 + changelog/57275.fixed | 1 + changelog/57477.fixed | 1 + changelog/57497.fixed | 1 + salt/modules/virt.py | 1080 ++++++------------- salt/states/virt.py | 201 +--- salt/templates/virt/libvirt_domain.jinja | 16 +- tests/unit/modules/test_virt.py | 1217 ++++++++++++++++------ tests/unit/states/test_virt.py | 755 +------------- 9 files changed, 1265 insertions(+), 2008 deletions(-) create mode 100644 changelog/57005.added create mode 100644 changelog/57275.fixed create mode 100644 changelog/57477.fixed create mode 100644 changelog/57497.fixed diff --git a/changelog/57005.added b/changelog/57005.added new file mode 100644 index 0000000000..6704541509 --- /dev/null +++ b/changelog/57005.added @@ -0,0 +1 @@ +Add support for disks volumes in virt.running state diff --git a/changelog/57275.fixed b/changelog/57275.fixed new file mode 100644 index 0000000000..6efbe48315 --- /dev/null +++ b/changelog/57275.fixed @@ -0,0 +1 @@ +Remove buggy start parameter from virt.pool_running docstring diff --git a/changelog/57477.fixed b/changelog/57477.fixed new file mode 100644 index 0000000000..f32f32fdfc --- /dev/null +++ b/changelog/57477.fixed @@ -0,0 +1 @@ +virt.init fix the disk target names diff --git a/changelog/57497.fixed b/changelog/57497.fixed new file mode 100644 index 0000000000..24697e108d --- /dev/null +++ b/changelog/57497.fixed @@ -0,0 +1 @@ +Fix volume name for disk-typed pools in virt.defined diff --git a/salt/modules/virt.py b/salt/modules/virt.py index 7314bf1d6e..fb27397baa 100644 --- a/salt/modules/virt.py +++ b/salt/modules/virt.py @@ -71,50 +71,6 @@ The calls not using the libvirt connection setup are: - `libvirt URI format <http://libvirt.org/uri.html#URI_config>`_ - `libvirt authentication configuration <http://libvirt.org/auth.html#Auth_client_config>`_ -Units -========== -.. _virt-units: -.. rubric:: Units specification -.. versionadded:: 3002 - -The string should contain a number optionally followed -by a unit. The number may have a decimal fraction. If -the unit is not given then MiB are set by default. -Units can optionally be given in IEC style (such as MiB), -although the standard single letter style (such as M) is -more convenient. - -Valid units include: - -========== ===== ========== ========== ====== -Standard IEC Standard IEC - Unit Unit Name Name Factor -========== ===== ========== ========== ====== - B Bytes 1 - K KiB Kilobytes Kibibytes 2**10 - M MiB Megabytes Mebibytes 2**20 - G GiB Gigabytes Gibibytes 2**30 - T TiB Terabytes Tebibytes 2**40 - P PiB Petabytes Pebibytes 2**50 - E EiB Exabytes Exbibytes 2**60 - Z ZiB Zettabytes Zebibytes 2**70 - Y YiB Yottabytes Yobibytes 2**80 -========== ===== ========== ========== ====== - -Additional decimal based units: - -====== ======= -Unit Factor -====== ======= -KB 10**3 -MB 10**6 -GB 10**9 -TB 10**12 -PB 10**15 -EB 10**18 -ZB 10**21 -YB 10**24 -====== ======= """ # Special Thanks to Michael Dehann, many of the concepts, and a few structures # of his in the virt func module have been used @@ -136,19 +92,22 @@ from xml.etree import ElementTree from xml.sax import saxutils import jinja2.exceptions -import salt.utils.data import salt.utils.files import salt.utils.json +import salt.utils.network import salt.utils.path import salt.utils.stringutils import salt.utils.templates -import salt.utils.virt +import salt.utils.validate.net +import salt.utils.versions import salt.utils.xmlutil as xmlutil import salt.utils.yaml from salt._compat import ipaddress from salt.exceptions import CommandExecutionError, SaltInvocationError +from salt.ext import six from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin from salt.ext.six.moves.urllib.parse import urlparse, urlunparse +from salt.utils.virt import check_remote, download_remote try: import libvirt # pylint: disable=import-error @@ -499,8 +458,6 @@ def _get_disks(conn, dom): """ disks = {} doc = ElementTree.fromstring(dom.XMLDesc(0)) - # Get the path, pool, volume name of each volume we can - all_volumes = _get_all_volumes_paths(conn) for elem in doc.findall("devices/disk"): source = elem.find("source") if source is None: @@ -513,61 +470,13 @@ def _get_disks(conn, dom): extra_properties = None if "dev" in target.attrib: disk_type = elem.get("type") - - def _get_disk_volume_data(pool_name, volume_name): - qemu_target = "{}/{}".format(pool_name, volume_name) - pool = conn.storagePoolLookupByName(pool_name) - vol = pool.storageVolLookupByName(volume_name) - vol_info = vol.info() - extra_properties = { - "virtual size": vol_info[1], - "disk size": vol_info[2], - } - - backing_files = [ - { - "file": node.find("source").get("file"), - "file format": node.find("format").get("type"), - } - for node in elem.findall(".//backingStore[source]") - ] - - if backing_files: - # We had the backing files in a flat list, nest them again. - extra_properties["backing file"] = backing_files[0] - parent = extra_properties["backing file"] - for sub_backing_file in backing_files[1:]: - parent["backing file"] = sub_backing_file - parent = sub_backing_file - - else: - # In some cases the backing chain is not displayed by the domain definition - # Try to see if we have some of it in the volume definition. - vol_desc = ElementTree.fromstring(vol.XMLDesc()) - backing_path = vol_desc.find("./backingStore/path") - backing_format = vol_desc.find("./backingStore/format") - if backing_path is not None: - extra_properties["backing file"] = {"file": backing_path.text} - if backing_format is not None: - extra_properties["backing file"][ - "file format" - ] = backing_format.get("type") - return (qemu_target, extra_properties) - if disk_type == "file": qemu_target = source.get("file", "") if qemu_target.startswith("/dev/zvol/"): disks[target.get("dev")] = {"file": qemu_target, "zfs": True} continue - - if qemu_target in all_volumes.keys(): - # If the qemu_target is a known path, output a volume - volume = all_volumes[qemu_target] - qemu_target, extra_properties = _get_disk_volume_data( - volume["pool"], volume["name"] - ) - elif elem.get("device", "disk") != "cdrom": - # Extract disk sizes, snapshots, backing files + # Extract disk sizes, snapshots, backing files + if elem.get("device", "disk") != "cdrom": try: stdout = subprocess.Popen( [ @@ -589,12 +498,6 @@ def _get_disks(conn, dom): disk.update({"file": "Does not exist"}) elif disk_type == "block": qemu_target = source.get("dev", "") - # If the qemu_target is a known path, output a volume - if qemu_target in all_volumes.keys(): - volume = all_volumes[qemu_target] - qemu_target, extra_properties = _get_disk_volume_data( - volume["pool"], volume["name"] - ) elif disk_type == "network": qemu_target = source.get("protocol") source_name = source.get("name") @@ -633,9 +536,43 @@ def _get_disks(conn, dom): elif disk_type == "volume": pool_name = source.get("pool") volume_name = source.get("volume") - qemu_target, extra_properties = _get_disk_volume_data( - pool_name, volume_name - ) + qemu_target = "{}/{}".format(pool_name, volume_name) + pool = conn.storagePoolLookupByName(pool_name) + vol = pool.storageVolLookupByName(volume_name) + vol_info = vol.info() + extra_properties = { + "virtual size": vol_info[1], + "disk size": vol_info[2], + } + + backing_files = [ + { + "file": node.find("source").get("file"), + "file format": node.find("format").get("type"), + } + for node in elem.findall(".//backingStore[source]") + ] + + if backing_files: + # We had the backing files in a flat list, nest them again. + extra_properties["backing file"] = backing_files[0] + parent = extra_properties["backing file"] + for sub_backing_file in backing_files[1:]: + parent["backing file"] = sub_backing_file + parent = sub_backing_file + + else: + # In some cases the backing chain is not displayed by the domain definition + # Try to see if we have some of it in the volume definition. + vol_desc = ElementTree.fromstring(vol.XMLDesc()) + backing_path = vol_desc.find("./backingStore/path") + backing_format = vol_desc.find("./backingStore/format") + if backing_path is not None: + extra_properties["backing file"] = {"file": backing_path.text} + if backing_format is not None: + extra_properties["backing file"][ + "file format" + ] = backing_format.get("type") if not qemu_target: continue @@ -676,258 +613,26 @@ def _libvirt_creds(): return {"user": user, "group": group} -def _migrate(dom, dst_uri, **kwargs): +def _get_migrate_command(): """ - Migrate the domain object from its current host to the destination - host given by URI. - - :param dom: domain object to migrate - :param dst_uri: destination URI - :param kwargs: - - live: Use live migration. Default value is True. - - persistent: Leave the domain persistent on destination host. - Default value is True. - - undefinesource: Undefine the domain on the source host. - Default value is True. - - offline: If set to True it will migrate the domain definition - without starting the domain on destination and without - stopping it on source host. Default value is False. - - max_bandwidth: The maximum bandwidth (in MiB/s) that will be used. - - max_downtime: Set maximum tolerable downtime for live-migration. - The value represents a number of milliseconds the guest - is allowed to be down at the end of live migration. - - parallel_connections: Specify a number of parallel network connections - to be used to send memory pages to the destination host. - - compressed: Activate compression. - - comp_methods: A comma-separated list of compression methods. Supported - methods are "mt" and "xbzrle" and can be used in any - combination. QEMU defaults to "xbzrle". - - comp_mt_level: Set compression level. Values are in range from 0 to 9, - where 1 is maximum speed and 9 is maximum compression. - - comp_mt_threads: Set number of compress threads on source host. - - comp_mt_dthreads: Set number of decompress threads on target host. - - comp_xbzrle_cache: Set the size of page cache for xbzrle compression in bytes. - - copy_storage: Migrate non-shared storage. It must be one of the following - values: all (full disk copy) or incremental (Incremental copy) - - postcopy: Enable the use of post-copy migration. - - postcopy_bandwidth: The maximum bandwidth allowed in post-copy phase. (MiB/s) - - username: Username to connect with target host - - password: Password to connect with target host + Returns the command shared by the different migration types """ - flags = 0 - params = {} - migrated_state = libvirt.VIR_DOMAIN_RUNNING_MIGRATED - - if kwargs.get("live", True): - flags |= libvirt.VIR_MIGRATE_LIVE - - if kwargs.get("persistent", True): - flags |= libvirt.VIR_MIGRATE_PERSIST_DEST - - if kwargs.get("undefinesource", True): - flags |= libvirt.VIR_MIGRATE_UNDEFINE_SOURCE - - max_bandwidth = kwargs.get("max_bandwidth") - if max_bandwidth: - try: - bandwidth_value = int(max_bandwidth) - except ValueError: - raise SaltInvocationError( - "Invalid max_bandwidth value: {}".format(max_bandwidth) - ) - dom.migrateSetMaxSpeed(bandwidth_value) - - max_downtime = kwargs.get("max_downtime") - if max_downtime: - try: - downtime_value = int(max_downtime) - except ValueError: - raise SaltInvocationError( - "Invalid max_downtime value: {}".format(max_downtime) - ) - dom.migrateSetMaxDowntime(downtime_value) - - if kwargs.get("offline") is True: - flags |= libvirt.VIR_MIGRATE_OFFLINE - migrated_state = libvirt.VIR_DOMAIN_RUNNING_UNPAUSED - - if kwargs.get("compressed") is True: - flags |= libvirt.VIR_MIGRATE_COMPRESSED - - comp_methods = kwargs.get("comp_methods") - if comp_methods: - params[libvirt.VIR_MIGRATE_PARAM_COMPRESSION] = comp_methods.split(",") - - comp_options = { - "comp_mt_level": libvirt.VIR_MIGRATE_PARAM_COMPRESSION_MT_LEVEL, - "comp_mt_threads": libvirt.VIR_MIGRATE_PARAM_COMPRESSION_MT_THREADS, - "comp_mt_dthreads": libvirt.VIR_MIGRATE_PARAM_COMPRESSION_MT_DTHREADS, - "comp_xbzrle_cache": libvirt.VIR_MIGRATE_PARAM_COMPRESSION_XBZRLE_CACHE, - } - - for (comp_option, param_key) in comp_options.items(): - comp_option_value = kwargs.get(comp_option) - if comp_option_value: - try: - params[param_key] = int(comp_option_value) - except ValueError: - raise SaltInvocationError("Invalid {} value".format(comp_option)) - - parallel_connections = kwargs.get("parallel_connections") - if parallel_connections: - try: - params[libvirt.VIR_MIGRATE_PARAM_PARALLEL_CONNECTIONS] = int( - parallel_connections - ) - except ValueError: - raise SaltInvocationError("Invalid parallel_connections value") - flags |= libvirt.VIR_MIGRATE_PARALLEL - - if __salt__["config.get"]("virt:tunnel"): - if parallel_connections: - raise SaltInvocationError( - "Parallel migration isn't compatible with tunneled migration" - ) - flags |= libvirt.VIR_MIGRATE_PEER2PEER - flags |= libvirt.VIR_MIGRATE_TUNNELLED - - if kwargs.get("postcopy") is True: - flags |= libvirt.VIR_MIGRATE_POSTCOPY - - postcopy_bandwidth = kwargs.get("postcopy_bandwidth") - if postcopy_bandwidth: - try: - postcopy_bandwidth_value = int(postcopy_bandwidth) - except ValueError: - raise SaltInvocationError("Invalid postcopy_bandwidth value") - dom.migrateSetMaxSpeed( - postcopy_bandwidth_value, - flags=libvirt.VIR_DOMAIN_MIGRATE_MAX_SPEED_POSTCOPY, - ) - - copy_storage = kwargs.get("copy_storage") - if copy_storage: - if copy_storage == "all": - flags |= libvirt.VIR_MIGRATE_NON_SHARED_DISK - elif copy_storage in ["inc", "incremental"]: - flags |= libvirt.VIR_MIGRATE_NON_SHARED_INC - else: - raise SaltInvocationError("invalid copy_storage value") - try: - state = False - dst_conn = __get_conn( - connection=dst_uri, - username=kwargs.get("username"), - password=kwargs.get("password"), + tunnel = __salt__["config.get"]("virt:tunnel") + if tunnel: + return ( + "virsh migrate --p2p --tunnelled --live --persistent " "--undefinesource " ) - new_dom = dom.migrate3(dconn=dst_conn, params=params, flags=flags) - if new_dom: - state = new_dom.state() - dst_conn.close() - return state and migrated_state in state - except libvirt.libvirtError as err: - dst_conn.close() - raise CommandExecutionError(err.get_error_message()) - - -def _get_volume_path(pool, volume_name): - """ - Get the path to a volume. If the volume doesn't exist, compute its path from the pool one. - """ - if volume_name in pool.listVolumes(): - volume = pool.storageVolLookupByName(volume_name) - volume_xml = ElementTree.fromstring(volume.XMLDesc()) - return volume_xml.find("./target/path").text - - # Get the path from the pool if the volume doesn't exist yet - pool_xml = ElementTree.fromstring(pool.XMLDesc()) - pool_path = pool_xml.find("./target/path").text - return pool_path + "/" + volume_name + return "virsh migrate --live --persistent --undefinesource " -def _disk_from_pool(conn, pool, pool_xml, volume_name): +def _get_target(target, ssh): """ - Create a disk definition out of the pool XML and volume name. - The aim of this function is to replace the volume-based definition when not handled by libvirt. - It returns the disk Jinja context to be used when creating the VM + Compute libvirt URL for target migration host. """ - pool_type = pool_xml.get("type") - disk_context = {} - - # handle dir, fs and netfs - if pool_type in ["dir", "netfs", "fs"]: - disk_context["type"] = "file" - disk_context["source_file"] = _get_volume_path(pool, volume_name) - - elif pool_type in ["logical", "disk", "iscsi", "scsi"]: - disk_context["type"] = "block" - disk_context["format"] = "raw" - disk_context["source_file"] = _get_volume_path(pool, volume_name) - - elif pool_type in ["rbd", "gluster", "sheepdog"]: - # libvirt can't handle rbd, gluster and sheepdog as volumes - disk_context["type"] = "network" - disk_context["protocol"] = pool_type - # Copy the hosts from the pool definition - disk_context["hosts"] = [ - {"name": host.get("name"), "port": host.get("port")} - for host in pool_xml.findall(".//host") - ] - dir_node = pool_xml.find("./source/dir") - # Gluster and RBD need pool/volume name - name_node = pool_xml.find("./source/name") - if name_node is not None: - disk_context["volume"] = "{}/{}".format(name_node.text, volume_name) - # Copy the authentication if any for RBD - auth_node = pool_xml.find("./source/auth") - if auth_node is not None: - username = auth_node.get("username") - secret_node = auth_node.find("./secret") - usage = secret_node.get("usage") - if not usage: - # Get the usage from the UUID - uuid = secret_node.get("uuid") - usage = conn.secretLookupByUUIDString(uuid).usageID() - disk_context["auth"] = { - "type": "ceph", - "username": username, - "usage": usage, - } - - return disk_context - - -def _handle_unit(s, def_unit="m"): - """ - Handle the unit conversion, return the value in bytes - """ - m = re.match(r"(?P<value>[0-9.]*)\s*(?P<unit>.*)$", str(s).strip()) - value = m.group("value") - # default unit - unit = m.group("unit").lower() or def_unit - try: - value = int(value) - except ValueError: - try: - value = float(value) - except ValueError: - raise SaltInvocationError("invalid number") - # flag for base ten - dec = False - if re.match(r"[kmgtpezy]b$", unit): - dec = True - elif not re.match(r"(b|[kmgtpezy](ib)?)$", unit): - raise SaltInvocationError("invalid units") - p = "bkmgtpezy".index(unit[0]) - value *= 10 ** (p * 3) if dec else 2 ** (p * 10) - return int(value) - - -def nesthash(): - """ - create default dict that allows arbitrary level of nesting - """ - return collections.defaultdict(nesthash) + proto = "qemu" + if ssh: + proto += "+ssh" + return " {}://{}/{}".format(proto, target, "system") def _gen_xml( @@ -942,31 +647,18 @@ def _gen_xml( arch, graphics=None, boot=None, - boot_dev=None, **kwargs ): """ Generate the XML string to define a libvirt VM """ + mem = int(mem) * 1024 # MB context = { "hypervisor": hypervisor, "name": name, "cpu": str(cpu), + "mem": str(mem), } - - context["mem"] = nesthash() - if isinstance(mem, int): - mem = int(mem) * 1024 # MB - context["mem"]["boot"] = str(mem) - context["mem"]["current"] = str(mem) - elif isinstance(mem, dict): - for tag, val in mem.items(): - if val: - if tag == "slots": - context["mem"]["slots"] = "{}='{}'".format(tag, val) - else: - context["mem"][tag] = str(int(_handle_unit(val) / 1024)) - if hypervisor in ["qemu", "kvm"]: context["controller_model"] = False elif hypervisor == "vmware": @@ -988,17 +680,15 @@ def _gen_xml( graphics = None context["graphics"] = graphics - context["boot_dev"] = boot_dev.split() if boot_dev is not None else ["hd"] + if "boot_dev" in kwargs: + context["boot_dev"] = [] + for dev in kwargs["boot_dev"].split(): + context["boot_dev"].append(dev) + else: + context["boot_dev"] = ["hd"] context["boot"] = boot if boot else {} - # if efi parameter is specified, prepare os_attrib - efi_value = context["boot"].get("efi", None) if boot else None - if efi_value is True: - context["boot"]["os_attrib"] = "firmware='efi'" - elif efi_value is not None and type(efi_value) != bool: - raise SaltInvocationError("Invalid efi value") - if os_type == "xen": # Compute the Xen PV boot method if __grains__["os_family"] == "Suse": @@ -1047,16 +737,41 @@ def _gen_xml( elif disk.get("pool"): disk_context["volume"] = disk["filename"] # If we had no source_file, then we want a volume - pool = conn.storagePoolLookupByName(disk["pool"]) - pool_xml = ElementTree.fromstring(pool.XMLDesc()) + pool_xml = ElementTree.fromstring( + conn.storagePoolLookupByName(disk["pool"]).XMLDesc() + ) pool_type = pool_xml.get("type") - - # For Xen VMs convert all pool types (issue #58333) - if hypervisor == "xen" or pool_type in ["rbd", "gluster", "sheepdog"]: - disk_context.update( - _disk_from_pool(conn, pool, pool_xml, disk_context["volume"]) - ) - + if pool_type in ["rbd", "gluster", "sheepdog"]: + # libvirt can't handle rbd, gluster and sheepdog as volumes + disk_context["type"] = "network" + disk_context["protocol"] = pool_type + # Copy the hosts from the pool definition + disk_context["hosts"] = [ + {"name": host.get("name"), "port": host.get("port")} + for host in pool_xml.findall(".//host") + ] + dir_node = pool_xml.find("./source/dir") + # Gluster and RBD need pool/volume name + name_node = pool_xml.find("./source/name") + if name_node is not None: + disk_context["volume"] = "{}/{}".format( + name_node.text, disk_context["volume"] + ) + # Copy the authentication if any for RBD + auth_node = pool_xml.find("./source/auth") + if auth_node is not None: + username = auth_node.get("username") + secret_node = auth_node.find("./secret") + usage = secret_node.get("usage") + if not usage: + # Get the usage from the UUID + uuid = secret_node.get("uuid") + usage = conn.secretLookupByUUIDString(uuid).usageID() + disk_context["auth"] = { + "type": "ceph", + "username": username, + "usage": usage, + } else: if pool_type in ["disk", "logical"]: # The volume format for these types doesn't match the driver format in the VM @@ -1086,6 +801,7 @@ def _gen_xml( except jinja2.exceptions.TemplateNotFound: log.error("Could not load template %s", fn_) return "" + return template.render(**context) @@ -1521,7 +1237,7 @@ def _disk_profile(conn, profile, hypervisor, disks, vm_name): overlay = {"device": "disk", "model": "virtio"} elif hypervisor == "xen": overlay = {"device": "disk", "model": "xen"} - elif hypervisor == "bhyve": + elif hypervisor in ["bhyve"]: overlay = {"format": "raw", "model": "virtio", "sparse_volume": False} else: overlay = {} @@ -1566,7 +1282,7 @@ def _disk_profile(conn, profile, hypervisor, disks, vm_name): disk["format"] = ( "qcow2" if disk.get("device", "disk") != "cdrom" else "raw" ) - elif vm_name and disk.get("device", "disk") == "disk": + elif disk.get("device", "disk") == "disk": _fill_disk_filename(conn, vm_name, disk, hypervisor, pool_caps) return disklist @@ -1803,68 +1519,30 @@ def _handle_remote_boot_params(orig_boot): new_boot = orig_boot.copy() keys = orig_boot.keys() cases = [ - {"efi"}, - {"kernel", "initrd", "efi"}, - {"kernel", "initrd", "cmdline", "efi"}, {"loader", "nvram"}, {"kernel", "initrd"}, {"kernel", "initrd", "cmdline"}, - {"kernel", "initrd", "loader", "nvram"}, - {"kernel", "initrd", "cmdline", "loader", "nvram"}, + {"loader", "nvram", "kernel", "initrd"}, + {"loader", "nvram", "kernel", "initrd", "cmdline"}, ] try: if keys in cases: for key in keys: - if key == "efi" and type(orig_boot.get(key)) == bool: - new_boot[key] = orig_boot.get(key) - elif orig_boot.get(key) is not None and salt.utils.virt.check_remote( - orig_boot.get(key) - ): + if orig_boot.get(key) is not None and check_remote(orig_boot.get(key)): if saltinst_dir is None: os.makedirs(CACHE_DIR) saltinst_dir = CACHE_DIR - new_boot[key] = salt.utils.virt.download_remote( - orig_boot.get(key), saltinst_dir - ) + new_boot[key] = download_remote(orig_boot.get(key), saltinst_dir) return new_boot else: raise SaltInvocationError( - "Invalid boot parameters,It has to follow this combination: [(kernel, initrd) or/and cmdline] or/and [(loader, nvram) or efi]" + "Invalid boot parameters, (kernel, initrd) or/and (loader, nvram) must be both present" ) except Exception as err: # pylint: disable=broad-except raise err -def _handle_efi_param(boot, desc): - """ - Checks if boot parameter contains efi boolean value, if so, handles the firmware attribute. - :param boot: The boot parameters passed to the init or update functions. - :param desc: The XML description of that domain. - :return: A boolean value. - """ - efi_value = boot.get("efi", None) if boot else None - parent_tag = desc.find("os") - os_attrib = parent_tag.attrib - - # newly defined vm without running, loader tag might not be filled yet - if efi_value is False and os_attrib != {}: - parent_tag.attrib.pop("firmware", None) - return True - - # check the case that loader tag might be present. This happens after the vm ran - elif type(efi_value) == bool and os_attrib == {}: - if efi_value is True and parent_tag.find("loader") is None: - parent_tag.set("firmware", "efi") - if efi_value is False and parent_tag.find("loader") is not None: - parent_tag.remove(parent_tag.find("loader")) - parent_tag.remove(parent_tag.find("nvram")) - return True - elif type(efi_value) != bool: - raise SaltInvocationError("Invalid efi value") - return False - - def init( name, cpu, @@ -1885,7 +1563,6 @@ def init( os_type=None, arch=None, boot=None, - boot_dev=None, **kwargs ): """ @@ -1893,28 +1570,7 @@ def init( :param name: name of the virtual machine to create :param cpu: Number of virtual CPUs to assign to the virtual machine - :param mem: Amount of memory to allocate to the virtual machine in MiB. Since 3002, a dictionary can be used to - contain detailed configuration which support memory allocation or tuning. Supported parameters are ``boot``, - ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit`` and ``min_guarantee``. The - structure of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. - Detail unit specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be - an integer. - - .. code-block:: python - - { - 'boot': 1g, - 'current': 1g, - 'max': 1g, - 'slots': 10, - 'hard_limit': '1024' - 'soft_limit': '512m' - 'swap_hard_limit': '1g' - 'min_guarantee': '512mib' - } - - .. versionchanged:: 3002 - + :param mem: Amount of memory to allocate to the virtual machine in MiB. :param nic: NIC profile to use (Default: ``'default'``). The profile interfaces can be customized / extended with the interfaces parameter. If set to ``None``, no profile will be used. @@ -1976,8 +1632,7 @@ def init( This is an optional parameter, all of the keys are optional within the dictionary. The structure of the dictionary is documented in :ref:`init-boot-def`. If a remote path is provided to kernel or initrd, salt will handle the downloading of the specified remote file and modify the XML accordingly. - To boot VM with UEFI, specify loader and nvram path or specify 'efi': ``True`` if your libvirtd version - is >= 5.2.0 and QEMU >= 3.0.0. + To boot VM with UEFI, specify loader and nvram path. .. versionadded:: 3000 @@ -1991,12 +1646,6 @@ def init( 'nvram': '/usr/share/OVMF/OVMF_VARS.ms.fd' } - :param boot_dev: - Space separated list of devices to boot from sorted by decreasing priority. - Values can be ``hd``, ``fd``, ``cdrom`` or ``network``. - - By default, the value will ``"hd"``. - .. _init-boot-def: .. rubric:: Boot parameters definition @@ -2015,47 +1664,12 @@ def init( loader The path to the UEFI binary loader to use. - .. versionadded:: 3001 + .. versionadded:: sodium nvram The path to the UEFI data template. The file will be copied when creating the virtual machine. - .. versionadded:: 3001 - - efi - A boolean value. - - .. versionadded:: sodium - - .. _init-mem-def: - - .. rubric:: Memory parameter definition - - Memory parameter can contain the following properties: - - boot - The maximum allocation of memory for the guest at boot time - - current - The actual allocation of memory for the guest - - max - The run time maximum memory allocation of the guest - - slots - specifies the number of slots available for adding memory to the guest - - hard_limit - the maximum memory the guest can use - - soft_limit - memory limit to enforce during memory contention - - swap_hard_limit - the maximum memory plus swap the guest can use - - min_guarantee - the guaranteed minimum memory allocation for the guest + .. versionadded:: sodium .. _init-nic-def: @@ -2099,7 +1713,7 @@ def init( Path to the folder or name of the pool where disks should be created. (Default: depends on hypervisor and the virt:storagepool configuration) - .. versionchanged:: 3001 + .. versionchanged:: sodium If the value contains no '/', it is considered a pool name where to create a volume. Using volumes will be mandatory for some pools types like rdb, iscsi, etc. @@ -2120,7 +1734,7 @@ def init( ``True`` to create a QCOW2 disk image with ``image`` as backing file. If ``False`` the file pointed to by the ``image`` property will simply be copied. (Default: ``False``) - .. versionchanged:: 3001 + .. versionchanged:: sodium This property is only valid on path-based disks, not on volumes. To create a volume with a backing store, set the ``backing_store_path`` and ``backing_store_format`` properties. @@ -2129,20 +1743,20 @@ def init( Path to the backing store image to use. This can also be the name of a volume to use as backing store within the same pool. - .. versionadded:: 3001 + .. versionadded:: sodium backing_store_format Image format of the disk or volume to use as backing store. This property is mandatory when using ``backing_store_path`` to avoid `problems <https://libvirt.org/kbase/backing_chains.html#troubleshooting>`_ - .. versionadded:: 3001 + .. versionadded:: sodium source_file Absolute path to the disk image to use. Not to be confused with ``image`` parameter. This parameter is useful to use disk images that are created outside of this module. Can also be ``None`` for devices that have no associated image like cdroms. - .. versionchanged:: 3001 + .. versionchanged:: sodium For volume disks, this can be the name of a volume already existing in the storage pool. @@ -2241,8 +1855,6 @@ def init( for x in y } ) - if len(hypervisors) == 0: - raise SaltInvocationError("No supported hypervisors were found") virt_hypervisor = "kvm" if "kvm" in hypervisors else hypervisors[0] # esxi used to be a possible value for the hypervisor: map it to vmware since it's the same @@ -2350,10 +1962,8 @@ def init( arch, graphics, boot, - boot_dev, **kwargs ) - log.debug("New virtual machine definition: %s", vm_xml) conn.defineXML(vm_xml) except libvirt.libvirtError as err: conn.close() @@ -2517,11 +2127,11 @@ def _get_disk_target(targets, disks_count, prefix): :param disks: the number of disks :param prefix: the prefix of the target name, i.e. "hd" """ - for i in range(disks_count): - ret = "{}{}".format(prefix, string.ascii_lowercase[i]) - if ret not in targets: - return ret - return None + return [ + "{}{}".format(prefix, string.ascii_lowercase[i]) + for i in range(disks_count) + if "{}{}".format(prefix, string.ascii_lowercase[i]) not in targets + ][0] def _diff_disk_lists(old, new): @@ -2638,17 +2248,8 @@ def update( Refer to :ref:`init-boot-def` for the complete boot parameter description. - To update any boot parameters, specify the new path for each. To remove any boot parameters, pass ``None`` object, - for instance: 'kernel': ``None``. To switch back to BIOS boot, specify ('loader': ``None`` and 'nvram': ``None``) - or 'efi': ``False``. Please note that ``None`` is mapped to ``null`` in sls file, pass ``null`` in sls file instead. - - SLS file Example: - - .. code-block:: yaml - - - boot: - loader: null - nvram: null + To update any boot parameters, specify the new path for each. To remove any boot parameters, + pass a None object, for instance: 'kernel': ``None``. .. versionadded:: 3000 @@ -2704,6 +2305,7 @@ def update( new_desc = ElementTree.fromstring( _gen_xml( + conn, name, cpu or 0, mem or 0, @@ -2725,121 +2327,67 @@ def update( cpu_node.set("current", str(cpu)) need_update = True - def _set_loader(node, value): - salt.utils.xmlutil.set_node_text(node, value) - if value is not None: - node.set("readonly", "yes") - node.set("type", "pflash") + # Update the kernel boot parameters + boot_tags = ["kernel", "initrd", "cmdline", "loader", "nvram"] + parent_tag = desc.find("os") - def _set_nvram(node, value): - node.set("template", value) + # We need to search for each possible subelement, and update it. + for tag in boot_tags: + # The Existing Tag... + found_tag = parent_tag.find(tag) - def _set_with_byte_unit(node, value): - node.text = str(value) - node.set("unit", "bytes") + # The new value + boot_tag_value = boot.get(tag, None) if boot else None - def _get_with_unit(node): - unit = node.get("unit", "KiB") - # _handle_unit treats bytes as invalid unit for the purpose of consistency - unit = unit if unit != "bytes" else "b" - value = node.get("memory") or node.text - return _handle_unit("{}{}".format(value, unit)) if value else None + # Existing tag is found and values don't match + if found_tag is not None and found_tag.text != boot_tag_value: - old_mem = int(_get_with_unit(desc.find("memory")) / 1024) + # If the existing tag is found, but the new value is None + # remove it. If the existing tag is found, and the new value + # doesn't match update it. In either case, mark for update. + if boot_tag_value is None and boot is not None and parent_tag is not None: + parent_tag.remove(found_tag) + else: + found_tag.text = boot_tag_value - # Update the kernel boot parameters - params_mapping = [ - {"path": "boot:kernel", "xpath": "os/kernel"}, - {"path": "boot:initrd", "xpath": "os/initrd"}, - {"path": "boot:cmdline", "xpath": "os/cmdline"}, - {"path": "boot:loader", "xpath": "os/loader", "set": _set_loader}, - {"path": "boot:nvram", "xpath": "os/nvram", "set": _set_nvram}, - # Update the memory, note that libvirt outputs all memory sizes in KiB - { - "path": "mem", - "xpath": "memory", - "convert": _handle_unit, - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "mem", - "xpath": "currentMemory", - "convert": _handle_unit, - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "mem:max", - "convert": _handle_unit, - "xpath": "maxMemory", - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "mem:boot", - "convert": _handle_unit, - "xpath": "memory", - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "mem:current", - "convert": _handle_unit, - "xpath": "currentMemory", - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "mem:slots", - "xpath": "maxMemory", - "get": lambda n: n.get("slots"), - "set": lambda n, v: n.set("slots", str(v)), - "del": salt.utils.xmlutil.del_attribute("slots", ["unit"]), - }, - { - "path": "mem:hard_limit", - "convert": _handle_unit, - "xpath": "memtune/hard_limit", - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "mem:soft_limit", - "convert": _handle_unit, - "xpath": "memtune/soft_limit", - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "mem:swap_hard_limit", - "convert": _handle_unit, - "xpath": "memtune/swap_hard_limit", - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "mem:min_guarantee", - "convert": _handle_unit, - "xpath": "memtune/min_guarantee", - "get": _get_with_unit, - "set": _set_with_byte_unit, - }, - { - "path": "boot_dev:{dev}", - "xpath": "os/boot[$dev]", - "get": lambda n: n.get("dev"), - "set": lambda n, v: n.set("dev", v), - "del": salt.utils.xmlutil.del_attribute("dev"), - }, - ] + # If the existing tag is loader or nvram, we need to update the corresponding attribute + if found_tag.tag == "loader" and boot_tag_value is not None: + found_tag.set("readonly", "yes") + found_tag.set("type", "pflash") - data = {k: v for k, v in locals().items() if bool(v)} - if boot_dev: - data["boot_dev"] = {i + 1: dev for i, dev in enumerate(boot_dev.split())} - need_update = ( - salt.utils.xmlutil.change_xml(desc, data, params_mapping) or need_update - ) + if found_tag.tag == "nvram" and boot_tag_value is not None: + found_tag.set("template", found_tag.text) + found_tag.text = None + + need_update = True + + # Need to check for parent tag, and add it if it does not exist. + # Add a subelement and set the value to the new value, and then + # mark for update. + if parent_tag is not None: + child_tag = ElementTree.SubElement(parent_tag, tag) + else: + new_parent_tag = ElementTree.Element("os") + child_tag = ElementTree.SubElement(new_parent_tag, tag) + + child_tag.text = boot_tag_value + + # If the newly created tag is loader or nvram, we need to update the corresponding attribute + if child_tag.tag == "loader": + child_tag.set("readonly", "yes") + child_tag.set("type", "pflash") + + if child_tag.tag == "nvram": + child_tag.set("template", child_tag.text) + child_tag.text = None + + # Update the memory, note that libvirt outputs all memory sizes in KiB + for mem_node_name in ["memory", "currentMemory"]: + mem_node = desc.find(mem_node_name) + if mem and int(mem_node.text) != mem * 1024: + mem_node.text = str(mem) + mem_node.set("unit", "MiB") + need_update = True # Update the XML definition with the new disks and diff changes devices_node = desc.find("devices") @@ -2869,18 +2417,22 @@ def update( # Set the new definition if need_update: # Create missing disks if needed - if changes["disk"]: - for idx, item in enumerate(changes["disk"]["sorted"]): - source_file = all_disks[idx]["source_file"] - if ( - item in changes["disk"]["new"] - and source_file - and not os.path.isfile(source_file) - and not test - ): - _qemu_image_create(all_disks[idx]) - try: + if changes["disk"]: + for idx, item in enumerate(changes["disk"]["sorted"]): + source_file = all_disks[idx].get("source_file") + # We don't want to create image disks for cdrom devices + if all_disks[idx].get("device", "disk") == "cdrom": + continue + if ( + item in changes["disk"]["new"] + and source_file + and not os.path.isfile(source_file) + ): + _qemu_image_create(all_disks[idx]) + elif item in changes["disk"]["new"] and not source_file: + _disk_volume_create(conn, all_disks[idx]) + if not test: conn.defineXML( salt.utils.stringutils.to_str(ElementTree.tostring(desc)) @@ -2905,24 +2457,13 @@ def update( } ) 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 old_mem != new_mem and new_mem is not None: - commands.append( - { - "device": "mem", - "cmd": "setMemoryFlags", - "args": [new_mem, libvirt.VIR_DOMAIN_AFFECT_LIVE], - } - ) + commands.append( + { + "device": "mem", + "cmd": "setMemoryFlags", + "args": [mem * 1024, libvirt.VIR_DOMAIN_AFFECT_LIVE], + } + ) # Look for removable device source changes new_disks = [] @@ -3663,12 +3204,13 @@ def get_profiles(hypervisor=None, **kwargs): .. code-block:: bash salt '*' virt.get_profiles - salt '*' virt.get_profiles hypervisor=vmware + salt '*' virt.get_profiles hypervisor=esxi """ + ret = {} + # Use the machine types as possible values # Prefer 'kvm' over the others if available - conn = __get_conn(**kwargs) - caps = _capabilities(conn) + caps = capabilities(**kwargs) hypervisors = sorted( { x @@ -3676,24 +3218,24 @@ def get_profiles(hypervisor=None, **kwargs): for x in y } ) - if len(hypervisors) == 0: - raise SaltInvocationError("No supported hypervisors were found") + default_hypervisor = "kvm" if "kvm" in hypervisors else hypervisors[0] if not hypervisor: - hypervisor = "kvm" if "kvm" in hypervisors else hypervisors[0] - - ret = { - "disk": {"default": _disk_profile(conn, "default", hypervisor, [], None)}, - "nic": {"default": _nic_profile("default", hypervisor)}, - } + hypervisor = default_hypervisor virtconf = __salt__["config.get"]("virt", {}) - - for profile in virtconf.get("disk", []): - ret["disk"][profile] = _disk_profile(conn, profile, hypervisor, [], None) - - for profile in virtconf.get("nic", []): - ret["nic"][profile] = _nic_profile(profile, hypervisor) - + for typ in ["disk", "nic"]: + _func = getattr(sys.modules[__name__], "_{}_profile".format(typ)) + ret[typ] = { + "default": _func( + "default", hypervisor if hypervisor else default_hypervisor + ) + } + if typ in virtconf: + ret.setdefault(typ, {}) + for prf in virtconf[typ]: + ret[typ][prf] = _func( + prf, hypervisor if hypervisor else default_hypervisor + ) return ret @@ -4061,7 +3603,7 @@ def define_vol_xml_str( storage pool name to define the volume in. If defined, this parameter will override the configuration setting. - .. versionadded:: 3001 + .. versionadded:: Sodium :param connection: libvirt connection URI, overriding defaults .. versionadded:: 2019.2.0 @@ -4106,7 +3648,7 @@ def define_vol_xml_path(path, pool=None, **kwargs): storage pool name to define the volume in. If defined, this parameter will override the configuration setting. - .. versionadded:: 3001 + .. versionadded:: Sodium :param connection: libvirt connection URI, overriding defaults .. versionadded:: 2019.2.0 @@ -4133,7 +3675,7 @@ def define_vol_xml_path(path, pool=None, **kwargs): return False -def migrate_non_shared(vm_, target, ssh=False, **kwargs): +def migrate_non_shared(vm_, target, ssh=False): """ Attempt to execute non-shared storage "all" migration @@ -4191,17 +3733,17 @@ def migrate_non_shared(vm_, target, ssh=False, **kwargs): For more details on tunnelled data migrations, report to https://libvirt.org/migration.html#transporttunnel """ - salt.utils.versions.warn_until( - "Silicon", - "The 'migrate_non_shared' feature has been deprecated. " - "Use 'migrate' with copy_storage='all' instead.", + cmd = ( + _get_migrate_command() + " --copy-storage-all " + vm_ + _get_target(target, ssh) ) - return migrate(vm_, target, ssh, copy_storage="all", **kwargs) + + stdout = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()[0] + return salt.utils.stringutils.to_str(stdout) -def migrate_non_shared_inc(vm_, target, ssh=False, **kwargs): +def migrate_non_shared_inc(vm_, target, ssh=False): """ - Attempt to execute non-shared storage "inc" migration + Attempt to execute non-shared storage "all" migration :param vm_: domain name :param target: target libvirt host name @@ -4257,15 +3799,15 @@ def migrate_non_shared_inc(vm_, target, ssh=False, **kwargs): For more details on tunnelled data migrations, report to https://libvirt.org/migration.html#transporttunnel """ - salt.utils.versions.warn_until( - "Silicon", - "The 'migrate_non_shared_inc' feature has been deprecated. " - "Use 'migrate' with copy_storage='inc' instead.", + cmd = ( + _get_migrate_command() + " --copy-storage-inc " + vm_ + _get_target(target, ssh) ) - return migrate(vm_, target, ssh, copy_storage="inc", **kwargs) + + stdout = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()[0] + return salt.utils.stringutils.to_str(stdout) -def migrate(vm_, target, ssh=False, **kwargs): +def migrate(vm_, target, ssh=False): """ Shared storage migration @@ -4328,51 +3870,10 @@ def migrate(vm_, target, ssh=False, **kwargs): For more details on tunnelled data migrations, report to https://libvirt.org/migration.html#transporttunnel """ + cmd = _get_migrate_command() + " " + vm_ + _get_target(target, ssh) - if ssh: - salt.utils.versions.warn_until( - "Silicon", - "The 'ssh' argument has been deprecated and " - "will be removed in a future release. " - "Use libvirt URI string 'target' instead.", - ) - - conn = __get_conn() - dom = _get_domain(conn, vm_) - - if not urlparse(target).scheme: - proto = "qemu" - if ssh: - proto += "+ssh" - dst_uri = "{}://{}/system".format(proto, target) - else: - dst_uri = target - - ret = _migrate(dom, dst_uri, **kwargs) - conn.close() - return ret - - -def migrate_start_postcopy(vm_): - """ - Starts post-copy migration. This function has to be called - while live migration is in progress and it has been initiated - with the `postcopy=True` option. - - CLI Example: - - .. code-block:: bash - - salt '*' virt.migrate_start_postcopy <domain> - """ - conn = __get_conn() - dom = _get_domain(conn, vm_) - try: - dom.migrateStartPostCopy() - except libvirt.libvirtError as err: - conn.close() - raise CommandExecutionError(err.get_error_message()) - conn.close() + stdout = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()[0] + return salt.utils.stringutils.to_str(stdout) def seed_non_shared_migrate(disks, force=False): @@ -5206,14 +4707,15 @@ def _parse_caps_guest(guest): # without possibility to toggle them. # Some guests may also have no feature at all (xen pv for instance) features_nodes = guest.find("features") - if features_nodes is not None and child is not None: + if features_nodes is not None: result["features"] = { child.tag: { - "toggle": child.get("toggle", "no") == "yes", - "default": child.get("default", "on") == "on", + "toggle": True if child.get("toggle") == "yes" else False, + "default": True if child.get("default") == "no" else True, } for child in features_nodes } + return result @@ -5623,7 +5125,7 @@ def domain_capabilities(emulator=None, arch=None, machine=None, domain=None, **k """ Return the domain capabilities given an emulator, architecture, machine or virtualization type. - .. versionadded:: Fluorine + .. versionadded:: 2019.2.0 :param emulator: return the capabilities for the given emulator binary :param arch: return the capabilities for the given CPU architecture @@ -5662,7 +5164,7 @@ def all_capabilities(**kwargs): """ Return the host and domain capabilities in a single call. - .. versionadded:: Neon + .. versionadded:: Sodium :param connection: libvirt connection URI, overriding defaults :param username: username to connect with, overriding defaults @@ -5929,7 +5431,7 @@ def list_networks(**kwargs): def network_info(name=None, **kwargs): """ - Return information on a virtual network provided its name. + Return informations on a virtual network provided its name. :param name: virtual network name :param connection: libvirt connection URI, overriding defaults @@ -6526,19 +6028,15 @@ def _pool_set_secret( if secret_type: # Get the previously defined secret if any secret = None - try: - if usage: - usage_type = ( - libvirt.VIR_SECRET_USAGE_TYPE_CEPH - if secret_type == "ceph" - else libvirt.VIR_SECRET_USAGE_TYPE_ISCSI - ) - secret = conn.secretLookupByUsage(usage_type, usage) - elif uuid: - secret = conn.secretLookupByUUIDString(uuid) - except libvirt.libvirtError as err: - # For some reason the secret has been removed. Don't fail since we'll recreate it - log.info("Secret not found: %s", err.get_error_message()) + if usage: + usage_type = ( + libvirt.VIR_SECRET_USAGE_TYPE_CEPH + if secret_type == "ceph" + else libvirt.VIR_SECRET_USAGE_TYPE_ISCSI + ) + secret = conn.secretLookupByUsage(usage_type, usage) + elif uuid: + secret = conn.secretLookupByUUIDString(uuid) # Create secret if needed if not secret: @@ -6790,7 +6288,7 @@ def list_pools(**kwargs): def pool_info(name=None, **kwargs): """ - Return information on a storage pool provided its name. + Return informations on a storage pool provided its name. :param name: libvirt storage pool name :param connection: libvirt connection URI, overriding defaults @@ -7007,6 +6505,22 @@ def pool_delete(name, **kwargs): conn = __get_conn(**kwargs) try: pool = conn.storagePoolLookupByName(name) + desc = ElementTree.fromstring(pool.XMLDesc()) + + # Is there a secret that we generated and would need to be removed? + # Don't remove the other secrets + auth_node = desc.find("source/auth") + if auth_node is not None: + auth_types = { + "ceph": libvirt.VIR_SECRET_USAGE_TYPE_CEPH, + "iscsi": libvirt.VIR_SECRET_USAGE_TYPE_ISCSI, + } + secret_type = auth_types[auth_node.get("type")] + secret_usage = auth_node.find("secret").get("usage") + if secret_type and "pool_{}".format(name) == secret_usage: + secret = conn.secretLookupByUsage(secret_type, secret_usage) + secret.undefine() + return not bool(pool.delete(libvirt.VIR_STORAGE_POOL_DELETE_NORMAL)) finally: conn.close() @@ -7125,33 +6639,29 @@ def _is_valid_volume(vol): def _get_all_volumes_paths(conn): """ - Extract the path, name, pool name and backing stores path of all volumes. + Extract the path and backing stores path of all volumes. :param conn: libvirt connection to use """ - pools = [ - pool - for pool in conn.listAllStoragePools() - if pool.info()[0] == libvirt.VIR_STORAGE_POOL_RUNNING + volumes = [ + vol + for l in [ + obj.listAllVolumes() + for obj in conn.listAllStoragePools() + if obj.info()[0] == libvirt.VIR_STORAGE_POOL_RUNNING + ] + for vol in l ] - volumes = {} - for pool in pools: - pool_volumes = { - volume.path(): { - "pool": pool.name(), - "name": volume.name(), - "backing_stores": [ - path.text - for path in ElementTree.fromstring(volume.XMLDesc()).findall( - ".//backingStore/path" - ) - ], - } - for volume in pool.listAllVolumes() - if _is_valid_volume(volume) - } - volumes.update(pool_volumes) - return volumes + return { + vol.path(): [ + path.text + for path in ElementTree.fromstring(vol.XMLDesc()).findall( + ".//backingStore/path" + ) + ] + for vol in volumes + if _is_valid_volume(vol) + } def volume_infos(pool=None, volume=None, **kwargs): @@ -7222,8 +6732,8 @@ def volume_infos(pool=None, volume=None, **kwargs): if vol.path(): as_backing_store = { path - for (path, volume) in backing_stores.items() - if vol.path() in volume.get("backing_stores") + for (path, all_paths) in backing_stores.items() + if vol.path() in all_paths } used_by = [ vm_name @@ -7346,7 +6856,7 @@ def volume_define( backing_store="{'path': '/path/to/base.img', 'format': 'raw'}" \ nocow=True - .. versionadded:: 3001 + .. versionadded:: Sodium """ ret = False try: @@ -7488,7 +6998,7 @@ def volume_upload(pool, volume, file, offset=0, length=0, sparse=False, **kwargs salt '*' virt.volume_upload default myvm.qcow2 /path/to/disk.qcow2 - .. versionadded:: 3001 + .. versionadded:: Sodium """ conn = __get_conn(**kwargs) diff --git a/salt/states/virt.py b/salt/states/virt.py index 2394d0745e..cb15d57d8f 100644 --- a/salt/states/virt.py +++ b/salt/states/virt.py @@ -33,8 +33,6 @@ except ImportError: __virtualname__ = "virt" -log = logging.getLogger(__name__) - def __virtual__(): """ @@ -42,6 +40,7 @@ def __virtual__(): :return: """ + if "virt.node_info" in __salt__: return __virtualname__ return (False, "virt module could not be loaded") @@ -332,19 +331,15 @@ def defined( but ``x86_64`` is prefed over ``i686``. Only used when creating a new virtual machine. :param boot: - Specifies kernel for the virtual machine, as well as boot parameters - for the virtual machine. This is an optionl parameter, and all of the - keys are optional within the dictionary. If a remote path is provided - to kernel or initrd, salt will handle the downloading of the specified - remote fild, and will modify the XML accordingly. + Specifies kernel, initial ramdisk and kernel command line parameters for the virtual machine. + This is an optional parameter, all of the keys are optional within the dictionary. - .. code-block:: python + Refer to :ref:`init-boot-def` for the complete boot parameters description. - { - 'kernel': '/root/f8-i386-vmlinuz', - 'initrd': '/root/f8-i386-initrd', - 'cmdline': 'console=ttyS0 ks=http://example.com/f8-i386/os/' - } + To update any boot parameters, specify the new path for each. To remove any boot parameters, + pass a None object, for instance: 'kernel': ``None``. + + .. versionadded:: 3000 :param update: set to ``False`` to prevent updating a defined domain. (Default: ``True``) @@ -461,7 +456,6 @@ def running( name, cpu=None, mem=None, - image=None, vm_type=None, disk_profile=None, disks=None, @@ -487,23 +481,7 @@ def running( :param name: name of the virtual machine to run :param cpu: number of CPUs for the virtual machine to create - :param mem: Amount of memory to allocate to the virtual machine in MiB. Since 3002, a dictionary can be used to - contain detailed configuration which support memory allocation or tuning. Supported parameters are ``boot``, - ``current``, ``max``, ``slots``, ``hard_limit``, ``soft_limit``, ``swap_hard_limit`` and ``min_guarantee``. The - structure of the dictionary is documented in :ref:`init-mem-def`. Both decimal and binary base are supported. - Detail unit specification is documented in :ref:`virt-units`. Please note that the value for ``slots`` must be - an integer. - - To remove any parameters, pass a None object, for instance: 'soft_limit': ``None``. Please note that ``None`` - is mapped to ``null`` in sls file, pass ``null`` in sls file instead. - - .. code-block:: yaml - - - mem: - hard_limit: null - soft_limit: null - - .. versionchanged:: 3002 + :param mem: amount of memory in MiB for the new virtual machine :param vm_type: force virtual machine type for the new VM. The default value is taken from the host capabilities. This could be useful for example to use ``'qemu'`` type instead of the ``'kvm'`` one. @@ -648,32 +626,10 @@ def running( """ merged_disks = disks - if image: - default_disks = [{"system": {}}] - disknames = ["system"] - if disk_profile: - disklist = copy.deepcopy( - __salt__["config.get"]("virt:disk", {}).get(disk_profile, default_disks) - ) - disknames = disklist.keys() - disk = {"name": disknames[0], "image": image} - if merged_disks: - first_disk = [d for d in merged_disks if d.get("name") == disknames[0]] - if first_disk and "image" not in first_disk[0]: - first_disk[0]["image"] = image - else: - merged_disks.append(disk) - else: - merged_disks = [disk] - salt.utils.versions.warn_until( - "Sodium", - "'image' parameter has been deprecated. Rather use the 'disks' parameter " - "to override or define the image. 'image' will be removed in {version}.", - ) if not update: salt.utils.versions.warn_until( - "Magnesium", + "Aluminium", "'update' parameter has been deprecated. Future behavior will be the one of update=True" "It will be removed in {version}.", ) @@ -1412,141 +1368,6 @@ def pool_running( # In the corner case where test=True and the pool wasn"t defined # we may get not get our pool in the info dict and that is normal. is_running = info.get(name, {}).get("state", "stopped") == "running" - if is_running: - if updated: - action = "restarted" - if not __opts__["test"]: - __salt__["virt.pool_stop"]( - name, - connection=connection, - username=username, - password=password, - ) - # if the disk or LV group is already existing build will fail (issue #56454) - if ptype in BUILDABLE_POOL_TYPES - {"disk", "logical"}: - if not __opts__["test"]: - __salt__["virt.pool_build"]( - name, - connection=connection, - username=username, - password=password, - ) - action = "built, {}".format(action) - else: - action = "already running" - result = True - - if not is_running or updated or defined: - if not __opts__["test"]: - __salt__["virt.pool_start"]( - name, - connection=connection, - username=username, - password=password, - ) - - comment = "Pool {}".format(name) - change = "Pool" - if name in ret["changes"]: - comment = "{},".format(ret["comment"]) - change = "{},".format(ret["changes"][name]) - - if action != "already running": - ret["changes"][name] = "{} {}".format(change, action) - - ret["comment"] = "{} {}".format(comment, action) - ret["result"] = result - - except libvirt.libvirtError as err: - ret["comment"] = err.get_error_message() - ret["result"] = False - - return ret - - -def pool_running( - name, - ptype=None, - target=None, - permissions=None, - source=None, - transient=False, - autostart=True, - connection=None, - username=None, - password=None, -): - """ - Defines and starts a new pool with specified arguments. - - .. versionadded:: 2019.2.0 - - :param ptype: libvirt pool type - :param target: full path to the target device or folder. (Default: ``None``) - :param permissions: - target permissions. See :ref:`pool-define-permissions` for more details on this structure. - :param source: - dictionary containing keys matching the ``source_*`` parameters in function - :func:`salt.modules.virt.pool_define`. - :param transient: - when set to ``True``, the pool will be automatically undefined after being stopped. (Default: ``False``) - :param autostart: - Whether to start the pool when booting the host. (Default: ``True``) - :param start: - When ``True``, define and start the pool, otherwise the pool will be left stopped. - :param connection: libvirt connection URI, overriding defaults - :param username: username to connect with, overriding defaults - :param password: password to connect with, overriding defaults - - .. code-block:: yaml - - pool_name: - virt.pool_running - - .. code-block:: yaml - - pool_name: - virt.pool_running: - - ptype: netfs - - target: /mnt/cifs - - permissions: - - mode: 0770 - - owner: 1000 - - group: 100 - - source: - dir: samba_share - hosts: - - one.example.com - - two.example.com - format: cifs - - autostart: True - - """ - ret = pool_defined( - name, - ptype=ptype, - target=target, - permissions=permissions, - source=source, - transient=transient, - autostart=autostart, - connection=connection, - username=username, - password=password, - ) - defined = name in ret["changes"] and ret["changes"][name].startswith("Pool defined") - updated = name in ret["changes"] and ret["changes"][name].startswith("Pool updated") - - result = True if not __opts__["test"] else None - if ret["result"] is None or ret["result"]: - try: - info = __salt__["virt.pool_info"]( - name, connection=connection, username=username, password=password - ) - action = "started" - # In the corner case where test=True and the pool wasn't defined - # we may get not get our pool in the info dict and that is normal. - is_running = info.get(name, {}).get("state", "stopped") == "running" if is_running: if updated: action = "built, restarted" @@ -1798,7 +1619,7 @@ def volume_defined( format: raw - nocow: True - .. versionadded:: 3001 + .. versionadded:: Sodium """ ret = {"name": name, "changes": {}, "result": True, "comment": ""} diff --git a/salt/templates/virt/libvirt_domain.jinja b/salt/templates/virt/libvirt_domain.jinja index da22b41440..439ed83f7f 100644 --- a/salt/templates/virt/libvirt_domain.jinja +++ b/salt/templates/virt/libvirt_domain.jinja @@ -56,13 +56,21 @@ {% 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 %} + {%- if disk.type == 'network' %} + <source protocol='{{ disk.protocol }}' name='{{ disk.volume }}'{% if disk.get('query') %} query='{{ disk.query }}'{% endif %}> + {%- for host in disk.get('hosts') %} + <host name='{{ host.name }}'{% if host.get("port") %} port='{{ host.port }}'{% endif %}/> + {%- endfor %} + {%- if disk.get("auth") %} + <auth username='{{ disk.auth.username }}'> + <secret type='{{ disk.auth.type }}' usage='{{ disk.auth.usage}}'/> + </auth> + {%- endif %} + </source> + {%- 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 }}' /> diff --git a/tests/unit/modules/test_virt.py b/tests/unit/modules/test_virt.py index a5c876e27d..f53b4a85c1 100644 --- a/tests/unit/modules/test_virt.py +++ b/tests/unit/modules/test_virt.py @@ -17,6 +17,7 @@ import salt.syspaths import salt.utils.yaml from salt._compat import ElementTree as ET from salt.exceptions import CommandExecutionError, SaltInvocationError +from salt.ext import six # pylint: disable=import-error from salt.ext.six.moves import range # pylint: disable=redefined-builtin @@ -1137,65 +1138,6 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual("vdb2", source.attrib["volume"]) self.assertEqual("raw", disk.find("driver").get("type")) - def test_get_xml_volume_xen_dir(self): - """ - Test virt._gen_xml generating disks for a Xen hypervisor - """ - self.mock_conn.listStoragePools.return_value = ["default"] - pool_mock = MagicMock() - pool_mock.XMLDesc.return_value = ( - "<pool type='dir'><target><path>/path/to/images</path></target></pool>" - ) - volume_xml = "<volume><target><path>/path/to/images/hello_system</path></target></volume>" - pool_mock.storageVolLookupByName.return_value.XMLDesc.return_value = volume_xml - self.mock_conn.storagePoolLookupByName.return_value = pool_mock - diskp = virt._disk_profile( - self.mock_conn, - None, - "xen", - [{"name": "system", "pool": "default"}], - "hello", - ) - xml_data = virt._gen_xml( - self.mock_conn, "hello", 1, 512, diskp, [], "xen", "hvm", "x86_64", - ) - root = ET.fromstring(xml_data) - disk = root.findall(".//disk")[0] - self.assertEqual(disk.attrib["type"], "file") - self.assertEqual( - "/path/to/images/hello_system", disk.find("source").attrib["file"] - ) - - def test_get_xml_volume_xen_block(self): - """ - Test virt._gen_xml generating disks for a Xen hypervisor - """ - self.mock_conn.listStoragePools.return_value = ["default"] - pool_mock = MagicMock() - pool_mock.listVolumes.return_value = ["vol01"] - volume_xml = "<volume><target><path>/dev/to/vol01</path></target></volume>" - pool_mock.storageVolLookupByName.return_value.XMLDesc.return_value = volume_xml - self.mock_conn.storagePoolLookupByName.return_value = pool_mock - - for pool_type in ["logical", "disk", "iscsi", "scsi"]: - pool_mock.XMLDesc.return_value = "<pool type='{}'><source><device path='/dev/sda'/></source></pool>".format( - pool_type - ) - diskp = virt._disk_profile( - self.mock_conn, - None, - "xen", - [{"name": "system", "pool": "default", "source_file": "vol01"}], - "hello", - ) - xml_data = virt._gen_xml( - self.mock_conn, "hello", 1, 512, diskp, [], "xen", "hvm", "x86_64", - ) - root = ET.fromstring(xml_data) - disk = root.findall(".//disk")[0] - self.assertEqual(disk.attrib["type"], "block") - self.assertEqual("/dev/to/vol01", disk.find("source").attrib["dev"]) - def test_gen_xml_cdrom(self): """ Test virt._gen_xml(), generating a cdrom device (different disk type, no source) @@ -1849,21 +1791,21 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual( { "definition": False, - "disk": {"attached": [], "detached": []}, + "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, }, - virt.update("my vm"), + virt.update("my_vm"), ) # Same parameters passed than in default virt.defined state case self.assertEqual( { "definition": False, - "disk": {"attached": [], "detached": []}, + "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, }, virt.update( - "my vm", + "my_vm", cpu=None, mem=None, disk_profile=None, @@ -1886,235 +1828,707 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): { "definition": True, "cpu": True, - "disk": {"attached": [], "detached": []}, + "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, }, - virt.update("my vm", cpu=2), + virt.update("my_vm", cpu=2), ) setxml = ET.fromstring(define_mock.call_args[0][0]) self.assertEqual(setxml.find("vcpu").text, "2") self.assertEqual(setvcpus_mock.call_args[0][0], 2) - setxml = ET.fromstring(define_mock_boot.call_args[0][0]) + boot = { + "kernel": "/root/f8-i386-vmlinuz", + "initrd": "/root/f8-i386-initrd", + "cmdline": "console=ttyS0 ks=http://example.com/f8-i386/os/", + } + + boot_uefi = { + "loader": "/usr/share/OVMF/OVMF_CODE.fd", + "nvram": "/usr/share/OVMF/OVMF_VARS.ms.fd", + } + + invalid_boot = { + "loader": "/usr/share/OVMF/OVMF_CODE.fd", + "initrd": "/root/f8-i386-initrd", + } + + # Update with boot parameter case self.assertEqual( - setxml.find("os").find("loader").text, "/usr/share/new/OVMF_CODE.fd" + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("my_vm", boot=boot), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("os").find("kernel").text, "/root/f8-i386-vmlinuz") + self.assertEqual(setxml.find("os").find("initrd").text, "/root/f8-i386-initrd") + self.assertEqual( + setxml.find("os").find("cmdline").text, + "console=ttyS0 ks=http://example.com/f8-i386/os/", + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("os").find("kernel").text, "/root/f8-i386-vmlinuz") + self.assertEqual(setxml.find("os").find("initrd").text, "/root/f8-i386-initrd") + self.assertEqual( + setxml.find("os").find("cmdline").text, + "console=ttyS0 ks=http://example.com/f8-i386/os/", + ) + + self.assertEqual( + { + "definition": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("my_vm", boot=boot_uefi), + ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual( + setxml.find("os").find("loader").text, "/usr/share/OVMF/OVMF_CODE.fd" ) self.assertEqual(setxml.find("os").find("loader").attrib.get("readonly"), "yes") self.assertEqual(setxml.find("os").find("loader").attrib["type"], "pflash") self.assertEqual( setxml.find("os").find("nvram").attrib["template"], - "/usr/share/new/OVMF_VARS.ms.fd", + "/usr/share/OVMF/OVMF_VARS.ms.fd", ) - kernel_none = { - "kernel": None, - "initrd": None, - "cmdline": None, - } + with self.assertRaises(SaltInvocationError): + virt.update("my_vm", boot=invalid_boot) - uefi_none = {"loader": None, "nvram": None} + # Update memory case + setmem_mock = MagicMock(return_value=0) + domain_mock.setMemoryFlags = setmem_mock self.assertEqual( { "definition": True, + "mem": True, "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, }, - virt.update("vm_with_boot_param", boot=kernel_none), + virt.update("my_vm", mem=2048), ) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual(setxml.find("memory").text, "2048") + self.assertEqual(setxml.find("memory").get("unit"), "MiB") + self.assertEqual(setmem_mock.call_args[0][0], 2048 * 1024) + + # Update disks case + devattach_mock = MagicMock(return_value=0) + devdetach_mock = MagicMock(return_value=0) + domain_mock.attachDevice = devattach_mock + domain_mock.detachDevice = devdetach_mock + mock_chmod = MagicMock() + mock_run = MagicMock() + with patch.dict( + os.__dict__, {"chmod": mock_chmod, "makedirs": MagicMock()} + ): # pylint: disable=no-member + with patch.dict( + virt.__salt__, {"cmd.run": mock_run} + ): # pylint: disable=no-member + ret = virt.update( + "my_vm", + disk_profile="default", + disks=[ + { + "name": "cddrive", + "device": "cdrom", + "source_file": None, + "model": "ide", + }, + {"name": "added", "size": 2048}, + ], + ) + added_disk_path = os.path.join( + virt.__salt__["config.get"]("virt:images"), "my_vm_added.qcow2" + ) # pylint: disable=no-member + self.assertEqual( + mock_run.call_args[0][0], + 'qemu-img create -f qcow2 "{}" 2048M'.format(added_disk_path), + ) + self.assertEqual(mock_chmod.call_args[0][0], added_disk_path) + self.assertListEqual( + [None, os.path.join(root_dir, "my_vm_added.qcow2")], + [ + ET.fromstring(disk).find("source").get("file") + if str(disk).find("<source") > -1 + else None + for disk in ret["disk"]["attached"] + ], + ) - setxml = ET.fromstring(define_mock_boot.call_args[0][0]) - self.assertEqual(setxml.find("os").find("kernel"), None) - self.assertEqual(setxml.find("os").find("initrd"), None) - self.assertEqual(setxml.find("os").find("cmdline"), None) + self.assertListEqual( + ["my_vm_data", "libvirt-pool/my_vm_data2"], + [ + ET.fromstring(disk).find("source").get("volume") + or ET.fromstring(disk).find("source").get("name") + for disk in ret["disk"]["detached"] + ], + ) + self.assertEqual(devattach_mock.call_count, 2) + self.assertEqual(devdetach_mock.call_count, 2) + # Update nics case + yaml_config = """ + virt: + nic: + myprofile: + - network: default + name: eth0 + """ + mock_config = salt.utils.yaml.safe_load(yaml_config) + devattach_mock.reset_mock() + devdetach_mock.reset_mock() + with patch.dict( + salt.modules.config.__opts__, mock_config # pylint: disable=no-member + ): + ret = virt.update( + "my_vm", + nic_profile="myprofile", + interfaces=[ + { + "name": "eth0", + "type": "network", + "source": "default", + "mac": "52:54:00:39:02:b1", + }, + {"name": "eth1", "type": "network", "source": "newnet"}, + ], + ) + self.assertEqual( + ["newnet"], + [ + ET.fromstring(nic).find("source").get("network") + for nic in ret["interface"]["attached"] + ], + ) + self.assertEqual( + ["oldnet"], + [ + ET.fromstring(nic).find("source").get("network") + for nic in ret["interface"]["detached"] + ], + ) + devattach_mock.assert_called_once() + devdetach_mock.assert_called_once() + + # Remove nics case + devattach_mock.reset_mock() + devdetach_mock.reset_mock() + ret = virt.update("my_vm", nic_profile=None, interfaces=[]) + self.assertEqual([], ret["interface"]["attached"]) + self.assertEqual(2, len(ret["interface"]["detached"])) + devattach_mock.assert_not_called() + devdetach_mock.assert_called() + + # Remove disks case (yeah, it surely is silly) + devattach_mock.reset_mock() + devdetach_mock.reset_mock() + ret = virt.update("my_vm", disk_profile=None, disks=[]) + self.assertEqual([], ret["disk"]["attached"]) + self.assertEqual(3, len(ret["disk"]["detached"])) + devattach_mock.assert_not_called() + devdetach_mock.assert_called() + + # Graphics change test case self.assertEqual( { "definition": True, "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, }, - virt.update("vm_with_boot_param", boot={"efi": False}), + virt.update("my_vm", graphics={"type": "vnc"}), ) - setxml = ET.fromstring(define_mock_boot.call_args[0][0]) - self.assertEqual(setxml.find("os").find("nvram"), None) - self.assertEqual(setxml.find("os").find("loader"), None) + setxml = ET.fromstring(define_mock.call_args[0][0]) + self.assertEqual("vnc", setxml.find("devices/graphics").get("type")) + # Update with no diff case + pool_mock = MagicMock() + default_pool_desc = "<pool type='dir'></pool>" + rbd_pool_desc = """ + <pool type='rbd'> + <name>test-rbd</name> + <source> + <host name='ses2.tf.local'/> + <host name='ses3.tf.local' port='1234'/> + <name>libvirt-pool</name> + <auth type='ceph' username='libvirt'> + <secret usage='pool_test-rbd'/> + </auth> + </source> + </pool> + """ + pool_mock.XMLDesc.side_effect = [ + default_pool_desc, + rbd_pool_desc, + default_pool_desc, + rbd_pool_desc, + ] + self.mock_conn.storagePoolLookupByName.return_value = pool_mock + self.mock_conn.listStoragePools.return_value = ["test-rbd", "default"] + self.assertEqual( + { + "definition": False, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update( + "my_vm", + cpu=1, + mem=1024, + disk_profile="default", + disks=[ + {"name": "data", "size": 2048, "pool": "default"}, + { + "name": "data2", + "size": 4096, + "pool": "test-rbd", + "format": "raw", + }, + ], + nic_profile="myprofile", + interfaces=[ + { + "name": "eth0", + "type": "network", + "source": "default", + "mac": "52:54:00:39:02:b1", + }, + {"name": "eth1", "type": "network", "source": "oldnet"}, + ], + graphics={ + "type": "spice", + "listen": {"type": "address", "address": "127.0.0.1"}, + }, + ), + ) + + # Failed XML description update case + self.mock_conn.defineXML.side_effect = self.mock_libvirt.libvirtError( + "Test error" + ) + setmem_mock.reset_mock() + with self.assertRaises(self.mock_libvirt.libvirtError): + virt.update("my_vm", mem=2048) + + # Failed single update failure case + self.mock_conn.defineXML = MagicMock(return_value=True) + setmem_mock.side_effect = self.mock_libvirt.libvirtError( + "Failed to live change memory" + ) self.assertEqual( { "definition": True, + "errors": ["Failed to live change memory"], "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, }, - virt.update("vm_with_boot_param", boot=uefi_none), + virt.update("my_vm", mem=2048), + ) + + # Failed multiple updates failure case + self.assertEqual( + { + "definition": True, + "errors": ["Failed to live change memory"], + "cpu": True, + "disk": {"attached": [], "detached": [], "updated": []}, + "interface": {"attached": [], "detached": []}, + }, + virt.update("my_vm", cpu=4, mem=2048), + ) + + def test_update_backing_store(self): + """ + Test updating a disk with a backing store + """ + xml = """ + <domain type='kvm' id='7'> + <name>my_vm</name> + <memory unit='KiB'>1048576</memory> + <currentMemory unit='KiB'>1048576</currentMemory> + <vcpu placement='auto'>1</vcpu> + <os> + <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type> + </os> + <devices> + <disk type='volume' device='disk'> + <driver name='qemu' type='qcow2' cache='none' io='native'/> + <source pool='default' volume='my_vm_system' index='1'/> + <backingStore type='file' index='2'> + <format type='qcow2'/> + <source file='/path/to/base.qcow2'/> + <backingStore/> + </backingStore> + <target dev='vda' bus='virtio'/> + <alias name='virtio-disk0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/> + </disk> + </devices> + </domain> + """ + domain_mock = self.set_mock_vm("my_vm", xml) + domain_mock.OSType.return_value = "hvm" + self.mock_conn.defineXML.return_value = True + updatedev_mock = MagicMock(return_value=0) + domain_mock.updateDeviceFlags = updatedev_mock + self.mock_conn.listStoragePools.return_value = ["default"] + self.mock_conn.storagePoolLookupByName.return_value.XMLDesc.return_value = ( + "<pool type='dir'/>" + ) + + ret = virt.update( + "my_vm", + disks=[ + { + "name": "system", + "pool": "default", + "backing_store_path": "/path/to/base.qcow2", + "backing_store_format": "qcow2", + }, + ], + ) + self.assertFalse(ret["definition"]) + self.assertFalse(ret["disk"]["attached"]) + self.assertFalse(ret["disk"]["detached"]) + + def test_update_removables(self): + """ + Test attaching, detaching, changing removable devices + """ + xml = """ + <domain type='kvm' id='7'> + <name>my_vm</name> + <memory unit='KiB'>1048576</memory> + <currentMemory unit='KiB'>1048576</currentMemory> + <vcpu placement='auto'>1</vcpu> + <os> + <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type> + </os> + <devices> + <disk type='network' device='cdrom'> + <driver name='qemu' type='raw' cache='none' io='native'/> + <source protocol='https' name='/dvd-image-1.iso'> + <host name='test-srv.local' port='80'/> + </source> + <backingStore/> + <target dev='hda' bus='ide'/> + <readonly/> + <alias name='ide0-0-0'/> + <address type='drive' controller='0' bus='0' target='0' unit='0'/> + </disk> + <disk type='file' device='cdrom'> + <driver name='qemu' type='raw' cache='none' io='native'/> + <target dev='hdb' bus='ide'/> + <readonly/> + <alias name='ide0-0-1'/> + <address type='drive' controller='0' bus='0' target='0' unit='1'/> + </disk> + <disk type='file' device='cdrom'> + <driver name='qemu' type='raw' cache='none' io='native'/> + <source file='/srv/dvd-image-2.iso'/> + <backingStore/> + <target dev='hdc' bus='ide'/> + <readonly/> + <alias name='ide0-0-2'/> + <address type='drive' controller='0' bus='0' target='0' unit='2'/> + </disk> + <disk type='file' device='cdrom'> + <driver name='qemu' type='raw' cache='none' io='native'/> + <source file='/srv/dvd-image-3.iso'/> + <backingStore/> + <target dev='hdd' bus='ide'/> + <readonly/> + <alias name='ide0-0-3'/> + <address type='drive' controller='0' bus='0' target='0' unit='3'/> + </disk> + <disk type='network' device='cdrom'> + <driver name='qemu' type='raw' cache='none' io='native'/> + <source protocol='https' name='/dvd-image-6.iso'> + <host name='test-srv.local' port='80'/> + </source> + <backingStore/> + <target dev='hde' bus='ide'/> + <readonly/> + </disk> + </devices> + </domain> + """ + domain_mock = self.set_mock_vm("my_vm", xml) + domain_mock.OSType.return_value = "hvm" + self.mock_conn.defineXML.return_value = True + updatedev_mock = MagicMock(return_value=0) + domain_mock.updateDeviceFlags = updatedev_mock + + ret = virt.update( + "my_vm", + disks=[ + { + "name": "dvd1", + "device": "cdrom", + "source_file": None, + "model": "ide", + }, + { + "name": "dvd2", + "device": "cdrom", + "source_file": "/srv/dvd-image-4.iso", + "model": "ide", + }, + { + "name": "dvd3", + "device": "cdrom", + "source_file": "/srv/dvd-image-2.iso", + "model": "ide", + }, + { + "name": "dvd4", + "device": "cdrom", + "source_file": "/srv/dvd-image-5.iso", + "model": "ide", + }, + { + "name": "dvd5", + "device": "cdrom", + "source_file": "/srv/dvd-image-6.iso", + "model": "ide", + }, + ], + ) + + self.assertTrue(ret["definition"]) + self.assertFalse(ret["disk"]["attached"]) + self.assertFalse(ret["disk"]["detached"]) + self.assertEqual( + [ + { + "type": "file", + "device": "cdrom", + "driver": { + "name": "qemu", + "type": "raw", + "cache": "none", + "io": "native", + }, + "backingStore": None, + "target": {"dev": "hda", "bus": "ide"}, + "readonly": None, + "alias": {"name": "ide0-0-0"}, + "address": { + "type": "drive", + "controller": "0", + "bus": "0", + "target": "0", + "unit": "0", + }, + }, + { + "type": "file", + "device": "cdrom", + "driver": { + "name": "qemu", + "type": "raw", + "cache": "none", + "io": "native", + }, + "target": {"dev": "hdb", "bus": "ide"}, + "readonly": None, + "alias": {"name": "ide0-0-1"}, + "address": { + "type": "drive", + "controller": "0", + "bus": "0", + "target": "0", + "unit": "1", + }, + "source": {"file": "/srv/dvd-image-4.iso"}, + }, + { + "type": "file", + "device": "cdrom", + "driver": { + "name": "qemu", + "type": "raw", + "cache": "none", + "io": "native", + }, + "backingStore": None, + "target": {"dev": "hdd", "bus": "ide"}, + "readonly": None, + "alias": {"name": "ide0-0-3"}, + "address": { + "type": "drive", + "controller": "0", + "bus": "0", + "target": "0", + "unit": "3", + }, + "source": {"file": "/srv/dvd-image-5.iso"}, + }, + { + "type": "file", + "device": "cdrom", + "driver": { + "name": "qemu", + "type": "raw", + "cache": "none", + "io": "native", + }, + "backingStore": None, + "target": {"dev": "hde", "bus": "ide"}, + "readonly": None, + "source": {"file": "/srv/dvd-image-6.iso"}, + }, + ], + [ + salt.utils.xmlutil.to_dict(ET.fromstring(disk), True) + for disk in ret["disk"]["updated"] + ], ) - setxml = ET.fromstring(define_mock_boot.call_args[0][0]) - self.assertEqual(setxml.find("os").find("loader"), None) - self.assertEqual(setxml.find("os").find("nvram"), None) - - def test_update_memtune_params(self): + def test_update_existing_boot_params(self): """ - Test virt.update() with memory tuning parameters. + Test virt.update() with existing boot parameters. """ - xml_with_memtune_params = """ + root_dir = os.path.join(salt.syspaths.ROOT_DIR, "srv", "salt-images") + xml_boot = """ <domain type='kvm' id='8'> <name>vm_with_boot_param</name> <memory unit='KiB'>1048576</memory> <currentMemory unit='KiB'>1048576</currentMemory> - <maxMemory slots="12" unit="bytes">1048576</maxMemory> <vcpu placement='auto'>1</vcpu> - <memtune> - <hard_limit unit="KiB">1048576</hard_limit> - <soft_limit unit="KiB">2097152</soft_limit> - <swap_hard_limit unit="KiB">2621440</swap_hard_limit> - <min_guarantee unit='KiB'>671088</min_guarantee> - </memtune> <os> <type arch='x86_64' machine='pc-i440fx-2.6'>hvm</type> + <kernel>/boot/oldkernel</kernel> + <initrd>/boot/initrdold.img</initrd> + <cmdline>console=ttyS0 ks=http://example.com/old/os/</cmdline> + <loader>/usr/share/old/OVMF_CODE.fd</loader> + <nvram>/usr/share/old/OVMF_VARS.ms.fd</nvram> </os> + <devices> + <disk type='file' device='disk'> + <driver name='qemu' type='qcow2'/> + <source file='{0}{1}vm_with_boot_param_system.qcow2'/> + <backingStore/> + <target dev='vda' bus='virtio'/> + <alias name='virtio-disk0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x0'/> + </disk> + <disk type='file' device='disk'> + <driver name='qemu' type='qcow2'/> + <source file='{0}{1}vm_with_boot_param_data.qcow2'/> + <backingStore/> + <target dev='vdb' bus='virtio'/> + <alias name='virtio-disk1'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x07' function='0x1'/> + </disk> + <interface type='network'> + <mac address='52:54:00:39:02:b1'/> + <source network='default' bridge='virbr0'/> + <target dev='vnet0'/> + <model type='virtio'/> + <alias name='net0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0'/> + </interface> + <interface type='network'> + <mac address='52:54:00:39:02:b2'/> + <source network='oldnet' bridge='virbr1'/> + <target dev='vnet1'/> + <model type='virtio'/> + <alias name='net1'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x1'/> + </interface> + <graphics type='spice' port='5900' autoport='yes' listen='127.0.0.1'> + <listen type='address' address='127.0.0.1'/> + </graphics> + <video> + <model type='qxl' ram='65536' vram='65536' vgamem='16384' heads='1' primary='yes'/> + <alias name='video0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/> + </video> + </devices> </domain> - """ - domain_mock = self.set_mock_vm("vm_with_memtune_param", xml_with_memtune_params) - domain_mock.OSType = MagicMock(return_value="hvm") - define_mock = MagicMock(return_value=True) - self.mock_conn.defineXML = define_mock + """.format( + root_dir, os.sep + ) + domain_mock_boot = self.set_mock_vm("vm_with_boot_param", xml_boot) + domain_mock_boot.OSType = MagicMock(return_value="hvm") + define_mock_boot = MagicMock(return_value=True) + self.mock_conn.defineXML = define_mock_boot + boot_new = { + "kernel": "/root/new-vmlinuz", + "initrd": "/root/new-initrd", + "cmdline": "console=ttyS0 ks=http://example.com/new/os/", + } - memtune_new_val = { - "boot": "0.7g", - "current": "2.5g", - "max": "3096m", - "slots": "10", - "soft_limit": "2048m", - "hard_limit": "1024", - "swap_hard_limit": "2.5g", - "min_guarantee": "1 g", + uefi_boot_new = { + "loader": "/usr/share/new/OVMF_CODE.fd", + "nvram": "/usr/share/new/OVMF_VARS.ms.fd", } - domain_mock.setMemoryFlags.return_value = 0 self.assertEqual( { "definition": True, "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, - "mem": True, }, - virt.update("vm_with_memtune_param", mem=memtune_new_val), - ) - self.assertEqual( - domain_mock.setMemoryFlags.call_args[0][0], int(2.5 * 1024 ** 2) - ) - - setxml = ET.fromstring(define_mock.call_args[0][0]) - self.assertEqual( - setxml.find("memtune").find("soft_limit").text, str(2048 * 1024) + virt.update("vm_with_boot_param", boot=boot_new), ) + setxml_boot = ET.fromstring(define_mock_boot.call_args[0][0]) self.assertEqual( - setxml.find("memtune").find("hard_limit").text, str(1024 * 1024) + setxml_boot.find("os").find("kernel").text, "/root/new-vmlinuz" ) + self.assertEqual(setxml_boot.find("os").find("initrd").text, "/root/new-initrd") self.assertEqual( - setxml.find("memtune").find("swap_hard_limit").text, - str(int(2.5 * 1024 ** 2)), + setxml_boot.find("os").find("cmdline").text, + "console=ttyS0 ks=http://example.com/new/os/", ) - self.assertEqual( - setxml.find("memtune").find("swap_hard_limit").get("unit"), "KiB", - ) - self.assertEqual( - setxml.find("memtune").find("min_guarantee").text, str(1 * 1024 ** 3) - ) - self.assertEqual( - setxml.find("memtune").find("min_guarantee").attrib.get("unit"), "bytes" - ) - self.assertEqual(setxml.find("maxMemory").text, str(3096 * 1024 ** 2)) - self.assertEqual(setxml.find("maxMemory").attrib.get("slots"), "10") - self.assertEqual(setxml.find("currentMemory").text, str(int(2.5 * 1024 ** 3))) - self.assertEqual(setxml.find("memory").text, str(int(0.7 * 1024 ** 3))) - - max_slot_reverse = { - "slots": "10", - "max": "3096m", - } - self.assertEqual( - { - "definition": True, - "disk": {"attached": [], "detached": [], "updated": []}, - "interface": {"attached": [], "detached": []}, - }, - virt.update("vm_with_memtune_param", mem=max_slot_reverse), - ) - setxml = ET.fromstring(define_mock.call_args[0][0]) - self.assertEqual(setxml.find("maxMemory").text, str(3096 * 1024 ** 2)) - self.assertEqual(setxml.find("maxMemory").get("unit"), "bytes") - self.assertEqual(setxml.find("maxMemory").attrib.get("slots"), "10") - - max_swap_none = { - "boot": "0.7g", - "current": "2.5g", - "max": None, - "slots": "10", - "soft_limit": "2048m", - "hard_limit": "1024", - "swap_hard_limit": None, - "min_guarantee": "1 g", - } - domain_mock.setMemoryFlags.reset_mock() self.assertEqual( { "definition": True, "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, - "mem": True, }, - virt.update("vm_with_memtune_param", mem=max_swap_none), - ) - self.assertEqual( - domain_mock.setMemoryFlags.call_args[0][0], int(2.5 * 1024 ** 2) + virt.update("vm_with_boot_param", boot=uefi_boot_new), ) - setxml = ET.fromstring(define_mock.call_args[0][0]) - self.assertEqual( - setxml.find("memtune").find("soft_limit").text, str(2048 * 1024) - ) - self.assertEqual( - setxml.find("memtune").find("hard_limit").text, str(1024 * 1024) - ) - self.assertEqual(setxml.find("memtune").find("swap_hard_limit"), None) + setxml = ET.fromstring(define_mock_boot.call_args[0][0]) self.assertEqual( - setxml.find("memtune").find("min_guarantee").text, str(1 * 1024 ** 3) + setxml.find("os").find("loader").text, "/usr/share/new/OVMF_CODE.fd" ) + self.assertEqual(setxml.find("os").find("loader").attrib.get("readonly"), "yes") + self.assertEqual(setxml.find("os").find("loader").attrib["type"], "pflash") self.assertEqual( - setxml.find("memtune").find("min_guarantee").attrib.get("unit"), "bytes" + setxml.find("os").find("nvram").attrib["template"], + "/usr/share/new/OVMF_VARS.ms.fd", ) - self.assertEqual(setxml.find("maxMemory").text, None) - self.assertEqual(setxml.find("currentMemory").text, str(int(2.5 * 1024 ** 3))) - self.assertEqual(setxml.find("memory").text, str(int(0.7 * 1024 ** 3))) - memtune_none = { - "soft_limit": None, - "hard_limit": None, - "swap_hard_limit": None, - "min_guarantee": None, + kernel_none = { + "kernel": None, + "initrd": None, + "cmdline": None, } + uefi_none = {"loader": None, "nvram": None} + self.assertEqual( { "definition": True, "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, }, - virt.update("vm_with_memtune_param", mem=memtune_none), + virt.update("vm_with_boot_param", boot=kernel_none), ) - setxml = ET.fromstring(define_mock.call_args[0][0]) - self.assertEqual(setxml.find("memtune").find("soft_limit"), None) - self.assertEqual(setxml.find("memtune").find("hard_limit"), None) - self.assertEqual(setxml.find("memtune").find("swap_hard_limit"), None) - self.assertEqual(setxml.find("memtune").find("min_guarantee"), None) - - max_none = { - "max": None, - } + setxml = ET.fromstring(define_mock_boot.call_args[0][0]) + self.assertEqual(setxml.find("os").find("kernel"), None) + self.assertEqual(setxml.find("os").find("initrd"), None) + self.assertEqual(setxml.find("os").find("cmdline"), None) self.assertEqual( { @@ -2122,48 +2536,12 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): "disk": {"attached": [], "detached": [], "updated": []}, "interface": {"attached": [], "detached": []}, }, - virt.update("vm_with_memtune_param", mem=max_none), + virt.update("vm_with_boot_param", boot=uefi_none), ) - setxml = ET.fromstring(define_mock.call_args[0][0]) - self.assertEqual(setxml.find("maxMemory"), None) - self.assertEqual(setxml.find("currentMemory").text, str(int(1 * 1024 ** 2))) - self.assertEqual(setxml.find("memory").text, str(int(1 * 1024 ** 2))) - - def test_handle_unit(self): - """ - Test regex function for handling units - """ - valid_case = [ - ("2", 2097152), - ("42", 44040192), - ("5b", 5), - ("2.3Kib", 2355), - ("5.8Kb", 5800), - ("16MiB", 16777216), - ("20 GB", 20000000000), - ("16KB", 16000), - (".5k", 512), - ("2.k", 2048), - ] - - for key, val in valid_case: - self.assertEqual(virt._handle_unit(key), val) - - invalid_case = [ - ("9ib", "invalid units"), - ("8byte", "invalid units"), - ("512bytes", "invalid units"), - ("4 Kbytes", "invalid units"), - ("3.4.MB", "invalid number"), - ("", "invalid number"), - ("bytes", "invalid number"), - ("2HB", "invalid units"), - ] - - for key, val in invalid_case: - with self.assertRaises(SaltInvocationError): - virt._handle_unit(key) + setxml = ET.fromstring(define_mock_boot.call_args[0][0]) + self.assertEqual(setxml.find("os").find("loader"), None) + self.assertEqual(setxml.find("os").find("nvram"), None) def test_mixed_dict_and_list_as_profile_objects(self): """ @@ -2242,30 +2620,6 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.assertEqual("/foo/bar", loader["path"]) self.assertEqual("yes", loader["readonly"]) - def test_cpu_baseline(self): - """ - Test virt.cpu_baseline() - """ - capabilities_xml = dedent( - """<capabilities> - <host> - <uuid>44454c4c-3400-105a-8033-b3c04f4b344a</uuid> - <cpu> - <arch>x86_64</arch> - <vendor>Intel</vendor> - </cpu> - </host> - </capabilities>""" - ) - - baseline_cpu_xml = b"""<cpu match="exact" mode="custom"> - <vendor>Intel</vendor> - </cpu>""" - - self.mock_conn.getCapabilities.return_value = capabilities_xml - self.mock_conn.baselineCPU.return_value = baseline_cpu_xml - self.assertMultiLineEqual(str(baseline_cpu_xml), str(virt.cpu_baseline())) - def test_parse_qemu_img_info(self): """ Make sure that qemu-img info output is properly parsed @@ -2906,7 +3260,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): "cpuselection": {"default": True, "toggle": False}, "deviceboot": {"default": True, "toggle": False}, "disksnapshot": {"default": True, "toggle": False}, - "acpi": {"default": False, "toggle": True}, + "acpi": {"default": True, "toggle": True}, "apic": {"default": True, "toggle": False}, "pae": {"default": True, "toggle": False}, "nonpae": {"default": True, "toggle": False}, @@ -2944,7 +3298,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): "deviceboot": {"default": True, "toggle": False}, "disksnapshot": {"default": True, "toggle": False}, "acpi": {"default": True, "toggle": True}, - "apic": {"default": False, "toggle": False}, + "apic": {"default": True, "toggle": False}, }, }, { @@ -3230,7 +3584,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): "44454c4c-3400-105a-8033-b3c04f4b344a", caps["host"]["host"]["uuid"] ) self.assertEqual( - {"qemu", "kvm"}, {domainCaps["domain"] for domainCaps in caps["domains"]} + {"qemu", "kvm"}, {domainCaps["domain"] for domainCaps in caps["domains"]}, ) def test_network_tag(self): @@ -3894,6 +4248,7 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): """ mock_pool = MagicMock() mock_pool.delete = MagicMock(return_value=0) + mock_pool.XMLDesc.return_value = "<pool type='dir'/>" self.mock_conn.storagePoolLookupByName = MagicMock(return_value=mock_pool) res = virt.pool_delete("test-pool") @@ -3907,12 +4262,12 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.mock_libvirt.VIR_STORAGE_POOL_DELETE_NORMAL ) - def test_pool_undefine_secret(self): + def test_pool_delete_secret(self): """ - Test virt.pool_undefine function where the pool has a secret + Test virt.pool_delete function where the pool has a secret """ mock_pool = MagicMock() - mock_pool.undefine = MagicMock(return_value=0) + mock_pool.delete = MagicMock(return_value=0) mock_pool.XMLDesc.return_value = """ <pool type='rbd'> <name>test-ses</name> @@ -3929,11 +4284,16 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): mock_undefine = MagicMock(return_value=0) self.mock_conn.secretLookupByUsage.return_value.undefine = mock_undefine - res = virt.pool_undefine("test-ses") + res = virt.pool_delete("test-ses") self.assertTrue(res) self.mock_conn.storagePoolLookupByName.assert_called_once_with("test-ses") - mock_pool.undefine.assert_called_once_with() + + # Shouldn't be called with another parameter so far since those are not implemented + # and thus throwing exceptions. + mock_pool.delete.assert_called_once_with( + self.mock_libvirt.VIR_STORAGE_POOL_DELETE_NORMAL + ) self.mock_conn.secretLookupByUsage.assert_called_once_with( self.mock_libvirt.VIR_SECRET_USAGE_TYPE_CEPH, "pool_test-ses" @@ -4202,6 +4562,24 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): </source> </pool>""" + expected_xml = ( + '<pool type="rbd">' + "<name>default</name>" + "<uuid>20fbe05c-ab40-418a-9afa-136d512f0ede</uuid>" + '<capacity unit="bytes">1999421108224</capacity>' + '<allocation unit="bytes">713207042048</allocation>' + '<available unit="bytes">1286214066176</available>' + "<source>" + '<host name="ses4.tf.local" />' + '<host name="ses5.tf.local" />' + '<auth type="ceph" username="libvirt">' + '<secret uuid="14e9a0f1-8fbf-4097-b816-5b094c182212" />' + "</auth>" + "<name>iscsi-images</name>" + "</source>" + "</pool>" + ) + mock_secret = MagicMock() self.mock_conn.secretLookupByUUIDString = MagicMock(return_value=mock_secret) @@ -4222,23 +4600,6 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): self.mock_conn.storagePoolDefineXML.assert_not_called() mock_secret.setValue.assert_called_once_with(b"secret") - # Case where the secret can't be found - self.mock_conn.secretLookupByUUIDString = MagicMock( - side_effect=self.mock_libvirt.libvirtError("secret not found") - ) - self.assertFalse( - virt.pool_update( - "default", - "rbd", - source_name="iscsi-images", - source_hosts=["ses4.tf.local", "ses5.tf.local"], - source_auth={"username": "libvirt", "password": "c2VjcmV0"}, - ) - ) - self.mock_conn.storagePoolDefineXML.assert_not_called() - self.mock_conn.secretDefineXML.assert_called_once() - mock_secret.setValue.assert_called_once_with(b"secret") - def test_pool_update_password_create(self): """ Test the pool_update function, where the password only is changed @@ -4898,3 +5259,279 @@ class VirtTestCase(TestCase, LoaderModuleMockMixin): "vm2", inactive=False, ) + + def test_volume_define(self): + """ + Test virt.volume_define function + """ + # Normal test case + pool_mock = MagicMock() + pool_mock.XMLDesc.return_value = "<pool type='dir'></pool>" + self.mock_conn.storagePoolLookupByName.return_value = pool_mock + + self.assertTrue( + virt.volume_define( + "testpool", + "myvm_system.qcow2", + 8192, + allocation=4096, + format="qcow2", + type="file", + ) + ) + + expected_xml = ( + "<volume type='file'>\n" + " <name>myvm_system.qcow2</name>\n" + " <source>\n" + " </source>\n" + " <capacity unit='KiB'>8388608</capacity>\n" + " <allocation unit='KiB'>4194304</allocation>\n" + " <target>\n" + " <format type='qcow2'/>\n" + " </target>\n" + "</volume>" + ) + + pool_mock.createXML.assert_called_once_with(expected_xml, 0) + + # backing store test case + pool_mock.reset_mock() + self.assertTrue( + virt.volume_define( + "testpool", + "myvm_system.qcow2", + 8192, + allocation=4096, + format="qcow2", + type="file", + backing_store={"path": "/path/to/base.raw", "format": "raw"}, + ) + ) + + expected_xml = ( + "<volume type='file'>\n" + " <name>myvm_system.qcow2</name>\n" + " <source>\n" + " </source>\n" + " <capacity unit='KiB'>8388608</capacity>\n" + " <allocation unit='KiB'>4194304</allocation>\n" + " <target>\n" + " <format type='qcow2'/>\n" + " </target>\n" + " <backingStore>\n" + " <path>/path/to/base.raw</path>\n" + " <format type='raw'/>\n" + " </backingStore>\n" + "</volume>" + ) + + pool_mock.createXML.assert_called_once_with(expected_xml, 0) + + # logical pool test case + pool_mock.reset_mock() + pool_mock.XMLDesc.return_value = "<pool type='logical'></pool>" + self.mock_conn.storagePoolLookupByName.return_value = pool_mock + + self.assertTrue( + virt.volume_define( + "testVG", + "myvm_system", + 8192, + backing_store={"path": "/dev/testVG/base"}, + ) + ) + + expected_xml = ( + "<volume>\n" + " <name>myvm_system</name>\n" + " <source>\n" + " </source>\n" + " <capacity unit='KiB'>8388608</capacity>\n" + " <allocation unit='KiB'>8388608</allocation>\n" + " <target>\n" + " </target>\n" + " <backingStore>\n" + " <path>/dev/testVG/base</path>\n" + " </backingStore>\n" + "</volume>" + ) + + pool_mock.createXML.assert_called_once_with(expected_xml, 0) + + def test_volume_upload(self): + """ + Test virt.volume_upload function + """ + pool_mock = MagicMock() + vol_mock = MagicMock() + pool_mock.storageVolLookupByName.return_value = vol_mock + self.mock_conn.storagePoolLookupByName.return_value = pool_mock + stream_mock = MagicMock() + self.mock_conn.newStream.return_value = stream_mock + + open_mock = MagicMock() + close_mock = MagicMock() + with patch.dict( + os.__dict__, {"open": open_mock, "close": close_mock} + ): # pylint: disable=no-member + # Normal case + self.assertTrue(virt.volume_upload("pool0", "vol1.qcow2", "/path/to/file")) + stream_mock.sendAll.assert_called_once() + stream_mock.finish.assert_called_once() + self.mock_conn.close.assert_called_once() + vol_mock.upload.assert_called_once_with(stream_mock, 0, 0, 0) + + # Sparse upload case + stream_mock.sendAll.reset_mock() + vol_mock.upload.reset_mock() + self.assertTrue( + virt.volume_upload( + "pool0", + "vol1.qcow2", + "/path/to/file", + offset=123, + length=456, + sparse=True, + ) + ) + stream_mock.sendAll.assert_not_called() + stream_mock.sparseSendAll.assert_called_once() + vol_mock.upload.assert_called_once_with( + stream_mock, + 123, + 456, + self.mock_libvirt.VIR_STORAGE_VOL_UPLOAD_SPARSE_STREAM, + ) + + # Upload unsupported case + vol_mock.upload.side_effect = self.mock_libvirt.libvirtError("Unsupported") + self.assertRaisesRegex( + CommandExecutionError, + "Unsupported", + virt.volume_upload, + "pool0", + "vol1.qcow2", + "/path/to/file", + ) + + def test_get_disks(self): + """ + Test the virt.get_disks function + """ + # test with volumes + vm_def = """<domain type='kvm' id='3'> + <name>srv01</name> + <devices> + <disk type='volume' device='disk'> + <driver name='qemu' type='qcow2' cache='none' io='native'/> + <source pool='default' volume='srv01_system'/> + <backingStore/> + <target dev='vda' bus='virtio'/> + <alias name='virtio-disk0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/> + </disk> + <disk type='volume' device='disk'> + <driver name='qemu' type='qcow2' cache='none' io='native'/> + <source pool='default' volume='srv01_data'/> + <backingStore type='file' index='1'> + <format type='qcow2'/> + <source file='/var/lib/libvirt/images/vol01'/> + <backingStore/> + </backingStore> + <target dev='vdb' bus='virtio'/> + <alias name='virtio-disk1'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x05' function='0x0'/> + </disk> + <disk type='volume' device='disk'> + <driver name='qemu' type='qcow2' cache='none' io='native'/> + <source pool='default' volume='vm05_system'/> + <backingStore type='file' index='1'> + <format type='qcow2'/> + <source file='/var/lib/libvirt/images/vm04_system.qcow2'/> + <backingStore type='file' index='2'> + <format type='raw'/> + <source file='/var/testsuite-data/disk-image-template.raw'/> + <backingStore/> + </backingStore> + </backingStore> + <target dev='vdc' bus='virtio'/> + <alias name='virtio-disk0'/> + <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/> + </disk> + <disk type='network' device='cdrom'> + <driver name='qemu' type='raw' cache='none' io='native'/> + <source protocol='http' name='/pub/iso/myimage.iso' query='foo=bar&baz=flurb' index='1'> + <host name='dev-srv.tf.local' port='80'/> + </source> + <target dev='hda' bus='ide'/> + <readonly/> + <alias name='ide0-0-0'/> + <address type='drive' controller='0' bus='0' target='0' unit='0'/> + </disk> + </devices> + </domain> + """ + self.set_mock_vm("srv01", vm_def) + + pool_mock = MagicMock() + pool_mock.storageVolLookupByName.return_value.info.return_value = [ + 0, + 1234567, + 12345, + ] + pool_mock.storageVolLookupByName.return_value.XMLDesc.side_effect = [ + "<volume />", + """ + <volume> + <backingStore> + <path>/var/lib/libvirt/images/vol01</path> + <format type="qcow2"/> + </backingStore> + </volume>""", + ] + self.mock_conn.storagePoolLookupByName.return_value = pool_mock + + self.assertDictEqual( + virt.get_disks("srv01"), + { + "vda": { + "type": "disk", + "file": "default/srv01_system", + "file format": "qcow2", + "disk size": 12345, + "virtual size": 1234567, + }, + "vdb": { + "type": "disk", + "file": "default/srv01_data", + "file format": "qcow2", + "disk size": 12345, + "virtual size": 1234567, + "backing file": { + "file": "/var/lib/libvirt/images/vol01", + "file format": "qcow2", + }, + }, + "vdc": { + "type": "disk", + "file": "default/vm05_system", + "file format": "qcow2", + "disk size": 12345, + "virtual size": 1234567, + "backing file": { + "file": "/var/lib/libvirt/images/vm04_system.qcow2", + "file format": "qcow2", + "backing file": { + "file": "/var/testsuite-data/disk-image-template.raw", + "file format": "raw", + }, + }, + }, + "hda": { + "type": "cdrom", + "file format": "raw", + "file": "http://dev-srv.tf.local:80/pub/iso/myimage.iso?foo=bar&baz=flurb", + }, + }, + ) diff --git a/tests/unit/states/test_virt.py b/tests/unit/states/test_virt.py index 0a47562074..6d38829870 100644 --- a/tests/unit/states/test_virt.py +++ b/tests/unit/states/test_virt.py @@ -8,6 +8,7 @@ import tempfile import salt.states.virt as virt import salt.utils.files from salt.exceptions import CommandExecutionError, SaltInvocationError +from salt.ext import six from tests.support.mixins import LoaderModuleMockMixin from tests.support.mock import MagicMock, mock_open, patch from tests.support.runtests import RUNTIME_VARS @@ -263,375 +264,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"}, - } - 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", - connection="someconnection", - username="libvirtuser", - password="supersecret", - ), - 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, - install=False, - start=False, - pub_key="/path/to/key.pub", - priv_key="/path/to/key", - connection="someconnection", - username="libvirtuser", - password="supersecret", - ) - - # 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, - test=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", - 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, - test=True, - boot_dev=None, - ) - - # 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, - test=True, - boot_dev=None, - ) - def test_defined(self): """ defined state test cases. @@ -1012,126 +644,18 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): "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, image="/path/to/img.qcow2"), - ret, - ) - init_mock.assert_called_with( - "myvm", - cpu=2, - mem=2048, - os_type=None, - arch=None, - boot=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, - connection=None, - username=None, - password=None, - ) - start_mock.assert_called_with( - "myvm", connection=None, username=None, password=None - ) - - # Test image parameter with disks with defined image - 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", - } - ) - disks = [ - { - "name": "system", - "size": 8192, - "overlay_image": True, - "pool": "default", - "image": "/path/to/image.qcow2", - }, - {"name": "data", "size": 16834}, - ] - self.assertDictEqual( - virt.running( - "myvm", cpu=2, mem=2048, disks=disks, image="/path/to/img.qcow2" - ), - ret, - ) - init_mock.assert_called_with( - "myvm", - cpu=2, - mem=2048, - os_type=None, - arch=None, - boot=None, - disk=None, - disks=disks, - nic=None, - interfaces=None, - graphics=None, - hypervisor=None, - start=False, - seed=True, - install=True, - pub_key=None, - priv_key=None, - connection=None, - username=None, - password=None, - ) - start_mock.assert_called_with( - "myvm", connection=None, username=None, password=None + "virt.list_domains": MagicMock(return_value=["myvm"]), + }, + ): + ret.update( + { + "changes": {"myvm": {"started": True}}, + "comment": "Domain myvm started", + } ) + self.assertDictEqual(virt.running("myvm"), ret) - # Test image parameter with disks without defined image + # Test defining and starting a guest the old way init_mock = MagicMock(return_value=True) start_mock = MagicMock(return_value=0) with patch.dict( @@ -1149,18 +673,12 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): "comment": "Domain myvm defined and started", } ) - disks = [ - { - "name": "system", - "size": 8192, - "overlay_image": True, - "pool": "default", - }, - {"name": "data", "size": 16834}, - ] self.assertDictEqual( virt.running( - "myvm", cpu=2, mem=2048, disks=disks, image="/path/to/img.qcow2" + "myvm", + cpu=2, + mem=2048, + disks=[{"name": "system", "image": "/path/to/img.qcow2"}], ), ret, ) @@ -1172,16 +690,7 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): arch=None, boot=None, disk=None, - disks=[ - { - "name": "system", - "size": 8192, - "overlay_image": True, - "pool": "default", - "image": "/path/to/img.qcow2", - }, - {"name": "data", "size": 16834}, - ], + disks=[{"name": "system", "image": "/path/to/img.qcow2"}], nic=None, interfaces=None, graphics=None, @@ -2100,199 +1609,6 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): virt.network_defined("mynet", "br2", "bridge"), 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. @@ -3303,45 +2619,6 @@ class LibvirtTestCase(TestCase, LoaderModuleMockMixin): ret, ) - # test case with test=True and pool to be defined - for mock in mocks: - mocks[mock].reset_mock() - with patch.dict( - virt.__salt__, - { # pylint: disable=no-member - "virt.pool_info": MagicMock(return_value={}), - }, - ): - ret.update( - { - "changes": { - "mypool": "Pool defined, marked for autostart, started" - }, - "comment": "Pool mypool defined, marked for autostart, started", - "result": None, - } - ) - self.assertDictEqual( - virt.pool_running( - "mypool", - ptype="logical", - target="/dev/base", - permissions={ - "mode": "0770", - "owner": 1000, - "group": 100, - "label": "seclabel", - }, - source={"devices": [{"path": "/dev/sda"}]}, - transient=True, - autostart=True, - connection="myconnection", - username="user", - password="secret", - ), - ret, - ) - def test_pool_deleted(self): """ Test the pool_deleted state -- 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