|
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")
|