Blame tools/test-case-generators/generate-all-test-cases

Packit Service 3a6627
#!/usr/bin/python3
Packit Service 3a6627
Packit Service 3a6627
# pylint: disable=line-too-long
Packit Service 3a6627
"""
Packit Service 3a6627
    generate-all-test-cases
Packit Service 3a6627
Packit Service 3a6627
    Script to generate all image test cases based on distro x arch x image-type
Packit Service 3a6627
    matrix read from `distro-arch-imagetype-map.json` or passed file. One can
Packit Service 3a6627
    filter the matrix just to a subset using `--distro`, `--arch` or
Packit Service 3a6627
    `--image-types` arguments.
Packit Service 3a6627
Packit Service 3a6627
    The script is intended to be run from the osbuild-composer sources directory
Packit Service 3a6627
    root, for which the image test cases should be (re)generated.
Packit Service 3a6627
Packit Service 3a6627
    Example (builds rhel-8 qcow2 images on aarch64 s390x ppc64le):
Packit Service 3a6627
    tools/test-case-generators/generate-all-test-cases \
Packit Service 3a6627
        --output=test/data/manifests \
Packit Service 3a6627
        --image-x86_64 ~/Downloads/Images/Fedora-Cloud-Base-33-1.2.x86_64.qcow2 \
Packit Service 3a6627
        --image-ppc64le ~/Downloads/Images/Fedora-Cloud-Base-33-1.2.ppc64le.qcow2 \
Packit Service 3a6627
        --image-aarch64 ~/Downloads/Images/Fedora-Cloud-Base-33-1.2.aarch64.qcow2 \
Packit Service 3a6627
        --image-s390x ~/Downloads/Images/Fedora-Cloud-Base-33-1.2.s390x.qcow2 \
Packit Service 3a6627
        --arch aarch64 s390x ppc64le \
Packit Service 3a6627
        --distro rhel-8 \
Packit Service 3a6627
        --image-types qcow2
Packit Service 3a6627
Packit Service 3a6627
    The script spins up an ephemeral QEMU VM (called Runner) per each required
Packit Service 3a6627
    architecture. The CWD (sources dir root) is attached to the Runner using
Packit Service 3a6627
    virtfs as readonly and later mounted into /mnt/sources on the Runner.
Packit Service 3a6627
    The 'output' directory is also attached to the Runner using virtfs as r/w
Packit Service 3a6627
    and later mounted into /mnt/output on the Runner. The next execution on
Packit Service 3a6627
    Runners is as follows:
Packit Service 3a6627
    - Wait for the runner to be configured using cloud-init.
Packit Service 3a6627
        - includes installing osbuild, osbuild-composer and golang
Packit Service 3a6627
    - Create /mnt/sources and /mnt/output and mount appropriate devices
Packit Service 3a6627
    - in /mnt/sources execute tools/test-case-generators/generate-test-cases
Packit Service 3a6627
      for each requested distro and image type combination on the particular
Packit Service 3a6627
      architecture. Output manifest is written into /mnt/output
Packit Service 3a6627
Packit Service 3a6627
    One can use e.q. Fedora cloud qcow2 images:
Packit Service 3a6627
    x86_64: https://download.fedoraproject.org/pub/fedora/linux/releases/33/Cloud/x86_64/images/Fedora-Cloud-Base-33-1.2.x86_64.qcow2
Packit Service 3a6627
    aarch64: https://download.fedoraproject.org/pub/fedora/linux/releases/33/Cloud/aarch64/images/Fedora-Cloud-Base-33-1.2.aarch64.qcow2
Packit Service 3a6627
    ppc64le: https://download.fedoraproject.org/pub/fedora-secondary/releases/33/Cloud/ppc64le/images/Fedora-Cloud-Base-33-1.2.ppc64le.qcow2
Packit Service 3a6627
    s390x: https://download.fedoraproject.org/pub/fedora-secondary/releases/33/Cloud/s390x/images/Fedora-Cloud-Base-33-1.2.s390x.qcow2
Packit Service 3a6627
Packit Service 3a6627
    aarch64 special note:
Packit Service 3a6627
    make sure to have the *edk2-aarch64* package installed, which provides UEFI
Packit Service 3a6627
    builds for QEMU and AARCH64 (/usr/share/edk2/aarch64/QEMU_EFI.fd)
Packit Service 3a6627
    https://fedoraproject.org/wiki/Architectures/AArch64/Install_with_QEMU
Packit Service 3a6627
Packit Service 3a6627
    Images need to have enough disk space to be able to build images using
Packit Service 3a6627
    osbuild. You can resize them using 'qemu-img resize <image> 20G' command.
Packit Service 3a6627
Packit Service 3a6627
    Known issues:
Packit Service 3a6627
    - The tool does not work with RHEL qcow2 images, becuase the "9p" filesystem
Packit Service 3a6627
    is not supported on RHEL.
Packit Service 3a6627
Packit Service 3a6627
    HW requirements:
Packit Service 3a6627
    - The x86_64 VM uses 1 CPU and 1GB of RAM
Packit Service 3a6627
    - The aarch64, s390x and ppc64le VMs each uses 2CPU and 2GB of RAM
Packit Service 3a6627
    - Unless filtered using `--arch` option, the script starts 4 VMs in parallel
Packit Service 3a6627
Packit Service 3a6627
    Tested with:
Packit Service 3a6627
    - Fedora 32 (x86_64) and QEMU version 4.2.1
Packit Service 3a6627
Packit Service 3a6627
    Not tested:
Packit Service 3a6627
    - installation of newer 'osbuild-composer' or 'osbuild' packages from the
Packit Service 3a6627
      local 'osbuild' repository, which is configured by cloud-init. Not sure
Packit Service 3a6627
      how dnf will behave if there are packages for multiple architectures.
Packit Service 3a6627
"""
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
import argparse
Packit Service 3a6627
import subprocess
Packit Service 3a6627
import json
Packit Service 3a6627
import os
Packit Service 3a6627
import tempfile
Packit Service 3a6627
import shutil
Packit Service 3a6627
import time
Packit Service 3a6627
import socket
Packit Service 3a6627
import contextlib
Packit Service 3a6627
import multiprocessing
Packit Service 3a6627
import logging
Packit Service 3a6627
Packit Service 3a6627
import yaml
Packit Service 3a6627
import paramiko
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
# setup logging
Packit Service 3a6627
log = logging.getLogger("generate-all-test-cases")
Packit Service 3a6627
log.setLevel(logging.INFO)
Packit Service 3a6627
formatter = logging.Formatter("%(asctime)s [%(levelname)s] - %(processName)s: %(message)s")
Packit Service 3a6627
sh = logging.StreamHandler()
Packit Service 3a6627
sh.setFormatter(formatter)
Packit Service 3a6627
log.addHandler(sh)
Packit Service 3a6627
Packit Service 3a6627
# suppress all errors logged by paramiko
Packit Service 3a6627
paramiko.util.log_to_file(os.devnull)
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
class RunnerMountPoint:
Packit Service 3a6627
    """
Packit Service 3a6627
    Data structure to represent basic data used by Runners to attach host
Packit Service 3a6627
    directory as virtfs to the guest and then to mount it.
Packit Service 3a6627
    """
Packit Service 3a6627
    def __init__(self, src_host, dst_guest, mount_tag, readonly):
Packit Service 3a6627
        self.src_host = src_host
Packit Service 3a6627
        self.dst_guest = dst_guest
Packit Service 3a6627
        self.readonly = readonly
Packit Service 3a6627
        self.mount_tag = mount_tag
Packit Service 3a6627
Packit Service 3a6627
    @staticmethod
Packit Service 3a6627
    def get_default_runner_mount_points(output_dir, sources_dir=None):
Packit Service 3a6627
        """
Packit Service 3a6627
        Returns a list of default mount points used by Runners when generating
Packit Service 3a6627
        image test cases.
Packit Service 3a6627
        """
Packit Service 3a6627
        sources_dir = os.getcwd() if sources_dir is None else sources_dir
Packit Service 3a6627
        mount_points = [
Packit Service 3a6627
            RunnerMountPoint(sources_dir, "/mnt/sources", "sources", True),
Packit Service 3a6627
            RunnerMountPoint(output_dir, "/mnt/output", "output", False)
Packit Service 3a6627
        ]
Packit Service 3a6627
        return mount_points
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
class BaseRunner(contextlib.AbstractContextManager):
Packit Service 3a6627
    """
Packit Service 3a6627
    Base class representing a QEMU VM runner, which is used for generating image
Packit Service 3a6627
    test case definitions.
Packit Service 3a6627
Packit Service 3a6627
    Each architecture-specific runner should inherit from this class and define
Packit Service 3a6627
    QEMU_BIN, QEMU_CMD class variable. These will be used to successfully boot
Packit Service 3a6627
    VM for the given architecture.
Packit Service 3a6627
    """
Packit Service 3a6627
Packit Service 3a6627
    # name of the QEMU binary to use for running the VM
Packit Service 3a6627
    QEMU_BIN = None
Packit Service 3a6627
    # the actual command to use for running QEMU VM
Packit Service 3a6627
    QEMU_CMD = None
Packit Service 3a6627
Packit Service 3a6627
    def __init__(self, image, user, passwd, cdrom_iso=None, mount_points=None):
Packit Service 3a6627
        self._check_qemu_bin()
Packit Service 3a6627
Packit Service 3a6627
        # path to image to run
Packit Service 3a6627
        self.image = image
Packit Service 3a6627
        # path to cdrom iso to attach (for cloud-init)
Packit Service 3a6627
        self.cdrom_iso = cdrom_iso
Packit Service 3a6627
        # host directories to share with the VM as virtfs devices
Packit Service 3a6627
        self.mount_points = mount_points if mount_points else list()
Packit Service 3a6627
        # Popen object of the qemu process
Packit Service 3a6627
        self.vm = None
Packit Service 3a6627
        self.vm_ready = False
Packit Service 3a6627
        # following values are set after the VM is terminated
Packit Service 3a6627
        self.vm_return_code = None
Packit Service 3a6627
        self.vm_stdout = None
Packit Service 3a6627
        self.vm_stderr = None
Packit Service 3a6627
        # credentials used to SSH to the VM
Packit Service 3a6627
        self.vm_user = user
Packit Service 3a6627
        self.vm_pass = passwd
Packit Service 3a6627
        # port on host to forward the guest's SSH port (22) to
Packit Service 3a6627
        self.host_fwd_ssh_port = None
Packit Service 3a6627
Packit Service 3a6627
    def _check_qemu_bin(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Checks whether QEMU binary used for the particular runner is present
Packit Service 3a6627
        on the system.
Packit Service 3a6627
        """
Packit Service 3a6627
        try:
Packit Service 3a6627
            subprocess.check_call([self.QEMU_BIN, "--version"])
Packit Service 3a6627
        except subprocess.CalledProcessError as _:
Packit Service 3a6627
            raise RuntimeError("QEMU binary {} not found".format(self.QEMU_BIN))
Packit Service 3a6627
Packit Service 3a6627
    def _get_qemu_cdrom_option(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Get the appropriate options for attaching CDROM device to the VM, if
Packit Service 3a6627
        the path to ISO has been provided.
Packit Service 3a6627
Packit Service 3a6627
        This method may be reimplemented by architecture specific runner class
Packit Service 3a6627
        if needed. Returns a list of strings to be appended to the QEMU command.
Packit Service 3a6627
        """
Packit Service 3a6627
        if self.cdrom_iso:
Packit Service 3a6627
            return ["-cdrom", self.cdrom_iso]
Packit Service 3a6627
Packit Service 3a6627
        return list()
Packit Service 3a6627
Packit Service 3a6627
    def _get_qemu_boot_image_option(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Get the appropriate options for specifying the image to boot from.
Packit Service 3a6627
Packit Service 3a6627
        This method may be reimplemented by architecture specific runner class
Packit Service 3a6627
        if needed.
Packit Service 3a6627
Packit Service 3a6627
        Returns a list of strings to be appended to the QEMU command.
Packit Service 3a6627
        """
Packit Service 3a6627
        return [self.image]
Packit Service 3a6627
Packit Service 3a6627
    def _get_qemu_ssh_fwd_option(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Get the appropriate options for forwarding guest's port 22 to host's
Packit Service 3a6627
        random available port.
Packit Service 3a6627
        """
Packit Service 3a6627
        # get a random free TCP port. This should work in majority of cases
Packit Service 3a6627
        with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
Packit Service 3a6627
            sock.bind(('localhost', 0))
Packit Service 3a6627
            self.host_fwd_ssh_port = sock.getsockname()[1]
Packit Service 3a6627
Packit Service 3a6627
            return ["-net", "user,hostfwd=tcp::{}-:22".format(self.host_fwd_ssh_port)]
Packit Service 3a6627
Packit Service 3a6627
    def _run_qemu_cmd(self, qemu_cmd):
Packit Service 3a6627
        """
Packit Service 3a6627
        Assembles the QEMU command to run and executes using subprocess.
Packit Service 3a6627
        """
Packit Service 3a6627
        # handle CDROM
Packit Service 3a6627
        qemu_cmd.extend(self._get_qemu_cdrom_option())
Packit Service 3a6627
Packit Service 3a6627
        # handle mount points
Packit Service 3a6627
        for mount_point in self.mount_points:
Packit Service 3a6627
            src_host = mount_point.src_host
Packit Service 3a6627
            tag = mount_point.mount_tag
Packit Service 3a6627
            readonly = ",readonly" if mount_point.readonly else ""
Packit Service 3a6627
            qemu_cmd.extend(["-virtfs", f"local,path={src_host},mount_tag={tag},security_model=mapped-xattr{readonly}"])
Packit Service 3a6627
Packit Service 3a6627
        # handle boot image
Packit Service 3a6627
        qemu_cmd.extend(self._get_qemu_boot_image_option())
Packit Service 3a6627
Packit Service 3a6627
        # handle forwarding of guest's SSH port to host
Packit Service 3a6627
        qemu_cmd.extend(self._get_qemu_ssh_fwd_option())
Packit Service 3a6627
Packit Service 3a6627
        log.debug("Starting VM using command: '%s'", " ".join(qemu_cmd))
Packit Service 3a6627
        self.vm = subprocess.Popen(
Packit Service 3a6627
            qemu_cmd,
Packit Service 3a6627
            stdout=subprocess.PIPE,
Packit Service 3a6627
            stderr=subprocess.PIPE
Packit Service 3a6627
        )
Packit Service 3a6627
Packit Service 3a6627
    def start(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Starts the QEMU process running the VM
Packit Service 3a6627
        """
Packit Service 3a6627
        if not self.QEMU_CMD:
Packit Service 3a6627
            raise NotImplementedError("The way to spin up QEMU VM is not implemented")
Packit Service 3a6627
Packit Service 3a6627
        # don't start the qemu process if there is already one running
Packit Service 3a6627
        if self.vm is None:
Packit Service 3a6627
            self._run_qemu_cmd(list(self.QEMU_CMD))
Packit Service 3a6627
        log.info(
Packit Service 3a6627
            "Runner started. You can SSH to it once it has been configured:" +\
Packit Service 3a6627
            "'ssh %s@localhost -p %d' using password: '%s'",
Packit Service 3a6627
            self.vm_user,
Packit Service 3a6627
            self.host_fwd_ssh_port,
Packit Service 3a6627
            self.vm_pass
Packit Service 3a6627
        )
Packit Service 3a6627
Packit Service 3a6627
    def stop(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Stops the QEMU process running the VM
Packit Service 3a6627
        """
Packit Service 3a6627
        if self.vm:
Packit Service 3a6627
            self.vm.terminate()
Packit Service 3a6627
            try:
Packit Service 3a6627
                # give the process some time to terminate
Packit Service 3a6627
                self.vm.wait(timeout=15)
Packit Service 3a6627
            except subprocess.TimeoutExpired as _:
Packit Service 3a6627
                self.vm.kill()
Packit Service 3a6627
                self.vm.wait(timeout=15)
Packit Service 3a6627
Packit Service 3a6627
            if self.vm.stdout:
Packit Service 3a6627
                self.vm_stdout = self.vm.stdout.read().decode()
Packit Service 3a6627
            if self.vm.stderr:
Packit Service 3a6627
                self.vm_stderr = self.vm.stderr.read().decode()
Packit Service 3a6627
            self.vm_return_code = self.vm.returncode
Packit Service 3a6627
Packit Service 3a6627
            if self.vm_return_code == 0:
Packit Service 3a6627
                log.debug("%s process ended with return code %d\n\n" + \
Packit Service 3a6627
                          "stdout:\n%s\nstderr:\n%s", self.QEMU_BIN,
Packit Service 3a6627
                          self.vm_return_code, self.vm_stdout, self.vm_stderr)
Packit Service 3a6627
            else:
Packit Service 3a6627
                log.error("%s process ended with return code %d\n\n" + \
Packit Service 3a6627
                          "stdout:\n%s\nstderr:\n%s", self.QEMU_BIN,
Packit Service 3a6627
                          self.vm_return_code, self.vm_stdout, self.vm_stderr)
Packit Service 3a6627
Packit Service 3a6627
            self.vm = None
Packit Service 3a6627
            self.vm_ready = False
Packit Service 3a6627
Packit Service 3a6627
    def run_command(self, command):
Packit Service 3a6627
        """
Packit Service 3a6627
        Runs a given command on the VM over ssh in a blocking fashion.
Packit Service 3a6627
Packit Service 3a6627
        Calling this method before is_ready() returned True has undefined
Packit Service 3a6627
        behavior.
Packit Service 3a6627
Packit Service 3a6627
        Returns stdin, stdout, stderr from the run command.
Packit Service 3a6627
        """
Packit Service 3a6627
        ssh = paramiko.SSHClient()
Packit Service 3a6627
        # don't ask / fail on unknown remote host fingerprint, just accept any
Packit Service 3a6627
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
Packit Service 3a6627
        try:
Packit Service 3a6627
            ssh.connect("localhost", self.host_fwd_ssh_port, self.vm_user, self.vm_pass)
Packit Service 3a6627
            ssh_tansport = ssh.get_transport()
Packit Service 3a6627
            channel = ssh_tansport.open_session()
Packit Service 3a6627
            # don't log commands when the vm is not yet ready for use
Packit Service 3a6627
            if self.vm_ready:
Packit Service 3a6627
                log.debug("Running on VM: '%s'", command)
Packit Service 3a6627
            channel.exec_command(command)
Packit Service 3a6627
            stdout = ""
Packit Service 3a6627
            stderr = ""
Packit Service 3a6627
            # wait for the command to finish
Packit Service 3a6627
            while True:
Packit Service 3a6627
                while channel.recv_ready():
Packit Service 3a6627
                    stdout += channel.recv(1024).decode()
Packit Service 3a6627
                while channel.recv_stderr_ready():
Packit Service 3a6627
                    stderr += channel.recv_stderr(1024).decode()
Packit Service 3a6627
                if channel.exit_status_ready():
Packit Service 3a6627
                    break
Packit Service 3a6627
                time.sleep(0.01)
Packit Service 3a6627
            returncode = channel.recv_exit_status()
Packit Service 3a6627
        except Exception as e:
Packit Service 3a6627
            # don't log errors when vm is not ready yet, because there are many errors
Packit Service 3a6627
            if self.vm_ready:
Packit Service 3a6627
                log.error("Running command over ssh failed: %s", str(e))
Packit Service 3a6627
            raise e
Packit Service 3a6627
        finally:
Packit Service 3a6627
            # closes the underlying transport
Packit Service 3a6627
            ssh.close()
Packit Service 3a6627
Packit Service 3a6627
        return stdout, stderr, returncode
Packit Service 3a6627
Packit Service 3a6627
    def run_command_check_call(self, command):
Packit Service 3a6627
        """
Packit Service 3a6627
        Runs a command on the VM over ssh in a similar fashion as subprocess.check_call()
Packit Service 3a6627
        """
Packit Service 3a6627
        _, _, ret = self.run_command(command)
Packit Service 3a6627
        if ret != 0:
Packit Service 3a6627
            raise subprocess.CalledProcessError(ret, command)
Packit Service 3a6627
Packit Service 3a6627
    def wait_until_ready(self, timeout=None):
Packit Service 3a6627
        """
Packit Service 3a6627
        Waits for the VM to be ready for use (cloud-init configuration finished).
Packit Service 3a6627
Packit Service 3a6627
        If timeout is provided
Packit Service 3a6627
        """
Packit Service 3a6627
        now = time.time()
Packit Service 3a6627
        while not self.is_ready():
Packit Service 3a6627
            if timeout is not None and time.time() > (now + timeout):
Packit Service 3a6627
                raise subprocess.TimeoutExpired("wait_until_ready()", timeout)
Packit Service 3a6627
            time.sleep(15)
Packit Service 3a6627
Packit Service 3a6627
    def is_ready(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Returns True if the VM is ready to be used.
Packit Service 3a6627
        VM is ready after the cloud-init setup is finished.
Packit Service 3a6627
        """
Packit Service 3a6627
        if self.vm_ready:
Packit Service 3a6627
            return True
Packit Service 3a6627
Packit Service 3a6627
        # check if the runner didn't terminate unexpectedly before being ready
Packit Service 3a6627
        try:
Packit Service 3a6627
            if self.vm:
Packit Service 3a6627
                self.vm.wait(1)
Packit Service 3a6627
        except subprocess.TimeoutExpired as _:
Packit Service 3a6627
            # process still running
Packit Service 3a6627
            pass
Packit Service 3a6627
        else:
Packit Service 3a6627
            # process not running, call .stop() to log stdout, stderr and retcode
Packit Service 3a6627
            self.stop()
Packit Service 3a6627
            qemu_bin = self.QEMU_BIN
Packit Service 3a6627
            raise RuntimeError(f"'{qemu_bin}' process ended before being ready to use")
Packit Service 3a6627
Packit Service 3a6627
        try:
Packit Service 3a6627
            # cloud-init touches /var/lib/cloud/instance/boot-finished after it finishes
Packit Service 3a6627
            self.run_command_check_call("ls /var/lib/cloud/instance/boot-finished")
Packit Service 3a6627
        except (paramiko.ChannelException,
Packit Service 3a6627
                paramiko.ssh_exception.NoValidConnectionsError,
Packit Service 3a6627
                paramiko.ssh_exception.SSHException,
Packit Service 3a6627
                EOFError,
Packit Service 3a6627
                socket.timeout,
Packit Service 3a6627
                subprocess.CalledProcessError) as _:
Packit Service 3a6627
            # ignore all reasonable paramiko exceptions, this is useful when the VM is still stating up
Packit Service 3a6627
            pass
Packit Service 3a6627
        else:
Packit Service 3a6627
            log.debug("VM is ready for use")
Packit Service 3a6627
            self.vm_ready = True
Packit Service 3a6627
Packit Service 3a6627
        return self.vm_ready
Packit Service 3a6627
Packit Service 3a6627
    def mount_mount_points(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        This method mounts the needed mount points on the VM.
Packit Service 3a6627
Packit Service 3a6627
        It should be called only after is_vm_ready() returned True. Otherwise it will fail.
Packit Service 3a6627
        """
Packit Service 3a6627
        for mount_point in self.mount_points:
Packit Service 3a6627
            dst_guest = mount_point.dst_guest
Packit Service 3a6627
            mount_tag = mount_point.mount_tag
Packit Service 3a6627
            self.run_command_check_call(f"sudo mkdir {dst_guest}")
Packit Service 3a6627
            #! FIXME: "9p" filesystem is not supported on RHEL!
Packit Service 3a6627
            out, err, ret = self.run_command(f"sudo mount -t 9p -o trans=virtio {mount_tag} {dst_guest} -oversion=9p2000.L")
Packit Service 3a6627
            if ret != 0:
Packit Service 3a6627
                log.error("Mounting '%s' to '%s' failed with retcode: %d\nstdout: %s\nstderr: %s", mount_tag, dst_guest,
Packit Service 3a6627
                ret, out, err)
Packit Service 3a6627
                raise subprocess.CalledProcessError(
Packit Service 3a6627
                    ret,
Packit Service 3a6627
                    f"sudo mount -t 9p -o trans=virtio {mount_tag} {dst_guest} -oversion=9p2000.L")
Packit Service 3a6627
Packit Service 3a6627
    def __enter__(self):
Packit Service 3a6627
        self.start()
Packit Service 3a6627
        return self
Packit Service 3a6627
Packit Service 3a6627
    def __exit__(self, *exc_details):
Packit Service 3a6627
        self.stop()
Packit Service 3a6627
Packit Service 3a6627
    @staticmethod
Packit Service 3a6627
    def prepare_cloud_init_cdrom(userdata, workdir):
Packit Service 3a6627
        """
Packit Service 3a6627
        Generates a CDROM ISO used as a data source for cloud-init.
Packit Service 3a6627
Packit Service 3a6627
        Returns path to the generated CDROM ISO image.
Packit Service 3a6627
        """
Packit Service 3a6627
        iso_path = os.path.join(workdir, "cloudinit.iso")
Packit Service 3a6627
        cidatadir = os.path.join(workdir, "cidata")
Packit Service 3a6627
        user_data_path = os.path.join(cidatadir, "user-data")
Packit Service 3a6627
        meta_data_path = os.path.join(cidatadir, "meta-data")
Packit Service 3a6627
Packit Service 3a6627
        os.mkdir(cidatadir)
Packit Service 3a6627
        if os.path.isdir(userdata):
Packit Service 3a6627
            with open(user_data_path, "w") as f:
Packit Service 3a6627
                script_dir = os.path.dirname(__file__)
Packit Service 3a6627
                subprocess.check_call(
Packit Service 3a6627
                    [os.path.abspath(f"{script_dir}/../gen-user-data"), userdata], stdout=f)
Packit Service 3a6627
        else:
Packit Service 3a6627
            shutil.copy(userdata, user_data_path)
Packit Service 3a6627
Packit Service 3a6627
        with open(meta_data_path, "w") as f:
Packit Service 3a6627
            f.write("instance-id: nocloud\nlocal-hostname: vm\n")
Packit Service 3a6627
Packit Service 3a6627
        sysname = os.uname().sysname
Packit Service 3a6627
        log.debug("Generating CDROM ISO image for cloud-init user data: %s", iso_path)
Packit Service 3a6627
        if sysname == "Linux":
Packit Service 3a6627
            subprocess.check_call(
Packit Service 3a6627
                [
Packit Service 3a6627
                    "genisoimage",
Packit Service 3a6627
                    "-input-charset", "utf-8",
Packit Service 3a6627
                    "-output", iso_path,
Packit Service 3a6627
                    "-volid", "cidata",
Packit Service 3a6627
                    "-joliet",
Packit Service 3a6627
                    "-rock",
Packit Service 3a6627
                    "-quiet",
Packit Service 3a6627
                    "-graft-points",
Packit Service 3a6627
                    user_data_path,
Packit Service 3a6627
                    meta_data_path
Packit Service 3a6627
                ],
Packit Service 3a6627
                stdout=subprocess.DEVNULL,
Packit Service 3a6627
                stderr=subprocess.DEVNULL
Packit Service 3a6627
            )
Packit Service 3a6627
        elif sysname == "Darwin":
Packit Service 3a6627
            subprocess.check_call(
Packit Service 3a6627
                [
Packit Service 3a6627
                    "hdiutil",
Packit Service 3a6627
                    "makehybrid",
Packit Service 3a6627
                    "-iso",
Packit Service 3a6627
                    "-joliet",
Packit Service 3a6627
                    "-o", iso_path,
Packit Service 3a6627
                    "{cidatadir}"
Packit Service 3a6627
                ],
Packit Service 3a6627
                stdout=subprocess.DEVNULL,
Packit Service 3a6627
                stderr=subprocess.DEVNULL
Packit Service 3a6627
            )
Packit Service 3a6627
        else:
Packit Service 3a6627
            raise NotImplementedError(f"Unsupported system '{sysname}' for generating cdrom iso")
Packit Service 3a6627
Packit Service 3a6627
        return iso_path
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
class X86_64Runner(BaseRunner):
Packit Service 3a6627
    """
Packit Service 3a6627
    VM Runner for x86_64 architecture
Packit Service 3a6627
    """
Packit Service 3a6627
Packit Service 3a6627
    QEMU_BIN = "qemu-system-x86_64"
Packit Service 3a6627
    QEMU_CMD = [
Packit Service 3a6627
        QEMU_BIN,
Packit Service 3a6627
        "-M", "accel=kvm:hvf",
Packit Service 3a6627
        "-m", "1024",
Packit Service 3a6627
        "-object", "rng-random,filename=/dev/urandom,id=rng0",
Packit Service 3a6627
        "-device", "virtio-rng-pci,rng=rng0",
Packit Service 3a6627
        "-snapshot",
Packit Service 3a6627
        "-cpu", "max",
Packit Service 3a6627
        "-net", "nic,model=virtio",
Packit Service 3a6627
    ]
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
class Ppc64Runner(BaseRunner):
Packit Service 3a6627
    """
Packit Service 3a6627
    VM Runner for ppc64le architecture
Packit Service 3a6627
    """
Packit Service 3a6627
Packit Service 3a6627
    QEMU_BIN = "qemu-system-ppc64"
Packit Service 3a6627
    QEMU_CMD = [
Packit Service 3a6627
        QEMU_BIN,
Packit Service 3a6627
        "-m", "2048", # RAM
Packit Service 3a6627
        "-smp", "2", # CPUs
Packit Service 3a6627
        "-object", "rng-random,filename=/dev/urandom,id=rng0",
Packit Service 3a6627
        "-device", "virtio-rng-pci,rng=rng0",
Packit Service 3a6627
        "-snapshot",
Packit Service 3a6627
        "-net", "nic,model=virtio",
Packit Service 3a6627
    ]
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
class Aarch64Runner(BaseRunner):
Packit Service 3a6627
    """
Packit Service 3a6627
    VM Runner for aarch64 architecture
Packit Service 3a6627
    """
Packit Service 3a6627
Packit Service 3a6627
    # aarch64 requires UEFI build for QEMU
Packit Service 3a6627
    # https://rwmj.wordpress.com/2015/02/27/how-to-boot-a-fedora-21-aarch64-uefi-guest-on-x86_64/
Packit Service 3a6627
    # https://fedoraproject.org/wiki/Architectures/AArch64/Install_with_QEMU
Packit Service 3a6627
    QEMU_BIN = "qemu-system-aarch64"
Packit Service 3a6627
    QEMU_CMD = [
Packit Service 3a6627
        QEMU_BIN,
Packit Service 3a6627
        "-m", "2048", # RAM
Packit Service 3a6627
        "-smp", "2", # CPUs
Packit Service 3a6627
        "-object", "rng-random,filename=/dev/urandom,id=rng0",
Packit Service 3a6627
        "-device", "virtio-rng-pci,rng=rng0",
Packit Service 3a6627
        "-snapshot",
Packit Service 3a6627
        "-monitor", "none",
Packit Service 3a6627
        "-machine", "virt",
Packit Service 3a6627
        "-cpu", "cortex-a57",
Packit Service 3a6627
        "-bios", "/usr/share/edk2/aarch64/QEMU_EFI.fd", # provided by 'edk2-aarch64' Fedora package
Packit Service 3a6627
        "-net", "nic,model=virtio",
Packit Service 3a6627
    ]
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
class S390xRunner(BaseRunner):
Packit Service 3a6627
    """
Packit Service 3a6627
    VM Runner for s390x architecture
Packit Service 3a6627
    """
Packit Service 3a6627
Packit Service 3a6627
    QEMU_BIN = "qemu-system-s390x"
Packit Service 3a6627
    QEMU_CMD = [
Packit Service 3a6627
        QEMU_BIN,
Packit Service 3a6627
        "-m", "2048", # RAM
Packit Service 3a6627
        "-smp", "2", # CPUs
Packit Service 3a6627
        "-machine", "s390-ccw-virtio",
Packit Service 3a6627
        # disable msa5-base to suppress errors:
Packit Service 3a6627
        # qemu-system-s390x: warning: 'msa5-base' requires 'kimd-sha-512'
Packit Service 3a6627
        # qemu-system-s390x: warning: 'msa5-base' requires 'klmd-sha-512'
Packit Service 3a6627
        "-cpu", "max,msa5-base=no",
Packit Service 3a6627
        "-object", "rng-random,filename=/dev/urandom,id=rng0",
Packit Service 3a6627
        "-device", "virtio-rng-ccw,rng=rng0",
Packit Service 3a6627
        "-monitor", "none",
Packit Service 3a6627
        "-snapshot",
Packit Service 3a6627
        "-net", "nic,model=virtio",
Packit Service 3a6627
    ]
Packit Service 3a6627
Packit Service 3a6627
    def _get_qemu_cdrom_option(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Get the appropriate options for attaching CDROM device to the VM, if the path to ISO has been provided.
Packit Service 3a6627
Packit Service 3a6627
        s390x tries to boot from the CDROM if attached the way as BaseRunner does it.
Packit Service 3a6627
        """
Packit Service 3a6627
        if self.cdrom_iso:
Packit Service 3a6627
            iso_path = self.cdrom_iso
Packit Service 3a6627
            return list(["-drive", f"file={iso_path},media=cdrom"])
Packit Service 3a6627
        else:
Packit Service 3a6627
            return list()
Packit Service 3a6627
Packit Service 3a6627
    def _get_qemu_boot_image_option(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Get the appropriate options for specifying the image to boot from.
Packit Service 3a6627
Packit Service 3a6627
        s390x needs to have an explicit 'bootindex' specified.
Packit Service 3a6627
        https://qemu.readthedocs.io/en/latest/system/s390x/bootdevices.html
Packit Service 3a6627
        """
Packit Service 3a6627
        image_path = self.image
Packit Service 3a6627
        return [
Packit Service 3a6627
            "-drive", f"if=none,id=dr1,file={image_path}",
Packit Service 3a6627
            "-device", "virtio-blk,drive=dr1,bootindex=1"
Packit Service 3a6627
        ]
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
class TestCaseMatrixGenerator(contextlib.AbstractContextManager):
Packit Service 3a6627
    """
Packit Service 3a6627
    Class representing generation of all test cases based on provided test
Packit Service 3a6627
    cases matrix.
Packit Service 3a6627
Packit Service 3a6627
    The class should be used as a context manager to ensure that cleanup
Packit Service 3a6627
    of all resources is done (mainly VMs and processes running them).
Packit Service 3a6627
Packit Service 3a6627
    VM for each architecture is run in a separate process to ensure that
Packit Service 3a6627
    generation is done in parallel.
Packit Service 3a6627
    """
Packit Service 3a6627
Packit Service 3a6627
    ARCH_RUNNER_MAP = {
Packit Service 3a6627
        "x86_64": X86_64Runner,
Packit Service 3a6627
        "aarch64": Aarch64Runner,
Packit Service 3a6627
        "ppc64le": Ppc64Runner,
Packit Service 3a6627
        "s390x": S390xRunner
Packit Service 3a6627
    }
Packit Service 3a6627
Packit Service 3a6627
    def __init__(self, images, ci_userdata, arch_gen_matrix, output, keep_image_info):
Packit Service 3a6627
        """
Packit Service 3a6627
        'images' is a dict of qcow2 image paths for each supported architecture,
Packit Service 3a6627
        that should be used for VMs:
Packit Service 3a6627
        {
Packit Service 3a6627
            "arch1": "<image path>",
Packit Service 3a6627
            "arch2": "<image path>",
Packit Service 3a6627
            ...
Packit Service 3a6627
        }
Packit Service 3a6627
        'ci_userdata' is path to file / directory containing cloud-init user-data used
Packit Service 3a6627
        for generating CDROM ISO image, that is attached to each VM as a cloud-init data source.
Packit Service 3a6627
        'arch_get_matrix' is a dict of requested distro-image_type matrix per architecture:
Packit Service 3a6627
        {
Packit Service 3a6627
            "arch1": {
Packit Service 3a6627
                "distro1": [
Packit Service 3a6627
                    "image-type1",
Packit Service 3a6627
                    "image-type2"
Packit Service 3a6627
                ],
Packit Service 3a6627
                "distro2": [
Packit Service 3a6627
                    "image-type2",
Packit Service 3a6627
                    "image-type3"
Packit Service 3a6627
                ]
Packit Service 3a6627
            },
Packit Service 3a6627
            "arch2": {
Packit Service 3a6627
                "distro2": [
Packit Service 3a6627
                    "image-type2"
Packit Service 3a6627
                ]
Packit Service 3a6627
            },
Packit Service 3a6627
            ...
Packit Service 3a6627
        }
Packit Service 3a6627
        'output' is a directory path, where the generated test case manifests should be stored.
Packit Service 3a6627
        'keep_image_info' specifies whether to pass the '--keep-image-info' option to the 'generate-test-cases' script.
Packit Service 3a6627
        """
Packit Service 3a6627
        self._processes = list()
Packit Service 3a6627
        self.images = images
Packit Service 3a6627
        self.ci_userdata = ci_userdata
Packit Service 3a6627
        self.arch_gen_matrix = arch_gen_matrix
Packit Service 3a6627
        self.output = output
Packit Service 3a6627
        self.keep_image_info = keep_image_info
Packit Service 3a6627
Packit Service 3a6627
        # check that we have image for each needed architecture
Packit Service 3a6627
        for arch in self.arch_gen_matrix.keys():
Packit Service 3a6627
            if self.images.get(arch, None) is None:
Packit Service 3a6627
                raise RuntimeError(f"architecture '{arch}' is in requested test matrix, but no image was provided")
Packit Service 3a6627
Packit Service 3a6627
    @staticmethod
Packit Service 3a6627
    def runner_function(arch, runner_cls, image, user, passwd, cdrom_iso, generation_matrix, output, keep_image_info):
Packit Service 3a6627
        """
Packit Service 3a6627
        Generate test cases using VM with appropriate architecture.
Packit Service 3a6627
Packit Service 3a6627
        'generation_matrix' is expected to be already architecture-specific
Packit Service 3a6627
        dict of 'distro' x 'image-type' matrix.
Packit Service 3a6627
Packit Service 3a6627
        {
Packit Service 3a6627
            "fedora-32": [
Packit Service 3a6627
                "qcow2",
Packit Service 3a6627
                "vmdk"
Packit Service 3a6627
            ],
Packit Service 3a6627
            "rhel-84": [
Packit Service 3a6627
                "qcow2",
Packit Service 3a6627
                "tar"
Packit Service 3a6627
            ],
Packit Service 3a6627
            ...
Packit Service 3a6627
        }
Packit Service 3a6627
        """
Packit Service 3a6627
Packit Service 3a6627
        mount_points = RunnerMountPoint.get_default_runner_mount_points(output)
Packit Service 3a6627
        go_tls_timeout_retries = 3
Packit Service 3a6627
Packit Service 3a6627
        # spin up appropriate VM represented by 'runner'
Packit Service 3a6627
        with runner_cls(image, user, passwd, cdrom_iso, mount_points=mount_points) as runner:
Packit Service 3a6627
            log.info("Waiting for the '%s' runner to be configured by cloud-init", arch)
Packit Service 3a6627
            runner.wait_until_ready()
Packit Service 3a6627
            runner.mount_mount_points()
Packit Service 3a6627
Packit Service 3a6627
            # don't use /var/tmp for osbuild's store directory to prevent systemd from possibly
Packit Service 3a6627
            # removing some of the downloaded RPMs due to "ageing"
Packit Service 3a6627
            guest_osbuild_store_dir = "/root/osbuild-store"
Packit Service 3a6627
            runner.run_command_check_call(f"sudo mkdir {guest_osbuild_store_dir}")
Packit Service 3a6627
Packit Service 3a6627
            # Log installed versions of important RPMs
Packit Service 3a6627
            rpm_versions, _, _ = runner.run_command("rpm -q osbuild osbuild-composer")
Packit Service 3a6627
            log.info("Installed packages: %s", " ".join(rpm_versions.split("\n")))
Packit Service 3a6627
Packit Service 3a6627
            for distro, img_type_list in generation_matrix.items():
Packit Service 3a6627
                for image_type in img_type_list:
Packit Service 3a6627
                    log.info("Generating test case for '%s' '%s' image on '%s'", distro, image_type, arch)
Packit Service 3a6627
Packit Service 3a6627
                    # is the image with customizations?
Packit Service 3a6627
                    if image_type.endswith("-customize"):
Packit Service 3a6627
                        with_customizations = True
Packit Service 3a6627
                        image_type = image_type.rstrip("-customize")
Packit Service 3a6627
                    else:
Packit Service 3a6627
                        with_customizations = False
Packit Service 3a6627
Packit Service 3a6627
                    gen_test_cases_cmd = "cd /mnt/sources; sudo tools/test-case-generators/generate-test-cases" + \
Packit Service 3a6627
                        f" --distro {distro} --arch {arch} --image-types {image_type}" + \
Packit Service 3a6627
                        f" --store {guest_osbuild_store_dir} --output /mnt/output/"
Packit Service 3a6627
                    if with_customizations:
Packit Service 3a6627
                        gen_test_cases_cmd += " --with-customizations"
Packit Service 3a6627
                    if keep_image_info:
Packit Service 3a6627
                        gen_test_cases_cmd += " --keep-image-info"
Packit Service 3a6627
Packit Service 3a6627
                    # allow fixed number of retries if the command fails for a specific reason
Packit Service 3a6627
                    for i in range(1, go_tls_timeout_retries+1):
Packit Service 3a6627
                        if i > 1:
Packit Service 3a6627
                            log.info("Retrying image test case generation (%d of %d)", i, go_tls_timeout_retries)
Packit Service 3a6627
Packit Service 3a6627
                        stdout, stderr, retcode = runner.run_command(gen_test_cases_cmd)
Packit Service 3a6627
                        # clean up the osbuild-store dir
Packit Service 3a6627
                        runner.run_command_check_call(f"sudo rm -rf {guest_osbuild_store_dir}/*")
Packit Service 3a6627
Packit Service 3a6627
                        if retcode != 0:
Packit Service 3a6627
                            log.error("'%s' retcode: %d\nstdout: %s\nstderr: %s", gen_test_cases_cmd, retcode,
Packit Service 3a6627
                                      stdout, stderr)
Packit Service 3a6627
Packit Service 3a6627
                            # Retry the command, if there was an error due to TLS handshake timeout
Packit Service 3a6627
                            # This is happening on all runners using other than host's arch from time to time.
Packit Service 3a6627
                            if stderr.find("net/http: TLS handshake timeout") != -1:
Packit Service 3a6627
                                continue
Packit Service 3a6627
                        else:
Packit Service 3a6627
                            log.info("Generating test case for %s-%s-%s - SUCCEEDED", distro, arch, image_type)
Packit Service 3a6627
Packit Service 3a6627
                        # don't retry if the process ended successfully or if there was a different error
Packit Service 3a6627
                        break
Packit Service 3a6627
Packit Service 3a6627
            log.info("'%s' runner finished its work", arch)
Packit Service 3a6627
Packit Service 3a6627
            # TODO: Possibly remove after testing / fine tuning the script
Packit Service 3a6627
            log.info("Waiting for 1 hour, before terminating the runner (CTRL + c will terminate all VMs)")
Packit Service 3a6627
            time.sleep(3600)
Packit Service 3a6627
            runner.stop()
Packit Service 3a6627
Packit Service 3a6627
    def generate(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Generates all test cases based on provided data
Packit Service 3a6627
        """
Packit Service 3a6627
        # use the same CDROM ISO image for all VMs
Packit Service 3a6627
        with tempfile.TemporaryDirectory(prefix="osbuild-composer-test-gen-") as tmpdir:
Packit Service 3a6627
            cdrom_iso = BaseRunner.prepare_cloud_init_cdrom(self.ci_userdata, tmpdir)
Packit Service 3a6627
Packit Service 3a6627
            # Load user / password from the cloud-init user-data
Packit Service 3a6627
            if os.path.isdir(self.ci_userdata):
Packit Service 3a6627
                user_data_path = os.path.join(self.ci_userdata, "user-data.yml")
Packit Service 3a6627
            else:
Packit Service 3a6627
                user_data_path = self.ci_userdata
Packit Service 3a6627
            with open(user_data_path, "r") as ud:
Packit Service 3a6627
                user_data = yaml.safe_load(ud)
Packit Service 3a6627
                vm_user = user_data["user"]
Packit Service 3a6627
                vm_pass = user_data["password"]
Packit Service 3a6627
Packit Service 3a6627
            # Start a separate runner VM for each required architecture
Packit Service 3a6627
            for arch, generation_matrix in self.arch_gen_matrix.items():
Packit Service 3a6627
                process = multiprocessing.Process(
Packit Service 3a6627
                    target=self.runner_function,
Packit Service 3a6627
                    args=(arch, self.ARCH_RUNNER_MAP[arch], self.images[arch], vm_user, vm_pass, cdrom_iso,
Packit Service 3a6627
                          generation_matrix, self.output, self.keep_image_info))
Packit Service 3a6627
                self._processes.append(process)
Packit Service 3a6627
                process.start()
Packit Service 3a6627
                log.info("Started '%s' runner - %s", arch, process.name)
Packit Service 3a6627
Packit Service 3a6627
            # wait for all processes to finish
Packit Service 3a6627
            log.info("Waiting for all runner processes to finish")
Packit Service 3a6627
            for process in self._processes:
Packit Service 3a6627
                process.join()
Packit Service 3a6627
            self._processes.clear()
Packit Service 3a6627
Packit Service 3a6627
    def cleanup(self):
Packit Service 3a6627
        """
Packit Service 3a6627
        Terminates all running processes of VM runners.
Packit Service 3a6627
        """
Packit Service 3a6627
        # ensure that all processes running VMs are stopped
Packit Service 3a6627
        for process in self._processes:
Packit Service 3a6627
            process.terminate()
Packit Service 3a6627
            process.join(5)
Packit Service 3a6627
            # kill the process if it didn't terminate yet
Packit Service 3a6627
            if process.exitcode is None:
Packit Service 3a6627
                process.kill()
Packit Service 3a6627
            process.close()
Packit Service 3a6627
        self._processes.clear()
Packit Service 3a6627
Packit Service 3a6627
    def __exit__(self, *exc_details):
Packit Service 3a6627
        self.cleanup()
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
def get_args():
Packit Service 3a6627
    """
Packit Service 3a6627
    Returns ArgumentParser instance specific to this script.
Packit Service 3a6627
    """
Packit Service 3a6627
    parser = argparse.ArgumentParser(description="(re)generate image all test cases")
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--image-x86_64",
Packit Service 3a6627
        help="Path to x86_64 image to use for QEMU VM",
Packit Service 3a6627
        required=False
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--image-ppc64le",
Packit Service 3a6627
        help="Path to ppc64le image to use for QEMU VM",
Packit Service 3a6627
        required=False
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--image-aarch64",
Packit Service 3a6627
        help="Path to aarch64 image to use for QEMU VM",
Packit Service 3a6627
        required=False
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--image-s390x",
Packit Service 3a6627
        help="Path to s390x image to use for QEMU VM",
Packit Service 3a6627
        required=False
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--distro",
Packit Service 3a6627
        help="Filters the matrix for generation only to specified distro",
Packit Service 3a6627
        nargs='*',
Packit Service 3a6627
        required=False
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--arch",
Packit Service 3a6627
        help="Filters the matrix for generation only to specified architecture",
Packit Service 3a6627
        nargs='*',
Packit Service 3a6627
        required=False
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--image-types",
Packit Service 3a6627
        help="Filters the matrix for generation only to specified image types",
Packit Service 3a6627
        nargs='*',
Packit Service 3a6627
        required=False
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--keep-image-info",
Packit Service 3a6627
        action='store_true',
Packit Service 3a6627
        help="Skip image info (re)generation, but keep the one found in the existing test case"
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--output",
Packit Service 3a6627
        metavar="OUTPUT_DIRECTORY",
Packit Service 3a6627
        type=os.path.abspath,
Packit Service 3a6627
        help="Path to the output directory, where to store resulting manifests for image test cases",
Packit Service 3a6627
        required=True
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--gen-matrix-file",
Packit Service 3a6627
        help="Path to JSON file from which to read the test case generation matrix (distro x arch x image type)." + \
Packit Service 3a6627
            " If not provided, '<script_location_dir>/distro-arch-imagetype-map.json' is read.",
Packit Service 3a6627
        type=os.path.abspath
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "--ci-userdata",
Packit Service 3a6627
        help="Path to a file/directory with cloud-init user-data, which should be used to configure runner VMs",
Packit Service 3a6627
        type=os.path.abspath
Packit Service 3a6627
    )
Packit Service 3a6627
    parser.add_argument(
Packit Service 3a6627
        "-d", "--debug",
Packit Service 3a6627
        action='store_true',
Packit Service 3a6627
        default=False,
Packit Service 3a6627
        help="turn on debug logging"
Packit Service 3a6627
    )
Packit Service 3a6627
    return parser.parse_args()
Packit Service 3a6627
Packit Service 3a6627
# pylint: disable=too-many-arguments,too-many-locals
Packit Service 3a6627
def main(vm_images, distros, arches, image_types, ci_userdata, gen_matrix_file, output, keep_image_info):
Packit Service 3a6627
    if not os.path.isdir(output):
Packit Service 3a6627
        raise RuntimeError(f"output directory {output} does not exist")
Packit Service 3a6627
Packit Service 3a6627
    script_dir = os.path.dirname(__file__)
Packit Service 3a6627
    gen_matrix_path = gen_matrix_file if gen_matrix_file else f"{script_dir}/distro-arch-imagetype-map.json"
Packit Service 3a6627
    log.info("Loading generation matrix from file: '%s'", gen_matrix_path)
Packit Service 3a6627
    with open(gen_matrix_path, "r") as gen_matrix_json:
Packit Service 3a6627
        gen_matrix_dict = json.load(gen_matrix_json)
Packit Service 3a6627
Packit Service 3a6627
    # Filter generation matrix based on passed arguments
Packit Service 3a6627
    for distro in list(gen_matrix_dict.keys()):
Packit Service 3a6627
        # filter the distros list
Packit Service 3a6627
        if distros and distro not in distros:
Packit Service 3a6627
            del gen_matrix_dict[distro]
Packit Service 3a6627
            continue
Packit Service 3a6627
        for arch in list(gen_matrix_dict[distro].keys()):
Packit Service 3a6627
            # filter the arches list of a distro
Packit Service 3a6627
            if arches and arch not in arches:
Packit Service 3a6627
                del gen_matrix_dict[distro][arch]
Packit Service 3a6627
                continue
Packit Service 3a6627
            # filter the image types of a distro and arch
Packit Service 3a6627
            if image_types:
Packit Service 3a6627
                gen_matrix_dict[distro][arch] = list(filter(lambda x: x in image_types, gen_matrix_dict[distro][arch]))
Packit Service 3a6627
                # delete the whole arch if there is no image type left after filtering
Packit Service 3a6627
                if len(gen_matrix_dict[distro][arch]) == 0:
Packit Service 3a6627
                    del gen_matrix_dict[distro][arch]
Packit Service 3a6627
Packit Service 3a6627
    log.debug("gen_matrix_dict:\n%s", json.dumps(gen_matrix_dict, indent=2, sort_keys=True))
Packit Service 3a6627
Packit Service 3a6627
    # Construct per-architecture matrix dictionary of distro x image type
Packit Service 3a6627
    arch_gen_matrix_dict = dict()
Packit Service 3a6627
    for distro, arches in gen_matrix_dict.items():
Packit Service 3a6627
        for arch, image_types in arches.items():
Packit Service 3a6627
            try:
Packit Service 3a6627
                arch_dict = arch_gen_matrix_dict[arch]
Packit Service 3a6627
            except KeyError as _:
Packit Service 3a6627
                arch_dict = arch_gen_matrix_dict[arch] = dict()
Packit Service 3a6627
            arch_dict[distro] = image_types.copy()
Packit Service 3a6627
Packit Service 3a6627
    log.debug("arch_gen_matrix_dict:\n%s", json.dumps(arch_gen_matrix_dict, indent=2, sort_keys=True))
Packit Service 3a6627
Packit Service 3a6627
    ci_userdata_path = ci_userdata if ci_userdata else os.path.abspath(f"{script_dir}/../deploy/gen-test-data")
Packit Service 3a6627
    log.debug("Using cloud-init user-data from '%s'", ci_userdata_path)
Packit Service 3a6627
Packit Service 3a6627
    with TestCaseMatrixGenerator(vm_images, ci_userdata_path, arch_gen_matrix_dict, output, keep_image_info) as generator:
Packit Service 3a6627
        generator.generate()
Packit Service 3a6627
Packit Service 3a6627
Packit Service 3a6627
if __name__ == '__main__':
Packit Service 3a6627
    args = get_args()
Packit Service 3a6627
Packit Service 3a6627
    if  args.debug:
Packit Service 3a6627
        log.setLevel(logging.DEBUG)
Packit Service 3a6627
Packit Service 3a6627
    vm_images = {
Packit Service 3a6627
        "x86_64": args.image_x86_64,
Packit Service 3a6627
        "aarch64": args.image_aarch64,
Packit Service 3a6627
        "ppc64le": args.image_ppc64le,
Packit Service 3a6627
        "s390x": args.image_s390x
Packit Service 3a6627
    }
Packit Service 3a6627
Packit Service 3a6627
    try:
Packit Service 3a6627
        main(
Packit Service 3a6627
            vm_images,
Packit Service 3a6627
            args.distro,
Packit Service 3a6627
            args.arch,
Packit Service 3a6627
            args.image_types,
Packit Service 3a6627
            args.ci_userdata,
Packit Service 3a6627
            args.gen_matrix_file,
Packit Service 3a6627
            args.output,
Packit Service 3a6627
            args.keep_image_info
Packit Service 3a6627
        )
Packit Service 3a6627
    except KeyboardInterrupt as _:
Packit Service 3a6627
        log.info("Interrupted by user")