Skip to main content

OS Tester

A Python pip package to automate testing of whole operating systems with an image recognition based approach and libvirt (qemu). Inspired by openQA.

Example

example_when_debug_is_enabled

from os_tester.vm import vm
from os_tester.stages import stages
import libvirt

# SELinux Policy for allowing Qemu to access image files:
# ausearch -c 'qemu-system-x86' --raw | audit2allow -M my-qemusystemx86
# semodule -X 300 -i my-qemusystemx86.pp

# NVME: http://blog.frankenmichl.de/2018/02/13/add-nvme-device-to-vm/
# dd if=/dev/zero of=/tmp/test_vm_1.img bs=1M count=8192
# Or:
# qemu-img create -f qcow2 /tmp/test_vm_1.qcow2 8G


def get_vm_xml(name: str, title: str, uuid: str, isoPath: str, vmImagePath: str) -> str:
    ramGiB: int = 2
    numCpus: int = 2

    return f"""
<domain type="kvm" xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
  <name>{name}</name>
  <uuid>{uuid}</uuid>
  <title>{title}</title>
  <memory unit="GiB">{ramGiB}</memory>
  <currentMemory unit="GiB">{ramGiB}</currentMemory>
  <vcpu placement="static">{numCpus}</vcpu>
  <os firmware='efi'>
  <!--To get a list of all machines: qemu-system-x86_64 -machine help-->
    <type arch="x86_64" machine="q35">hvm</type>
    <bootmenu enable="yes"/>
    <boot dev="hd"/>
    <boot dev="cdrom"/>
  </os>
  <features>
    <acpi/>
    <apic/>
  </features>
  <clock offset="localtime">
    <timer name="rtc" tickpolicy="catchup"/>
    <timer name="pit" tickpolicy="delay"/>
    <timer name="hpet" present="no"/>
  </clock>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>restart</on_crash>
  <pm>
    <suspend-to-mem enabled="no"/>
    <suspend-to-disk enabled="no"/>
  </pm>
  <devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <controller type='pci' index='0' model='pcie-root'/>
    <controller type='pci' index='1' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='1' port='0x10'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
    </controller>
    <disk type="file" device="cdrom">
      <driver name="qemu" type="raw"/>
      <source file="{isoPath}" startupPolicy="mandatory"/>
      <target dev="hdc" bus="sata"/>
      <readonly/>
      <address type="drive" controller="0" bus="0" target="0" unit="2"/>
    </disk>
    <interface type="user">
      <mac address="52:54:00:8d:ce:97"/>
      <model type="virtio"/>
      <address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x00"/>
    </interface>
    <serial type="pty">
      <target type="isa-serial" port="0">
        <model name="isa-serial"/>
      </target>
    </serial>
    <console type="pty">
      <target type="serial" port="0"/>
    </console>
    <input type="tablet" bus="usb">
      <address type="usb" bus="0" port="2"/>
    </input>
    <input type="mouse" bus="ps2"/>
    <input type="keyboard" bus="ps2"/>
    <memballoon model="virtio">
      <address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0"/>
    </memballoon>
  </devices>
  <qemu:commandline>
	  <qemu:arg value='-drive'/>
	  <qemu:arg value='file={vmImagePath},format=qcow2,if=none,id=nvdisk1,media=disk'/>
	  <qemu:arg value='-device'/>
	  <qemu:arg value='nvme,bootindex=1,drive=nvdisk1,serial=1234,id=nvme0,bus=pcie.0,addr=0x04'/>
  </qemu:commandline>
</domain>
    """

if __name__ == "__main__":
    # Connect to qemu
    conn: libvirt.virConnect = libvirt.open("qemu:///system")

    uuid: str = "1e6cae9f-41d7-4fca-8033-fbd538a65173" # Replace with your (random?) UUID
    vmObj: vm = vm(conn, uuid, debugPlt=False)

    # Delete eventually existing VMs
    if vmObj.try_load():
        print(f"Deleting existing VM for UUID '{uuid}'...")
        vmObj.destroy()
        exit(0)
        print(f"VM destroyed.")
    else:
        print(f"No existing VM found for UUID '{uuid}'.")

    # Create and start a new VM
    vmXml: str = get_vm_xml(
        "test_vm_1",
        "Test_VM_1",
        uuid,
        "<PATH_TO_THE_ISO_FILE_TO_BOOT_FROM>",
        "test_vm_1.qcow2", # qemu-img create -f qcow2 test_vm_1.qcow2 8G
    )
    vmObj.create(vmXml)

    # Load stages automation.
    # We expect the `stages.yml` and referenced files inside the stages directory.
    basePath: str = "stages"
    stagesObj: stages = stages(basePath)
    print(stagesObj)

    vmObj.run_stages(stagesObj)

    print("All stages done. Exiting...")
    conn.close()
    exit(0)

Stages

Stages are defined as a YAML file. The schema for it is available under stages_schema.yml. The following shows an example of such a file:

stages:
  - stage: Bootloader Selection
    timeout_s: 15
    paths:
      - path:
          checks:
            - file:
              path: 0.png
              ssim_geq: 0.99
          actions:
            - keyboard_key:
                value: up
                duration_s: 0.25
            - keyboard_key:
                value: ret
                duration_s: 0.25
          nextStage: Installation Started
      - path:
          checks:
            - file:
              path: 0_1.png
              ssim_geq: 0.99
          actions:
            - keyboard_key:
                value: up
                duration_s: 0.25
            - keyboard_key:
                value: up
                duration_s: 0.25
            - keyboard_key:
                value: ret
                duration_s: 0.25
          nextStage: Installation Started

  - stage: Installation Started
    timeout_s: 600
    paths:
      - path:
          checks:
            - file:
              path: 1.png
              ssim_geq: 0.99
          actions:
            - keyboard_key:
                value: up
                duration_s: 0.25
          nextStage: Installation Complete

  - stage: Installation Complete
    timeout_s: 600
    paths:
      - path:
          checks:
            - file:
              path: 2.png
              ssim_geq: 0.99
          actions:
            - keyboard_key:
                value: tab
                duration_s: 0.25
            - keyboard_key:
                value: tab
                duration_s: 0.25
            - keyboard_key:
                value: ret
                duration_s: 0.25
          nextStage: Enter LUKS Password

  - stage: Enter LUKS Password
    timeout_s: 600
    paths:
      - path:
          checks:
            - file:
              path: 3.png
              ssim_geq: 0.99
          actions:
            - keyboard_text:
                value: something
                duration_s: 0.25
            - keyboard_key:
                value: ret
                duration_s: 0.25
          nextStage: None

Building the pip-Package

To build the pip package run:

rm -rf dist/
python3 -m build

The output is then available inside the dist/ directory.

Install Locally

python3 -m pip install --force-reinstall --no-deps dist/*.whl

Upload

twine upload dist/*

pre-commit

Before committing you have to run pre-commit to check for linting and type errors. For this first install pre-commit.

dnf install pre-commit
pre-commit install

To run pre-commit manually run:

pre-commit run --all-files

Testing

To run the unit tests locally:

# System deps (needed to build libvirt-python)
dnf install libvirt-devel pkgconf-pkg-config

# Python deps
python3 -m pip install -r requirements.txt
python3 -m pip install pytest

# Run tests
pytest -q

On Debian/Ubuntu, replace the system deps line with:

sudo apt-get install libvirt-dev pkg-config

Metadata

Release files for os-tester 1.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for os-tester 1.2.0
File Size Uploaded
os_tester-1.2.0.tar.gz 28.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for os-tester 1.2.0
File Interpreter ABI Platform
os_tester-1.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 52.9 kB

Release files / os_tester-1.2.0.tar.gz

Download URL os_tester-1.2.0.tar.gz
Size 28.5 kB
Tags Source
SHA-256 checksum
How to use checksums
c3e17d7cddef907c4b34f665fff3be52a693a4bbcf463f3914031dc45d915377
BLAKE2b-256 checksum
How to use checksums
5edf78077dd8a3d19e41534984da0d93ee998b3e8094c73080ca3b99bb390147
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.2

Release files / os_tester-1.2.0-py3-none-any.whl

Download URL os_tester-1.2.0-py3-none-any.whl
Size 24.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e3e178538e954985768d6bcbff190ed29e345df3cd3091345eab91ae1a44b7d2
BLAKE2b-256 checksum
How to use checksums
0f2157f86f2b85537a8327da8e755f4b65688123401d8a5d7dd27710d6977792
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.2
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page