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
Do some more testing of the generated image by booting it and
checking that ssh comes up. There will be a followup that will
actually login into the VM and ensure that also works.
  • Loading branch information
mvo5 authored and ondrejbudai committed Dec 8, 2023
1 parent b998c19 commit 5f13442
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/testingfarm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ jobs:
git_url: ${{ github.event.pull_request.head.repo.clone_url }}
git_ref: ${{ github.event.pull_request.head.ref }}
pull_request_status_name: "Testing farm"
tf_scope: private
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
4 changes: 4 additions & 0 deletions plans/all.fmf
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ summary: Run all tests inside a VM environment
provision:
how: virtual
image: fedora:39
hardware:
virtualization:
is-supported: true
prepare:
how: install
package:
- podman
- pytest
- python3-flake8
- qemu-kvm
execute:
how: tmt
script: pytest -s -vv
9 changes: 6 additions & 3 deletions test/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

# local test utils
import testutil
from vm import VM


@pytest.fixture(name="output_path")
Expand Down Expand Up @@ -76,7 +77,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",
])
generated_img = pathlib.Path(output_path) / "qcow2/disk.qcow2"
Expand All @@ -90,5 +91,7 @@ def test_smoke(output_path, config_json):
else:
print("WARNING: selinux not enabled, cannot check for denials")

# 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")
27 changes: 27 additions & 0 deletions test/testutil_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import contextlib
import subprocess
import time
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", wraps=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 5f13442

Please sign in to comment.