# 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](examples/example.png)
```python
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`](stages_schema.yml).
The following shows an example of such a file:
```yaml
stages:
- stage: Bootloader Selection
timeout_s: 15
paths:
- path:
check:
file: 0.png
mse_leq: 0.1
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:
check:
file: 0_1.png
mse_leq: 0.1
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:
check:
file: 1.png
mse_leq: 0.1
ssim_geq: 0.99
actions:
- keyboard_key:
value: up
duration_s: 0.25
nextStage: Installation Complete
- stage: Installation Complete
timeout_s: 600
paths:
- path:
check:
file: 2.png
mse_leq: 0.1
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:
check:
file: 3.png
mse_leq: 0.1
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:
```bash
python3 -m build
```
The output is then available inside the `dist/` directory.
## pre-commit
Before committing you have to run `pre-commit` to check for linting and type errors.
For this first install `pre-commit`.
```bash
dnf install pre-commit
pre-commit install
```
To run `pre-commit` manually run:
```bash
pre-commit run --all-files
```
Raw data
{
"_id": null,
"home_page": null,
"name": "os-tester",
"maintainer": null,
"docs_url": null,
"requires_python": ">=3.7",
"maintainer_email": "Fabian Sauter <fabian.sauter+pip@apsensing.com>",
"keywords": "testing, os, qemu, libvirt",
"author": null,
"author_email": "Fabian Sauter <fabian.sauter+pip@apsensing.com>",
"download_url": "https://files.pythonhosted.org/packages/ae/37/4f9c2dc99fbb0b2560f687bde627ef9cf969c0532a5ef896049401ffdcb6/os_tester-0.3.0.tar.gz",
"platform": null,
"description": "# OS Tester\nA Python pip package to automate testing of whole operating systems with an image recognition based approach and libvirt (qemu). Inspired by openQA.\n\n## Example\n\n![example_when_debug_is_enabled](examples/example.png)\n\n```python\nfrom os_tester.vm import vm\nfrom os_tester.stages import stages\nimport libvirt\n\n# SELinux Policy for allowing Qemu to access image files:\n# ausearch -c 'qemu-system-x86' --raw | audit2allow -M my-qemusystemx86\n# semodule -X 300 -i my-qemusystemx86.pp\n\n# NVME: http://blog.frankenmichl.de/2018/02/13/add-nvme-device-to-vm/\n# dd if=/dev/zero of=/tmp/test_vm_1.img bs=1M count=8192\n# Or:\n# qemu-img create -f qcow2 /tmp/test_vm_1.qcow2 8G\n\n\ndef get_vm_xml(name: str, title: str, uuid: str, isoPath: str, vmImagePath: str) -> str:\n ramGiB: int = 2\n numCpus: int = 2\n\n return f\"\"\"\n<domain type=\"kvm\" xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>\n <name>{name}</name>\n <uuid>{uuid}</uuid>\n <title>{title}</title>\n <memory unit=\"GiB\">{ramGiB}</memory>\n <currentMemory unit=\"GiB\">{ramGiB}</currentMemory>\n <vcpu placement=\"static\">{numCpus}</vcpu>\n <os firmware='efi'>\n <!--To get a list of all machines: qemu-system-x86_64 -machine help-->\n <type arch=\"x86_64\" machine=\"q35\">hvm</type>\n <bootmenu enable=\"yes\"/>\n <boot dev=\"hd\"/>\n <boot dev=\"cdrom\"/>\n </os>\n <features>\n <acpi/>\n <apic/>\n </features>\n <clock offset=\"localtime\">\n <timer name=\"rtc\" tickpolicy=\"catchup\"/>\n <timer name=\"pit\" tickpolicy=\"delay\"/>\n <timer name=\"hpet\" present=\"no\"/>\n </clock>\n <on_poweroff>destroy</on_poweroff>\n <on_reboot>restart</on_reboot>\n <on_crash>restart</on_crash>\n <pm>\n <suspend-to-mem enabled=\"no\"/>\n <suspend-to-disk enabled=\"no\"/>\n </pm>\n <devices>\n <emulator>/usr/bin/qemu-system-x86_64</emulator>\n <controller type='pci' index='0' model='pcie-root'/>\n <controller type='pci' index='1' model='pcie-root-port'>\n <model name='pcie-root-port'/>\n <target chassis='1' port='0x10'/>\n <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>\n </controller>\n <disk type=\"file\" device=\"cdrom\">\n <driver name=\"qemu\" type=\"raw\"/>\n <source file=\"{isoPath}\" startupPolicy=\"mandatory\"/>\n <target dev=\"hdc\" bus=\"sata\"/>\n <readonly/>\n <address type=\"drive\" controller=\"0\" bus=\"0\" target=\"0\" unit=\"2\"/>\n </disk>\n <interface type=\"user\">\n <mac address=\"52:54:00:8d:ce:97\"/>\n <model type=\"virtio\"/>\n <address type=\"pci\" domain=\"0x0000\" bus=\"0x01\" slot=\"0x00\" function=\"0x00\"/>\n </interface>\n <serial type=\"pty\">\n <target type=\"isa-serial\" port=\"0\">\n <model name=\"isa-serial\"/>\n </target>\n </serial>\n <console type=\"pty\">\n <target type=\"serial\" port=\"0\"/>\n </console>\n <input type=\"tablet\" bus=\"usb\">\n <address type=\"usb\" bus=\"0\" port=\"2\"/>\n </input>\n <input type=\"mouse\" bus=\"ps2\"/>\n <input type=\"keyboard\" bus=\"ps2\"/>\n <memballoon model=\"virtio\">\n <address type=\"pci\" domain=\"0x0000\" bus=\"0x05\" slot=\"0x00\" function=\"0x0\"/>\n </memballoon>\n </devices>\n <qemu:commandline>\n\t <qemu:arg value='-drive'/>\n\t <qemu:arg value='file={vmImagePath},format=qcow2,if=none,id=nvdisk1,media=disk'/>\n\t <qemu:arg value='-device'/>\n\t <qemu:arg value='nvme,bootindex=1,drive=nvdisk1,serial=1234,id=nvme0,bus=pcie.0,addr=0x04'/>\n </qemu:commandline>\n</domain>\n \"\"\"\n\nif __name__ == \"__main__\":\n # Connect to qemu\n conn: libvirt.virConnect = libvirt.open(\"qemu:///system\")\n\n uuid: str = \"1e6cae9f-41d7-4fca-8033-fbd538a65173\" # Replace with your (random?) UUID\n vmObj: vm = vm(conn, uuid, debugPlt=False)\n\n # Delete eventually existing VMs\n if vmObj.try_load():\n print(f\"Deleting existing VM for UUID '{uuid}'...\")\n vmObj.destroy()\n exit(0)\n print(f\"VM destroyed.\")\n else:\n print(f\"No existing VM found for UUID '{uuid}'.\")\n\n # Create and start a new VM\n vmXml: str = get_vm_xml(\n \"test_vm_1\",\n \"Test_VM_1\",\n uuid,\n \"<PATH_TO_THE_ISO_FILE_TO_BOOT_FROM>\",\n \"test_vm_1.qcow2\", # qemu-img create -f qcow2 test_vm_1.qcow2 8G\n )\n vmObj.create(vmXml)\n\n # Load stages automation.\n # We expect the `stages.yml` and referenced files inside the stages directory.\n basePath: str = \"stages\"\n stagesObj: stages = stages(basePath)\n print(stagesObj)\n \n vmObj.run_stages(stagesObj)\n\n print(\"All stages done. Exiting...\")\n conn.close()\n exit(0)\n```\n\n### Stages\nStages are defined as a YAML file. The schema for it is available under [`stages_schema.yml`](stages_schema.yml).\nThe following shows an example of such a file:\n```yaml\nstages:\n - stage: Bootloader Selection\n timeout_s: 15\n paths:\n - path:\n check:\n file: 0.png\n mse_leq: 0.1\n ssim_geq: 0.99\n actions:\n - keyboard_key:\n value: up\n duration_s: 0.25\n - keyboard_key:\n value: ret\n duration_s: 0.25\n nextStage: Installation Started\n - path:\n check:\n file: 0_1.png\n mse_leq: 0.1\n ssim_geq: 0.99\n actions:\n - keyboard_key:\n value: up\n duration_s: 0.25\n - keyboard_key:\n value: up\n duration_s: 0.25\n - keyboard_key:\n value: ret\n duration_s: 0.25\n nextStage: Installation Started\n\n - stage: Installation Started\n timeout_s: 600\n paths:\n - path:\n check:\n file: 1.png\n mse_leq: 0.1\n ssim_geq: 0.99\n actions:\n - keyboard_key:\n value: up\n duration_s: 0.25\n nextStage: Installation Complete\n\n - stage: Installation Complete\n timeout_s: 600\n paths:\n - path:\n check:\n file: 2.png\n mse_leq: 0.1\n ssim_geq: 0.99\n actions:\n - keyboard_key:\n value: tab\n duration_s: 0.25\n - keyboard_key:\n value: tab\n duration_s: 0.25\n - keyboard_key:\n value: ret\n duration_s: 0.25\n nextStage: Enter LUKS Password\n\n - stage: Enter LUKS Password\n timeout_s: 600\n paths:\n - path:\n check:\n file: 3.png\n mse_leq: 0.1\n ssim_geq: 0.99\n actions:\n - keyboard_text:\n value: something\n duration_s: 0.25\n - keyboard_key:\n value: ret\n duration_s: 0.25\n nextStage: None\n\n```\n\n## Building the pip-Package\n\nTo build the pip package run:\n```bash\npython3 -m build\n```\nThe output is then available inside the `dist/` directory.\n\n## pre-commit\nBefore committing you have to run `pre-commit` to check for linting and type errors.\nFor this first install `pre-commit`.\n\n```bash\ndnf install pre-commit\npre-commit install\n```\n\nTo run `pre-commit` manually run:\n```bash\npre-commit run --all-files\n```\n",
"bugtrack_url": null,
"license": "GNU General Public License v3 (GPLv3)",
"summary": "A Python pip package to automate testing of whole operating systems with an image recognition based approach and libvirt (qemu). Inspired by openQA.",
"version": "0.3.0",
"project_urls": {
"Homepage": "https://github.com/AP-Sensing/os-tester",
"Issues": "https://github.com/AP-Sensing/os-tester/issues",
"Repository": "https://github.com/AP-Sensing/os-tester"
},
"split_keywords": [
"testing",
" os",
" qemu",
" libvirt"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "6a98d36fb349d5984e54adec31675d2567190cd8fd10b5174302b881a93967fa",
"md5": "4ea2d6d78f203db7199df7f83804ac80",
"sha256": "0f939af09f7cf3fedbf0a1a78175db62c011bc80484ada764e284f6cfd9ef13a"
},
"downloads": -1,
"filename": "os_tester-0.3.0-py3-none-any.whl",
"has_sig": false,
"md5_digest": "4ea2d6d78f203db7199df7f83804ac80",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.7",
"size": 22420,
"upload_time": "2024-08-05T08:21:59",
"upload_time_iso_8601": "2024-08-05T08:21:59.292308Z",
"url": "https://files.pythonhosted.org/packages/6a/98/d36fb349d5984e54adec31675d2567190cd8fd10b5174302b881a93967fa/os_tester-0.3.0-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "ae374f9c2dc99fbb0b2560f687bde627ef9cf969c0532a5ef896049401ffdcb6",
"md5": "7ca64aa1360a891299ca2a26b814644d",
"sha256": "d016a6c29b2335c5a2dc28dbd9d459629fcf43e304eb8c7871581ab916bd1f87"
},
"downloads": -1,
"filename": "os_tester-0.3.0.tar.gz",
"has_sig": false,
"md5_digest": "7ca64aa1360a891299ca2a26b814644d",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.7",
"size": 24971,
"upload_time": "2024-08-05T08:22:00",
"upload_time_iso_8601": "2024-08-05T08:22:00.425630Z",
"url": "https://files.pythonhosted.org/packages/ae/37/4f9c2dc99fbb0b2560f687bde627ef9cf969c0532a5ef896049401ffdcb6/os_tester-0.3.0.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-08-05 08:22:00",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "AP-Sensing",
"github_project": "os-tester",
"travis_ci": false,
"coveralls": false,
"github_actions": true,
"requirements": [
{
"name": "scikit-image",
"specs": [
[
"==",
"0.24.0"
]
]
},
{
"name": "opencv-python-headless",
"specs": []
},
{
"name": "libvirt-python",
"specs": []
},
{
"name": "PyYAML",
"specs": []
},
{
"name": "numpy",
"specs": []
},
{
"name": "matplotlib",
"specs": []
}
],
"lcname": "os-tester"
}