Skip to content

Commit

Permalink
test: add container installer test
Browse files Browse the repository at this point in the history
Add a new integration test that checks that the container installer
is working. The contaner installer just does an unattended install
of a disk. The test will run qemu with the installer.iso and an
empty disk. Once a reboot from the ISO is detected (via QMP) qemu
exists and boots the test disk and checks that it boots and the
test user can login.
  • Loading branch information
mvo5 committed Jan 17, 2024
1 parent 220a974 commit 00215c5
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 9 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
# make sure test deps are available for root
sudo -E pip install --user -r test/requirements.txt
# podman needs (parts of) the environment but will break when
# XDG_RUNTIME_DIR is set.
# TODO: figure out what exactly podman needs
Expand Down
1 change: 1 addition & 0 deletions test/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pytest==7.4.3
flake8==6.1.0
paramiko==2.12.0
boto3==1.33.13
qmp==1.1.0
21 changes: 20 additions & 1 deletion test/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ def image_type_fixture(tmpdir_factory, build_container, request, force_aws_uploa
"qcow2": pathlib.Path(output_path) / "qcow2/disk.qcow2",
"ami": pathlib.Path(output_path) / "image/disk.raw",
"raw": pathlib.Path(output_path) / "image/disk.raw",
"iso": pathlib.Path(output_path) / "bootiso/install.iso",
}
assert len(artifact) == len(SUPPORTED_IMAGE_TYPES), \
assert not set(SUPPORTED_IMAGE_TYPES).issuperset(set(artifact)), \
"please keep artifact mapping and supported images in sync"
generated_img = artifact[image_type]

Expand Down Expand Up @@ -237,3 +238,21 @@ def test_image_build_without_se_linux_denials(image_type):
assert image_type.journal_output != ""
assert not log_has_osbuild_selinux_denials(image_type.journal_output), \
f"denials in log {image_type.journal_output}"


@pytest.mark.skipif(platform.system() != "Linux", reason="boot test only runs on linux right now")
@pytest.mark.parametrize("image_type", ["iso"], indirect=["image_type"])
def test_iso_installs(image_type):
installer_iso_path = image_type.img_path
test_disk_path = installer_iso_path.with_name("test-disk.img")
with open(test_disk_path, "w") as fp:
fp.truncate(10_1000_1000_1000)
# install to test disk
with QEMU(test_disk_path, cdrom=installer_iso_path) as vm:
vm.start(wait_event="qmp:RESET", snapshot=False, use_ovmf=True)
vm.force_stop()
# boot test disk and do extremly simple check
with QEMU(test_disk_path) as vm:
vm.start(use_ovmf=True)
exit_status, _ = vm.run("true", user=image_type.username, password=image_type.password)
assert exit_status == 0
73 changes: 65 additions & 8 deletions test/vm.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import abc
import os
import pathlib
import subprocess
import sys
import time
import uuid
from io import StringIO

import boto3
from botocore.exceptions import ClientError

from paramiko.client import AutoAddPolicy, SSHClient

from testutil import AWS_REGION, get_free_port, wait_ssh_ready
Expand Down Expand Up @@ -79,20 +82,37 @@ def __exit__(self, exc_type, exc_value, traceback):
self.force_stop()


class QEMU(VM):
# needed as each distro puts the OVMF.fd in a different location
def find_ovmf():
for p in [
"/usr/share/ovmf/OVMF.fd", # Debian
"/usr/share/OVMF/OVMF_CODE.fd", # Fedora
]:
if os.path.exists(p):
return p
raise ValueError("cannot find a OVMF bios")


class QEMU(VM):
MEM = "2000"
# TODO: support qemu-system-aarch64 too :)
QEMU = "qemu-system-x86_64"

def __init__(self, img, snapshot=True):
def __init__(self, img, snapshot=True, cdrom=None):
super().__init__()
self._img = pathlib.Path(img)
self._qmp_socket = self._img.with_suffix(".qemp-socket")
self._qemu_p = None
self._snapshot = snapshot
self._cdrom = cdrom
self._ssh_port = None

def start(self):
if self.running():
def __del__(self):
self.force_stop()

# XXX: move args to init() so that __enter__ can use them?
def start(self, wait_event="ssh", snapshot=True, use_ovmf=False):
if self._qemu_p is not None:
return
log_path = self._img.with_suffix(".serial-log")
self._ssh_port = get_free_port()
Expand All @@ -107,18 +127,52 @@ def start(self):
"-monitor", "none",
"-netdev", f"user,id=net.0,hostfwd=tcp::{self._ssh_port}-:22",
"-device", "rtl8139,netdev=net.0",
"-qmp", f"unix:{self._qmp_socket},server,nowait",
]
if self._snapshot:
if use_ovmf:
qemu_cmdline.extend(["-bios", find_ovmf()])
if self._cdrom:
qemu_cmdline.extend(["-cdrom", self._cdrom])
if snapshot:
qemu_cmdline.append("-snapshot")
qemu_cmdline.append(self._img)
self._log(f"vm starting, log available at {log_path}")

# XXX: use systemd-run to ensure cleanup?
self._qemu_p = subprocess.Popen(
qemu_cmdline, stdout=sys.stdout, stderr=sys.stderr)
qemu_cmdline,
stdout=sys.stdout,
stderr=sys.stderr,
)
# XXX: also check that qemu is working and did not crash
self.wait_ssh_ready()
self._log(f"vm ready at port {self._ssh_port}")
match wait_event.split(":"):
case ["ssh"]:
self.wait_ssh_ready()
self._log(f"vm ready at port {self._ssh_port}")
case ["qmp", qmp_event]:
self.wait_qmp_event(qmp_event)
self._log(f"qmp event {qmp_event}")
case _:
raise ValueError(f"unsupported wait_event {wait_event}")

def _wait_qmp_socket(self, timeout_sec):
for _ in range(timeout_sec):
if os.path.exists(self._qmp_socket):
return True
time.sleep(1)
raise Exception(f"no {self._qmp_socket} after {timeout_sec} seconds")

def wait_qmp_event(self, qmp_event):
# import lazy to avoid requiring it for all operations
import qmp
self._wait_qmp_socket(30)
mon = qmp.QEMUMonitorProtocol(os.fspath(self._qmp_socket))
mon.connect()
while True:
event = mon.pull_event(wait=True)
self._log(f"DEBUG: got event {event}")
if event["event"] == qmp_event:
return

def force_stop(self):
if self._qemu_p:
Expand All @@ -130,6 +184,9 @@ def force_stop(self):
def running(self):
return self._qemu_p is not None

def __enter__(self):
return self


class AWS(VM):

Expand Down

0 comments on commit 00215c5

Please sign in to comment.