Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ build:x86_64-qnx --credential_helper=*.qnx.com=%workspace%/tools/qnx_credential_

test:qemu-integration --config=x86_64-qnx
test:qemu-integration --run_under=//scripts:run_under_qemu
test:qemu-integration --build_tests_only

build --java_language_version=17
build --java_runtime_version=remotejdk_17
Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/itf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ jobs:
uses: docker/setup-buildx-action@v3.8.0
- name: Setup Bazel
uses: bazel-contrib/setup-bazel@0.14.0
- name: Install qemu
run: |
sudo apt-get update
sudo apt-get install -y qemu-system
- name: Enable KVM group perms
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Allow unprivileged user namespaces
run: |
sudo sysctl kernel.apparmor_restrict_unprivileged_userns=0
- name: Run build
run: |
bazel build //...
Expand Down
14 changes: 14 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,17 @@ imagefs.toolchain(
type = "ifs",
)
use_repo(imagefs, "score_qnx_x86_64_ifs_toolchain")

###############################################################################
#
# EBclfsa fastdev archive repository for Linux QEMU integration tests
#
###############################################################################
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")

http_file(
name = "eb_corbos_toolkit_fastdev_archive",
downloaded_file_path = "fastdev-ubuntu-ebclfsa-ebcl-qemuarm64.tar.gz",
sha256 = "9ca3891b27e4b7bbf6c519d8924283e89c03b62513a988f751a3ee3d10c293e4",
urls = ["https://github.com/elektrobit/eb_corbos_toolkit/releases/download/v2.0.0-beta1/fastdev-ubuntu-ebclfsa-ebcl-qemuarm64.tar.gz"],
)
95 changes: 91 additions & 4 deletions score/itf/plugins/qemu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
import logging
import os
import subprocess
import socket
import tempfile
import pytest

from score.itf.plugins.qemu.qemu_target import qemu_target
Expand All @@ -23,14 +26,74 @@
logger = logging.getLogger(__name__)


def _get_image_format(path_to_image: str) -> str:
"""Determine the disk image format based on the file extension."""
image_lower = path_to_image.lower()
if image_lower.endswith(".wic") or image_lower.endswith(".img"):
return "raw"
return "qcow2"


def _create_overlay(base_image: str) -> str:
"""Create a temporary qcow2 overlay backed by *base_image*.

All writes go to the ephemeral overlay so the original image is never
modified. The caller is responsible for cleaning up the returned path.
"""
overlay_fd, overlay_path = tempfile.mkstemp(suffix=".qcow2", prefix="qemu_overlay_")
os.close(overlay_fd)
subprocess.run(
[
"qemu-img",
"create",
"-f",
"qcow2",
"-b",
base_image,
"-F",
_get_image_format(base_image),
overlay_path,
],
check=True,
capture_output=True,
)
logger.info(f"Created qcow2 overlay: {overlay_path} (backing: {base_image})")
return overlay_path
Comment thread
lurtz marked this conversation as resolved.


def pytest_addoption(parser):
parser.addoption(
"--qemu-config",
action="store",
required=True,
help="Path to json file with target configurations.",
)
parser.addoption("--qemu-image", action="store", help="Path to a QEMU image")
parser.addoption(
"--qemu-image",
action="store",
default=None,
help="Path to a QEMU kernel image",
)
parser.addoption(
"--qemu-disk-image",
action="store",
default=None,
help="Path to a QEMU disk image (qcow2, wic, or img). "
"An ephemeral overlay is created so the original image is not modified.",
)
parser.addoption(
"--qemu-architecture",
action="store",
choices=("x86_64", "aarch64"),
default="x86_64",
help="Target CPU architecture used to select the QEMU binary and CPU model.",
)
parser.addoption(
"--qemu-kernel-cmdline-file",
action="store",
default=None,
help="Path to a file containing the Linux kernel command line.",
)


@pytest.fixture(scope="session")
Expand All @@ -43,15 +106,39 @@ def dlt():

@pytest.fixture(scope="session")
def config(request):
qemu_kernel_cmdline = None
kernel_cmdline_file = request.config.getoption("qemu_kernel_cmdline_file")
if kernel_cmdline_file:
with open(os.path.abspath(kernel_cmdline_file), encoding="utf-8") as handle:
qemu_kernel_cmdline = handle.read().strip()

return Bunch(
qemu_config=load_configuration(request.config.getoption("qemu_config")),
qemu_image=request.config.getoption("qemu_image"),
qemu_disk_image=request.config.getoption("qemu_disk_image"),
qemu_architecture=request.config.getoption("qemu_architecture"),
qemu_kernel_cmdline=qemu_kernel_cmdline,
)


@pytest.fixture(scope="session")
def target_init(config, request, dlt):
logger.info(f"Starting tests on host: {socket.gethostname()}")
with qemu_target(config) as qemu:
pre_tests_phase(qemu)
yield qemu
overlay_path = None
if config.qemu_disk_image:
overlay_path = _create_overlay(os.path.abspath(config.qemu_disk_image))
try:
with qemu_target(
Bunch(
qemu_config=config.qemu_config,
qemu_image=config.qemu_image,
qemu_disk_image=overlay_path,
qemu_architecture=config.qemu_architecture,
qemu_kernel_cmdline=config.qemu_kernel_cmdline,
)
) as qemu:
pre_tests_phase(qemu)
yield qemu
finally:
if overlay_path and os.path.exists(overlay_path):
os.unlink(overlay_path)
118 changes: 71 additions & 47 deletions score/itf/plugins/qemu/qemu.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,32 @@
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
import logging
import os
import shlex
import subprocess
import sys
import logging

logger = logging.getLogger(__name__)


_SUPPORTED_ARCHITECTURES = {
"x86_64": {
"qemu_path": "/usr/bin/qemu-system-x86_64",
"cpu": "Cascadelake-Server-v5",
"network_device": "virtio-net-pci",
"machine": "pc",
"block_device": "virtio-blk-pci",
},
"aarch64": {
"qemu_path": "/usr/bin/qemu-system-aarch64",
"cpu": "cortex-a53",
"network_device": "virtio-net-device",
"machine": "virt,virtualization=true,gic-version=3",
"block_device": "virtio-blk-device",
},
}


class Qemu:
"""
This class shall be used to start an qemu instance based on pre-configured Qemu parameters.
Expand All @@ -27,32 +44,37 @@ class Qemu:
def __init__(
self,
path_to_image,
ram="1G",
cores="2",
cpu="Cascadelake-Server-v5",
network_adapters=[],
port_forwarding=[],
ram,
cores,
architecture,
network_adapters,
port_forwarding,
disk_image,
kernel_cmdline,
):
"""Create a QEMU instance with the specified parameters.

:param str path_to_image: The path to the Qemu image file.
:param str ram: The amount of RAM to allocate to the QEMU instance.
:param str cores: The number of CPU cores to allocate to the QEMU instance.
:param str cpu: The CPU model to emulate.
Default is Cascadelake-Server-v5 used to emulate modern Intel CPU features.
For older Ubuntu versions change that to host in case of errors.
:param str architecture: The CPU architecture to emulate (x86_64 or aarch64).
:param list network_adapters: List of network adapter names.
:param list port_forwarding: List of port forwarding configurations.
:param str disk_image: Optional path to a qcow2 disk image.
:param str kernel_cmdline: Optional kernel command line string.
"""
self.__qemu_path = "/usr/bin/qemu-system-x86_64"
if architecture not in _SUPPORTED_ARCHITECTURES:
raise ValueError("architecture must be one of: " + ", ".join(sorted(_SUPPORTED_ARCHITECTURES)))
self.__arch_config = _SUPPORTED_ARCHITECTURES[architecture]
self.__path_to_image = path_to_image
self.__ram = ram
self.__cores = cores
self.__cpu = cpu
self.__network_adapters = network_adapters
self.__port_forwarding = port_forwarding
self.__disk_image = disk_image
self.__kernel_cmdline = kernel_cmdline

self.__check_qemu_is_installed()
self.__find_available_kvm_support()
self.__check_kvm_readable_when_necessary()

self._subprocess = None

Expand Down Expand Up @@ -82,46 +104,28 @@ def stop(self):
raise Exception(f"QEMU process returned: {ret}")

def __check_qemu_is_installed(self):
if not os.path.isfile(self.__qemu_path):
logger.fatal(f"Qemu is not installed under {self.__qemu_path}")
qemu_path = self.__arch_config["qemu_path"]
if not os.path.isfile(qemu_path):
logger.fatal(f"Qemu is not installed under {qemu_path}")
sys.exit(-1)

def __find_available_kvm_support(self):
self._accelerator_support = "kvm"
with open("/proc/cpuinfo") as cpuinfo:
cpu_options = str(cpuinfo.read())
if "vmx" not in cpu_options and "svm" not in cpu_options:
logger.error("No virtual capability on machine. We're using standard TCG accel on QEMU")
self._accelerator_support = "tcg"

if not os.path.exists("/dev/kvm"):
logger.error("No KVM available. We're using standard TCG accel on QEMU")
self._accelerator_support = "tcg"

def __check_kvm_readable_when_necessary(self):
if self._accelerator_support == "kvm":
if not os.access("/dev/kvm", os.R_OK):
logger.fatal(
"You dont have access rights to /dev/kvm. Consider adding yourself to kvm group. Aborting."
)
sys.exit(-1)

def __build_qemu_command(self):
# Use hardware virtualization if available
accel = ["-enable-kvm"] if self._accelerator_support == "kvm" else ["-accel", "tcg"]

return (
[f"{self.__qemu_path}"]
+ accel
+ [
[
self.__arch_config["qemu_path"],
# Use hardware virtualization if available
"-accel",
"kvm",
"-accel",
"tcg",
"-smp",
f"{self.__cores},maxcpus={self.__cores},cores={self.__cores}",
"-machine",
self.__arch_config["machine"],
"-cpu",
f"{self.__cpu}", # Specify CPU to emulate
self.__arch_config["cpu"], # Specify CPU to emulate
"-m",
f"{self.__ram}", # Specify RAM size
"-kernel",
f"{self.__path_to_image}", # Specify kernel image
"-nographic", # Disable graphical display (console-only)
"-serial",
"mon:stdio", # Redirect serial output to console
Expand All @@ -132,15 +136,35 @@ def __build_qemu_command(self):
]
+ self.__network_devices_args()
+ self.__port_forwarding_args()
+ self.__kernel_args()
+ self.__disk_image_args()
)

def __kernel_args(self):
if not self.__path_to_image:
return []
args = ["-kernel", self.__path_to_image]
if self.__kernel_cmdline:
args.extend(["-append", self.__kernel_cmdline])
return args

def __disk_image_args(self):
if not self.__disk_image:
return []
return [
"-device",
f"{self.__arch_config['block_device']},drive=vd0",
"-drive",
f"if=none,format=qcow2,file={self.__disk_image},id=vd0",
]

def __network_devices_args(self):
def get_netdev_args(adapter, id):
return [
"-netdev",
f"tap,id=t{id},ifname={adapter},script=no,downscript=no",
"-device",
f"virtio-net-pci,netdev=t{id},id=nic{id},guest_csum=off",
f"{self.__arch_config['network_device']},netdev=t{id},id=nic{id},guest_csum=off",
]

result = []
Expand All @@ -157,7 +181,7 @@ def __port_forwarding_args(self):
"-netdev",
f"user,id=net{id},hostfwd=tcp::{forwarding.host_port}-:{forwarding.guest_port}",
"-device",
f"virtio-net-pci,netdev=net{id}",
f"{self.__arch_config['network_device']},netdev=net{id}",
]
)
return result
Loading
Loading