From fc5638bf50bf303b4dd7ca3270167d677a93d10d Mon Sep 17 00:00:00 2001 From: Chris Gunn Date: Wed, 19 Jan 2022 14:59:48 -0800 Subject: [PATCH] Add support for QEMU/libvirt platform. (#1677) * Add support for QEMU/libvirt platform. Add basic support for running LISA framework on Linux platform using QEMU via the libvirt API. This includes succesfully running the Microsoft T0 tests against the CBL-Mariner image. --- .../continuous-integration-workflow.yml | 3 +- lisa/mixin_modules.py | 8 + lisa/sut_orchestrator/__init__.py | 1 + lisa/sut_orchestrator/qemu/context.py | 27 ++ lisa/sut_orchestrator/qemu/platform.py | 447 ++++++++++++++++++ lisa/sut_orchestrator/qemu/schema.py | 31 ++ microsoft/runbook/qemu/CBL-Mariner.yml | 6 + microsoft/runbook/qemu/CBL-Mariner/user-data | 34 ++ microsoft/runbook/qemu/qemu.yml | 21 + poetry.lock | 28 +- pyproject.toml | 2 + 11 files changed, 603 insertions(+), 5 deletions(-) create mode 100644 lisa/sut_orchestrator/qemu/context.py create mode 100644 lisa/sut_orchestrator/qemu/platform.py create mode 100644 lisa/sut_orchestrator/qemu/schema.py create mode 100644 microsoft/runbook/qemu/CBL-Mariner.yml create mode 100644 microsoft/runbook/qemu/CBL-Mariner/user-data create mode 100644 microsoft/runbook/qemu/qemu.yml diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index c6e927a40d..c4857ce878 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -38,7 +38,8 @@ jobs: - name: Install Python dependencies (Linux-only) if: runner.os == 'Linux' run: | - sudo apt install libgirepository1.0-dev libcairo2-dev + sudo apt update + sudo apt install libgirepository1.0-dev libcairo2-dev qemu-utils libvirt-dev - name: Install Python dependencies run: make setup diff --git a/lisa/mixin_modules.py b/lisa/mixin_modules.py index 93b56eb8b9..85f88646d0 100644 --- a/lisa/mixin_modules.py +++ b/lisa/mixin_modules.py @@ -4,6 +4,8 @@ # The file imports all the mix-in types that can be initialized # using reflection. +import platform + import lisa.combinators.batch_combinator # noqa: F401 import lisa.combinators.csv_combinator # noqa: F401 import lisa.combinators.grid_combinator # noqa: F401 @@ -16,6 +18,12 @@ import lisa.sut_orchestrator.azure.hooks # noqa: F401 import lisa.sut_orchestrator.azure.transformers # noqa: F401 import lisa.sut_orchestrator.ready # noqa: F401 + +if platform.system() == "Linux": + import lisa.sut_orchestrator.qemu.context # noqa: F401 + import lisa.sut_orchestrator.qemu.platform # noqa: F401 + import lisa.sut_orchestrator.qemu.schema # noqa: F401 + import lisa.transformers.kernel_source_installer # noqa: F401 import lisa.transformers.script_transformer # noqa: F401 import lisa.transformers.to_list # noqa: F401 diff --git a/lisa/sut_orchestrator/__init__.py b/lisa/sut_orchestrator/__init__.py index 00c6de4e97..824051a2e7 100644 --- a/lisa/sut_orchestrator/__init__.py +++ b/lisa/sut_orchestrator/__init__.py @@ -3,4 +3,5 @@ from lisa.util.constants import PLATFORM_READY AZURE = "azure" +QEMU = "qemu" READY = PLATFORM_READY diff --git a/lisa/sut_orchestrator/qemu/context.py b/lisa/sut_orchestrator/qemu/context.py new file mode 100644 index 0000000000..7d7be53f38 --- /dev/null +++ b/lisa/sut_orchestrator/qemu/context.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass +from typing import Any, Dict, Optional + +from lisa.environment import Environment +from lisa.node import Node + + +@dataclass +class EnvironmentContext: + ssh_public_key: str = "" + + +@dataclass +class NodeContext: + vm_name: str = "" + cloud_init_file_path: str = "" + os_disk_base_file_path: str = "" + os_disk_file_path: str = "" + extra_cloud_init_user_data: Optional[Dict[str, Any]] = None + + +def get_environment_context(environment: Environment) -> EnvironmentContext: + return environment.get_context(EnvironmentContext) + + +def get_node_context(node: Node) -> NodeContext: + return node.get_context(NodeContext) diff --git a/lisa/sut_orchestrator/qemu/platform.py b/lisa/sut_orchestrator/qemu/platform.py new file mode 100644 index 0000000000..31f250c659 --- /dev/null +++ b/lisa/sut_orchestrator/qemu/platform.py @@ -0,0 +1,447 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT license. + +import io +import os +import random +import string +import subprocess +import time +import xml.etree.ElementTree as ET # noqa: N817 +from typing import List, Optional, Tuple, Type + +import libvirt # type: ignore +import pycdlib # type: ignore +import yaml + +from lisa import schema +from lisa.environment import Environment +from lisa.feature import Feature +from lisa.node import Node, RemoteNode +from lisa.platform_ import Platform +from lisa.util import LisaException, constants, get_public_key_data +from lisa.util.logger import Logger + +from .. import QEMU +from .context import get_environment_context, get_node_context +from .schema import QemuNodeSchema + + +class QemuPlatform(Platform): + def __init__(self, runbook: schema.Platform) -> None: + super().__init__(runbook=runbook) + + @classmethod + def type_name(cls) -> str: + return QEMU + + @classmethod + def supported_features(cls) -> List[Type[Feature]]: + return [] + + def _prepare_environment(self, environment: Environment, log: Logger) -> bool: + return True + + def _deploy_environment(self, environment: Environment, log: Logger) -> None: + self._deploy_nodes(environment, log) + + def _delete_environment(self, environment: Environment, log: Logger) -> None: + with libvirt.open("qemu:///system") as qemu_conn: + self._delete_nodes(environment, log, qemu_conn) + + def _deploy_nodes(self, environment: Environment, log: Logger) -> None: + self._configure_nodes(environment, log) + + with libvirt.open("qemu:///system") as qemu_conn: + try: + self._create_nodes(environment, log, qemu_conn) + self._fill_nodes_metadata(environment, log, qemu_conn) + + except Exception as ex: + self._delete_nodes(environment, log, qemu_conn) + raise ex + + # Pre-determine all the nodes' properties, including the name of all the resouces + # to be created. This makes it easier to cleanup everything after the test is + # finished (or fails). + def _configure_nodes(self, environment: Environment, log: Logger) -> None: + environment_context = get_environment_context(environment) + + # Generate a random name for the VMs. + test_suffix = "".join(random.choice(string.ascii_uppercase) for i in range(5)) + vm_name_prefix = f"lisa-{test_suffix}" + + environment_context.ssh_public_key = get_public_key_data( + self.runbook.admin_private_key_file + ) + + assert environment.runbook.nodes_requirement + for i, node_space in enumerate(environment.runbook.nodes_requirement): + assert isinstance( + node_space, schema.NodeSpace + ), f"actual: {type(node_space)}" + + qemu_node_runbook: QemuNodeSchema = node_space.get_extended_runbook( + QemuNodeSchema, type_name=QEMU + ) + + vm_disks_dir = os.path.dirname(qemu_node_runbook.qcow2) + + node = environment.create_node_from_requirement(node_space) + node_context = get_node_context(node) + + node_context.vm_name = f"{vm_name_prefix}-{i}" + node_context.cloud_init_file_path = os.path.join( + vm_disks_dir, f"{node_context.vm_name}-cloud-init.iso" + ) + node_context.os_disk_base_file_path = qemu_node_runbook.qcow2 + node_context.os_disk_file_path = os.path.join( + vm_disks_dir, f"{node_context.vm_name}-os.qcow2" + ) + + if not node.name: + node.name = node_context.vm_name + + # Read extra cloud-init data. + if ( + qemu_node_runbook.cloud_init + and qemu_node_runbook.cloud_init.extra_user_data + ): + extra_user_data_file_path = str( + constants.RUNBOOK_PATH.joinpath( + qemu_node_runbook.cloud_init.extra_user_data + ) + ) + with open(extra_user_data_file_path, "r") as file: + node_context.extra_cloud_init_user_data = yaml.safe_load(file) + + # Create all the VMs. + def _create_nodes( + self, environment: Environment, log: Logger, qemu_conn: libvirt.virConnect + ) -> None: + for node in environment.nodes.list(): + # Create cloud-init ISO file. + self._create_node_cloud_init_iso(environment, log, node) + + # Create OS disk from the provided image. + self._create_node_os_disk(environment, log, node) + + # Create libvirt domain (i.e. VM). + xml = self._create_node_domain_xml(environment, log, node) + domain = qemu_conn.defineXML(xml) + domain.create() + + # Delete all the VMs. + def _delete_nodes( + self, environment: Environment, log: Logger, qemu_conn: libvirt.virConnect + ) -> None: + for node in environment.nodes.list(): + node_context = get_node_context(node) + log.debug(f"Delete VM: {node_context.vm_name}") + + # Shutdown and delete the VM. + self._stop_and_delete_vm(environment, log, qemu_conn, node) + + # Delete the files created for the VM. + try: + os.remove(node_context.os_disk_file_path) + except Exception as ex: + log.warning(f"OS disk delete failed. {ex}") + + try: + os.remove(node_context.cloud_init_file_path) + except Exception as ex: + log.warning(f"cloud-init ISO file delete failed. {ex}") + + # Delete a VM. + def _stop_and_delete_vm( + self, + environment: Environment, + log: Logger, + qemu_conn: libvirt.virConnect, + node: Node, + ) -> None: + node_context = get_node_context(node) + + # Find the VM. + try: + domain = qemu_conn.lookupByName(node_context.vm_name) + except libvirt.libvirtError as ex: + log.warning(f"VM delete failed. Can't find domain. {ex}") + return + + # Stop the VM. + try: + # In the libvirt API, "destroy" means "stop". + domain.destroy() + except libvirt.libvirtError as ex: + log.warning(f"VM stop failed. {ex}") + + # Undefine the VM. + try: + domain.undefineFlags( + libvirt.VIR_DOMAIN_UNDEFINE_MANAGED_SAVE + | libvirt.VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA + | libvirt.VIR_DOMAIN_UNDEFINE_NVRAM + | libvirt.VIR_DOMAIN_UNDEFINE_CHECKPOINTS_METADATA + ) + except libvirt.libvirtError as ex: + log.warning(f"VM delete failed. {ex}") + + # Retrieve the VMs' dynamic properties (e.g. IP address). + def _fill_nodes_metadata( + self, environment: Environment, log: Logger, qemu_conn: libvirt.virConnect + ) -> None: + # Give all the VMs some time to boot and then acquire an IP address. + timeout = time.time() + 30 # seconds + + for node in environment.nodes.list(): + assert isinstance(node, RemoteNode) + + # Get the VM's IP address. + address = self._get_node_ip_address( + environment, log, qemu_conn, node, timeout + ) + + # Set SSH connection info for the node. + node.set_connection_info( + address=address, + username=self.runbook.admin_username, + private_key_file=self.runbook.admin_private_key_file, + ) + + # Create a cloud-init ISO for a VM. + def _create_node_cloud_init_iso( + self, environment: Environment, log: Logger, node: Node + ) -> None: + environment_context = get_environment_context(environment) + node_context = get_node_context(node) + + user_data = { + "users": [ + "default", + { + "name": self.runbook.admin_username, + "shell": "/bin/bash", + "sudo": ["ALL=(ALL) NOPASSWD:ALL"], + "groups": ["sudo", "docker"], + "ssh_authorized_keys": [environment_context.ssh_public_key], + }, + ], + } + + if node_context.extra_cloud_init_user_data: + user_data.update(node_context.extra_cloud_init_user_data) + + meta_data = { + "local-hostname": node_context.vm_name, + } + + # Note: cloud-init requires the user-data file to be prefixed with + # `#cloud-config`. + user_data_string = "#cloud-config\n" + yaml.safe_dump(user_data) + meta_data_string = yaml.safe_dump(meta_data) + + self._create_iso( + node_context.cloud_init_file_path, + [("/user-data", user_data_string), ("/meta-data", meta_data_string)], + ) + + # Create an ISO file. + def _create_iso(self, file_path: str, files: List[Tuple[str, str]]) -> None: + iso = pycdlib.PyCdlib() + iso.new(joliet=3, vol_ident="cidata") + + for i, file in enumerate(files): + path, contents = file + contents_data = contents.encode() + iso.add_fp( + io.BytesIO(contents_data), + len(contents_data), + f"/{i}.;1", + joliet_path=path, + ) + + iso.write(file_path) + + # Create the OS disk. + def _create_node_os_disk( + self, environment: Environment, log: Logger, node: Node + ) -> None: + node_context = get_node_context(node) + + # Create a new differencing image with the user provided image as the base. + subprocess.run( + [ + "qemu-img", + "create", + "-F", + "qcow2", + "-f", + "qcow2", + "-b", + node_context.os_disk_base_file_path, + node_context.os_disk_file_path, + ], + check=True, + capture_output=True, + ) + + # Create the XML definition for the VM. + def _create_node_domain_xml( + self, environment: Environment, log: Logger, node: Node + ) -> str: + node_context = get_node_context(node) + + domain = ET.Element("domain") + domain.attrib["type"] = "kvm" + + name = ET.SubElement(domain, "name") + name.text = node_context.vm_name + + memory = ET.SubElement(domain, "memory") + memory.attrib["unit"] = "GiB" + memory.text = "4" + + vcpu = ET.SubElement(domain, "vcpu") + vcpu.text = "2" + + os = ET.SubElement(domain, "os") + os.attrib["firmware"] = "efi" + + os_type = ET.SubElement(os, "type") + os_type.text = "hvm" + + features = ET.SubElement(domain, "features") + + acpi = ET.SubElement(features, "acpi") # noqa: F841 + + apic = ET.SubElement(features, "apic") # noqa: F841 + + cpu = ET.SubElement(domain, "cpu") + cpu.attrib["mode"] = "host-passthrough" + + clock = ET.SubElement(domain, "clock") + clock.attrib["offset"] = "utc" + + on_poweroff = ET.SubElement(domain, "on_poweroff") + on_poweroff.text = "destroy" + + on_reboot = ET.SubElement(domain, "on_reboot") + on_reboot.text = "restart" + + on_crash = ET.SubElement(domain, "on_crash") + on_crash.text = "destroy" + + devices = ET.SubElement(domain, "devices") + + serial = ET.SubElement(devices, "serial") + serial.attrib["type"] = "pty" + + serial_target = ET.SubElement(serial, "target") + serial_target.attrib["type"] = "isa-serial" + serial_target.attrib["port"] = "0" + + serial_target_model = ET.SubElement(serial_target, "model") + serial_target_model.attrib["name"] = "isa-serial" + + console = ET.SubElement(devices, "console") + console.attrib["type"] = "pty" + + console_target = ET.SubElement(console, "target") + console_target.attrib["type"] = "serial" + console_target.attrib["port"] = "0" + + video = ET.SubElement(devices, "video") + + video_model = ET.SubElement(video, "model") + video_model.attrib["type"] = "qxl" + + graphics = ET.SubElement(devices, "graphics") + graphics.attrib["type"] = "spice" + + network_interface = ET.SubElement(devices, "interface") + network_interface.attrib["type"] = "network" + + network_interface_source = ET.SubElement(network_interface, "source") + network_interface_source.attrib["network"] = "default" + + cloud_init_disk = ET.SubElement(devices, "disk") + cloud_init_disk.attrib["type"] = "file" + cloud_init_disk.attrib["device"] = "cdrom" + + cloud_init_disk_driver = ET.SubElement(cloud_init_disk, "driver") + cloud_init_disk_driver.attrib["name"] = "qemu" + cloud_init_disk_driver.attrib["type"] = "raw" + + cloud_init_disk_target = ET.SubElement(cloud_init_disk, "target") + cloud_init_disk_target.attrib["dev"] = "sda" + cloud_init_disk_target.attrib["bus"] = "sata" + + cloud_init_disk_target = ET.SubElement(cloud_init_disk, "source") + cloud_init_disk_target.attrib["file"] = node_context.cloud_init_file_path + + os_disk = ET.SubElement(devices, "disk") + os_disk.attrib["type"] = "file" + os_disk.attrib["device"] = "disk" + + os_disk_driver = ET.SubElement(os_disk, "driver") + os_disk_driver.attrib["name"] = "qemu" + os_disk_driver.attrib["type"] = "qcow2" + + os_disk_target = ET.SubElement(os_disk, "target") + os_disk_target.attrib["dev"] = "sdb" + os_disk_target.attrib["bus"] = "sata" + + os_disk_target = ET.SubElement(os_disk, "source") + os_disk_target.attrib["file"] = node_context.os_disk_file_path + + xml = ET.tostring(domain, "unicode") + return xml + + # Wait for the VM to boot and then get the IP address. + def _get_node_ip_address( + self, + environment: Environment, + log: Logger, + qemu_conn: libvirt.virConnect, + node: Node, + timeout: float, + ) -> str: + node_context = get_node_context(node) + + while True: + addr = self._try_get_node_ip_address(environment, log, qemu_conn, node) + if addr: + return addr + + if time.time() > timeout: + raise LisaException(f"no IP addresses found for {node_context.vm_name}") + + # Try to get the IP address of the VM. + def _try_get_node_ip_address( + self, + environment: Environment, + log: Logger, + qemu_conn: libvirt.virConnect, + node: Node, + ) -> Optional[str]: + node_context = get_node_context(node) + + domain = qemu_conn.lookupByName(node_context.vm_name) + + # Acquire IP address from libvirt's DHCP server. + interfaces = domain.interfaceAddresses( + libvirt.VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE + ) + if len(interfaces) < 1: + return None + + interface_name = next(iter(interfaces)) + addrs = interfaces[interface_name]["addrs"] + if len(addrs) < 1: + return None + + addr = addrs[0]["addr"] + assert isinstance(addr, str) + return addr diff --git a/lisa/sut_orchestrator/qemu/schema.py b/lisa/sut_orchestrator/qemu/schema.py new file mode 100644 index 0000000000..6637bb7dca --- /dev/null +++ b/lisa/sut_orchestrator/qemu/schema.py @@ -0,0 +1,31 @@ +from dataclasses import dataclass +from typing import Optional + +from dataclasses_json import dataclass_json + + +# Configuration options for cloud-init ISO generation for the VM. +@dataclass_json() +@dataclass +class CloudInitSchema: + # Additional values to apply to the cloud-init user-data file. + extra_user_data: Optional[str] = None + + +# QEMU orchestrator's global configuration options. +@dataclass_json() +@dataclass +class QemuPlatformSchema: + pass + + +# QEMU orchestrator's per-node configuration options. +@dataclass_json() +@dataclass +class QemuNodeSchema: + # The disk image to use for the node. + # The file must use the qcow2 file format and should not be changed during test + # execution. + qcow2: str = "" + # Configuration options for cloud-init. + cloud_init: Optional[CloudInitSchema] = None diff --git a/microsoft/runbook/qemu/CBL-Mariner.yml b/microsoft/runbook/qemu/CBL-Mariner.yml new file mode 100644 index 0000000000..c3f02903df --- /dev/null +++ b/microsoft/runbook/qemu/CBL-Mariner.yml @@ -0,0 +1,6 @@ +name: qemu default +include: + - path: ./qemu.yml +variable: + - name: extra_user_data + value: "./CBL-Mariner/user-data" diff --git a/microsoft/runbook/qemu/CBL-Mariner/user-data b/microsoft/runbook/qemu/CBL-Mariner/user-data new file mode 100644 index 0000000000..19756152cf --- /dev/null +++ b/microsoft/runbook/qemu/CBL-Mariner/user-data @@ -0,0 +1,34 @@ +write_files: +- content: | + [Unit] + Description=Startup script + After=local-fs.target network-online.target network.target + Wants=local-fs.target network-online.target network.target + + [Service] + ExecStart=/usr/local/bin/startup.sh + Type=oneshot + + [Install] + WantedBy=multi-user.target + owner: root:root + path: /lib/systemd/system/startup.service + permissions: '0644' +- content: | + #!/bin/bash + iptables -P INPUT ACCEPT + iptables -P OUTPUT ACCEPT + owner: root:root + path: /usr/local/bin/startup.sh + permissions: '0755' +- content: | + IPv4: \4 + IPv6: \6 + path: /etc/issue + append: true +runcmd: +- systemctl enable startup.service +- systemctl start startup.service +- dnf install openssh-server -y +- systemctl enable sshd.service +- systemctl start sshd.service diff --git a/microsoft/runbook/qemu/qemu.yml b/microsoft/runbook/qemu/qemu.yml new file mode 100644 index 0000000000..2acb92fe68 --- /dev/null +++ b/microsoft/runbook/qemu/qemu.yml @@ -0,0 +1,21 @@ +name: qemu default +include: + - path: ../tiers/tier.yml +variable: + - name: keep_environment + value: "no" + - name: qcow2 + value: "" + - name: extra_user_data + value: "" +notifier: + - type: html +platform: + - type: qemu + admin_private_key_file: $(admin_private_key_file) + keep_environment: $(keep_environment) + requirement: + qemu: + qcow2: $(qcow2) + cloud_init: + extra_user_data: $(extra_user_data) diff --git a/poetry.lock b/poetry.lock index 268d9a5361..03af19e3af 100644 --- a/poetry.lock +++ b/poetry.lock @@ -560,6 +560,14 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "libvirt-python" +version = "7.10.0" +description = "The libvirt virtualization API python binding" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "markupsafe" version = "2.0.1" @@ -822,6 +830,14 @@ category = "main" optional = false python-versions = ">=3.6, <4" +[[package]] +name = "pycdlib" +version = "1.12.0" +description = "Pure python ISO manipulation library" +category = "main" +optional = false +python-versions = "*" + [[package]] name = "pycodestyle" version = "2.7.0" @@ -1602,7 +1618,7 @@ watchmedo = ["PyYAML (>=3.10)", "argh (>=0.24.1)"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "8d969efc8b82999d94f728f0842a51e105e217a9149363e0acb4d7e11786aaed" +content-hash = "6293068d74d54ab78d84d56881e9cde4477ae2aea4aead3403a87d4432118729" [metadata.files] alabaster = [ @@ -1673,13 +1689,10 @@ babel = [ {file = "Babel-2.9.1.tar.gz", hash = "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"}, ] bcrypt = [ - {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b589229207630484aefe5899122fb938a5b017b0f4349f769b8c13e78d99a8fd"}, {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a0584a92329210fcd75eb8a3250c5a941633f8bfaf2a18f81009b097732839b7"}, - {file = "bcrypt-3.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:56e5da069a76470679f312a7d3d23deb3ac4519991a0361abc11da837087b61d"}, {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, @@ -1907,6 +1920,9 @@ jinja2 = [ {file = "Jinja2-3.0.2-py3-none-any.whl", hash = "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"}, {file = "Jinja2-3.0.2.tar.gz", hash = "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45"}, ] +libvirt-python = [ + {file = "libvirt-python-7.10.0.tar.gz", hash = "sha256:267774bbdf99d47515274542880499437dc94ae291771f5663c62020a62da975"}, +] markupsafe = [ {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, @@ -2122,6 +2138,10 @@ pycairo = [ {file = "pycairo-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:f123d3818e30b77b7209d70a6dcfd5b4e34885f9fa539d92dd7ff3e4e2037213"}, {file = "pycairo-1.20.1.tar.gz", hash = "sha256:1ee72b035b21a475e1ed648e26541b04e5d7e753d75ca79de8c583b25785531b"}, ] +pycdlib = [ + {file = "pycdlib-1.12.0-py2.py3-none-any.whl", hash = "sha256:b16fb79f552740c28d74314a6fcbab91b44f44f36a23041725adea11436512a6"}, + {file = "pycdlib-1.12.0.tar.gz", hash = "sha256:6549f29f8d9b4ba6933b309c51ff6a4dc79ef5831a81e065cb6cc385c1ba7fe2"}, +] pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, diff --git a/pyproject.toml b/pyproject.toml index ad764f435e..ed43842894 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,8 @@ pep8-naming = "^0.12.0" types-toml = "^0.1.5" Pillow = "^9.0.0" azure-storage-file-share = "12.4.0" +libvirt-python = {version = "^7.10.0", platform = 'linux'} +pycdlib = "^1.12.0" [tool.poetry.dev-dependencies] black = "^21.7b0"