Skip to content

Commit

Permalink
test: boot generated VM and wait for ssh port
Browse files Browse the repository at this point in the history
  • Loading branch information
mvo5 committed Dec 7, 2023
1 parent 4bc0ea3 commit e8489e1
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
uses: actions/setup-python@v4
- name: Install test dependencies
run: |
sudo apt install -y podman python3-pytest flake8
sudo apt install -y podman python3-pytest flake8 qemu-system-x86
- name: Run tests
run: |
# podman needs (parts of) the environment but will break when
Expand Down
3 changes: 3 additions & 0 deletions plans/all.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ summary: Run all tests inside a VM environment
provision:
how: virtual
image: fedora:39
hardware:
virtualisation:
is-supported: true
prepare:
how: install
package:
Expand Down
9 changes: 6 additions & 3 deletions test/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

# local test utils
import testutil
from vm import VM


@pytest.fixture(name="output_path")
Expand Down Expand Up @@ -53,7 +54,7 @@ def test_smoke(output_path, config_json):
"--security-opt", "label=type:unconfined_t",
"-v", f"{output_path}:/output",
"bootc-image-builder-test",
"quay.io/centos-bootc/centos-bootc:stream9",
"quay.io/centos-bootc/fedora-bootc:eln",
"--config", "/output/config.json",
])
# check that there are no denials
Expand All @@ -63,5 +64,7 @@ def test_smoke(output_path, config_json):
assert journal_output != ""
generated_img = pathlib.Path(output_path) / "qcow2/disk.qcow2"
assert generated_img.exists(), f"output file missing, dir content: {os.listdir(os.fspath(output_path))}"
# TODO: boot and do basic checks, see
# https://github.com/osbuild/bootc-image-builder/compare/main...mvo5:integration-test?expand=1
with VM(generated_img) as test_vm:
# TODO: replace with 'test_vm.run("true")' once user creation via
# blueprints works
test_vm.wait_ssh_ready()
23 changes: 23 additions & 0 deletions test/testutil.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import socket
import shutil
import subprocess
import time


def journal_cursor():
Expand All @@ -15,3 +17,24 @@ def journal_after_cursor(cursor):

def has_executable(name):
return shutil.which(name) is not None


def get_free_port() -> int:
# this is racy but there is no race-free way to do better with the qemu CLI
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("localhost", 0))
return s.getsockname()[1]


def wait_ssh_ready(port, sleep, max_wait_sec):
for i in range(int(max_wait_sec / sleep)):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(sleep)
try:
s.connect(("localhost", port))
data = s.recv(256)
if b"OpenSSH" in data:
return
except (ConnectionRefusedError, TimeoutError):
time.sleep(sleep)
raise ConnectionRefusedError(f"cannot connect to port {port} after {max_wait_sec}s")
26 changes: 26 additions & 0 deletions test/testutil_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import contextlib
import subprocess
from unittest.mock import call, patch

import pytest

from testutil import has_executable, get_free_port, wait_ssh_ready


def test_get_free_port():
port_nr = get_free_port()
assert port_nr > 1024 and port_nr < 65535


@pytest.mark.skipif(not has_executable("nc"), reason="needs nc")
@patch("time.sleep")
def test_wait_ssh_ready(mocked_sleep):
port = get_free_port()
with pytest.raises(ConnectionRefusedError):
wait_ssh_ready(port, sleep=0.1, max_wait_sec=0.35)
assert mocked_sleep.call_args_list == [call(0.1), call(0.1), call(0.1)]
# now make port ready
with contextlib.ExitStack() as cm:
p = subprocess.Popen(f"echo OpenSSH | nc -l {port}", shell=True)
cm.callback(p.kill)
wait_ssh_ready(port, sleep=0.1, max_wait_sec=10)
66 changes: 66 additions & 0 deletions test/vm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import pathlib
import subprocess
import sys

from testutil import get_free_port, wait_ssh_ready


class VM:
MEM = "2000"
QEMU = "qemu-system-x86_64"

def __init__(self, img, snapshot=True):
self._img = pathlib.Path(img)
self._qemu_p = None
self._ssh_port = None
self._snapshot = snapshot

def __del__(self):
self.force_stop()

def start(self):
if self._qemu_p is not None:
return
log_path = self._img.with_suffix(".serial-log")
self._ssh_port = get_free_port()
qemu_cmdline = [
self.QEMU, "-enable-kvm",
"-m", self.MEM,
# get "illegal instruction" inside the VM otherwise
"-cpu", "host",
"-nographic",
"-serial", "stdio",
"-monitor", "none",
"-netdev", f"user,id=net.0,hostfwd=tcp::{self._ssh_port}-:22",
"-device", "rtl8139,netdev=net.0",
]
if self._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)
# 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}")

def _log(self, msg):
# XXX: use a proper logger
sys.stdout.write(msg.rstrip("\n") + "\n")

def wait_ssh_ready(self):
wait_ssh_ready(self._ssh_port, sleep=1, max_wait_sec=600)

def force_stop(self):
if self._qemu_p:
self._qemu_p.kill()
self._qemu_p = None

def __enter__(self):
self.start()
return self

def __exit__(self, type, value, tb):
self.force_stop()

0 comments on commit e8489e1

Please sign in to comment.