Skip to content

Commit

Permalink
target: Implement QEMU target runner
Browse files Browse the repository at this point in the history
Add support for launching emulated targets on QEMU.

This requires having buildroot and QEMU packages installed on host
machine.

Newly introduced QEMUTargetRunner class is a simple wrapper around
TargetRunner which is also a new class to manage target runners like
QEMU:
- perform sanity checks to ensure QEMU executable, rootfs and kernel
  images exist,
- build QEMU parameters properly,
- create Target class based object,
- let TargetRunner start QEMU process to launch the guest OS and stop it
  once the target is done.

Also add a new test case in ``tests/spin_targets.py`` to ensure devlib
can run a QEMU target and execute some basic commands on it.

Signed-off-by: Metin Kaya <[email protected]>
  • Loading branch information
metin-arm committed Jan 26, 2024
1 parent 61c9f84 commit ab2b6c1
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 3 deletions.
2 changes: 1 addition & 1 deletion devlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

from devlib.target import (
Target, LinuxTarget, AndroidTarget, LocalLinuxTarget,
ChromeOsTarget,
ChromeOsTarget, QEMUTargetRunner,
)

from devlib.host import (
Expand Down
128 changes: 127 additions & 1 deletion devlib/target.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@
#

import asyncio
import atexit
import io
import base64
import functools
import gzip
import glob
import os
import re
import sys
import time
import logging
import posixpath
import select
import signal
import subprocess
import tarfile
import tempfile
Expand All @@ -38,6 +42,7 @@
from past.types import basestring
from numbers import Number
from shlex import quote
from platform import machine
try:
from collections.abc import Mapping
except ImportError:
Expand All @@ -56,7 +61,7 @@
from devlib.utils.ssh import SshConnection
from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, INTENT_FLAGS
from devlib.utils.misc import memoized, isiterable, convert_new_lines, groupby_value
from devlib.utils.misc import commonprefix, merge_lists
from devlib.utils.misc import check_output, get_subprocess, commonprefix, merge_lists, which
from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list
from devlib.utils.misc import batch_contextmanager, tls_property, _BoundTLSProperty, nullcontext
from devlib.utils.misc import safe_extract
Expand Down Expand Up @@ -2893,6 +2898,127 @@ def _resolve_paths(self):
self.executables_directory = '/tmp/devlib-target/bin'


class TargetRunner:
'''
Class for running targets.
'''

banner = None
boot_timeout = None
runner_cmd = None
target = None
connection_settings = None

def __init__(self):

self.logger = logging.getLogger(self.__class__.__name__)

self.logger.info('runner_cmd: %s', self.runner_cmd)

try:
self.runner_process = get_subprocess(self.runner_cmd, shell=True)
except Exception as ex:
raise HostError(f'Error while running "{self.runner_cmd}": {ex}') from ex

atexit.register(self.terminate_runner)

self.wait_boot_complete()

self.target.connect(check_boot_completed=False)

def wait_boot_complete(self):
start_time = time.time()
boot_completed = False
poll_obj = select.poll()
poll_obj.register(self.runner_process.stdout, select.POLLIN)

while not boot_completed and self.boot_timeout >= (time.time() - start_time):
poll_result = poll_obj.poll(0)
if poll_result:
line = self.runner_process.stdout.readline().rstrip(b'\r\n')
line = line.decode(sys.stdout.encoding or 'utf-8', "replace") if line else ''
self.logger.debug(line)
if self.banner in line:
self.logger.info('Target is ready.')
boot_completed = True
break

if not boot_completed:
raise TargetStableError(f'Target could not finish boot up in {self.boot_timeout} seconds!')

def terminate_runner(self):
self.logger.info('Terminating target runner...')
os.killpg(self.runner_process.pid, signal.SIGTERM)


class QEMUTargetRunner(TargetRunner):
'''
Class for running QEMU targets.
'''

def __init__(self,
qemu_params=None,
connection_settings=None,
target_class=LinuxTarget,
target_params=None):

qemu_params_full={
'kernel_image': str(),
'arch': 'aarch64',
'cpu_types': '-cpu cortex-a72',
'initrd_image': str(),
'mem_size': 512,
'num_cores': 2,
'num_threads': 2,
'cmdline': 'console=ttyAMA0',
'enable_kvm': True,
'boot_timeout': 60,
'banner': 'Welcome to Buildroot'
}

for key, value in qemu_params.items():
if key in qemu_params_full:
qemu_params_full[key] = value

qemu_executable = f'qemu-system-{qemu_params_full["arch"]}'
qemu_path = which(qemu_executable)
if qemu_path is None:
raise FileNotFoundError(f'Cannot find {qemu_executable} executable!')

if not os.path.exists(qemu_params_full["kernel_image"]):
raise FileNotFoundError(f'{qemu_params_full["kernel_image"]} does not exist!')

qemu_cmd = f'{qemu_path} -kernel {quote(qemu_params_full["kernel_image"])} ' \
f'-append "{quote(qemu_params_full["cmdline"])}" ' \
f'-smp cores={qemu_params_full["num_cores"]},' \
f'threads={qemu_params_full["num_threads"]} ' \
f'-m {qemu_params_full["mem_size"]} ' \
f'-netdev user,id=net0,hostfwd=tcp::{connection_settings["port"]}-:22 ' \
'-device virtio-net-pci,netdev=net0 --nographic'

if qemu_params_full["initrd_image"]:
if not os.path.exists(qemu_params_full["initrd_image"]):
raise FileNotFoundError(f'{qemu_params_full["initrd_image"]} does not exist!')
qemu_cmd = f'{qemu_cmd} -initrd {quote(qemu_params_full["initrd_image"])}'

if qemu_params_full["arch"] == machine():
qemu_cmd = f'{qemu_cmd} {"--enable-kvm" if qemu_params_full["enable_kvm"] else ""}'
else:
qemu_cmd = f'{qemu_cmd} -machine virt {qemu_params_full["cpu_types"]}'

self.runner_cmd = qemu_cmd
self.banner = qemu_params_full["banner"]
self.connection_settings = connection_settings
self.boot_timeout = qemu_params_full["boot_timeout"]

self.target = target_class(connect=False,
conn_cls=SshConnection,
connection_settings=connection_settings,
**target_params)

super().__init__()


def _get_model_name(section):
name_string = section['model name']
parts = name_string.split('@')[0].strip().split()
Expand Down
3 changes: 3 additions & 0 deletions doc/overview.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ There are currently four target interfaces:
- :class:`~devlib.target.ChromeOsTarget`: for interacting with ChromeOS devices
over SSH, and their Android containers over adb.
- :class:`~devlib.target.LocalLinuxTarget`: for interacting with the local Linux host.
- :class:`~devlib.target.TargetRunner`: a generic class for interacting with targets
runners (e.g., QEMU).
- :class:`~devlib.target.QEMUTargetRunner`: for interacting with QEMU runners.

They all work in more-or-less the same way, with the major difference being in
how connection settings are specified; though there may also be a few APIs
Expand Down
96 changes: 96 additions & 0 deletions doc/target.rst
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,102 @@ Android Target
support the majority of newer devices.


Target Runner
-------------

.. class:: TargetRunner()

:class:`TargetRunner` is a class to provide framework support for QEMU like
target runners.

.. attribute:: TargetRunner.banner

This is the expected banner when the target OS prints to indicate it's ready
for connecting.

.. attribute:: TargetRunner.boot_timeout

Timeout for waiting target OS to finish boot up.

.. attribute:: TargetRunner.runner_cmd

This is the command necessary for executing target OS (e.g.,
``qemu-system-aarch64 -kernel Image -append "console=ttyAMA0" ...``).

.. attribute:: TargetRunner.target

This is :class:`Target` based object to indicate type of target OS (e.g.,
``LinuxTarget``).

.. attribute:: TargetRunner.connection_settings

The dictionary which stores connection settings of
:param:`Target.connection_settings`.

.. method:: TargetRunner.wait_boot_complete()

Method for ensuring target OS can finish boot up (a.k.a print
:attribute:`TargetRunner.banner`) in at most
:attribute:`TargetRunner.boot_timeout` seconds.

.. method:: TargetRunner.terminate_runner()

This method ensures termination of target runner process after the target OS
completes its execution.


QEMU Target Runner
------------------

.. class:: QEMUTargetRunner(qemu_params=None, connection_settings=None, target_class=LinuxTarget, target_params=None)

:class:`QEMUTargetRunner` is a subclass of :class:`TargetRunner` which
performs necessary groundwork for running a guest OS on QEMU.

:param qemu_params: A dictionary which has QEMU related parameters.

The full list of QEMU parameters is below:

* ``kernel_image``: This is the location of kernel image
(e.g., ``bzImage``) which will be used as target's kernel.

* ``arch``: Architecture type. Defaults to ``aarch64``.

* ``cpu_types``: List of CPU ids for QEMU. Defaults to ``-cpu cortex-a72``.
Valid only for Arm architectures.

* ``initrd_image``: This is an optional parameter which points to the
location of initrd image (e.g., ``rootfs.cpio.xz``) which will be used
as target's root filesystem if kernel does not include one already.

* ``mem_size``: Size of guest memory in MiB.

* ``num_cores``: Number of CPU cores. Guest will have ``2`` cores in
default settings.

* ``num_threads``: Number of CPU threads. Set to ``2`` by defaults.

* ``cmdline``: Kernel command line parameter. It only specifies console
device in default (i.e., ``console=ttyAMA0``) which is valid for Arm
architectures. It should be changed to ``ttyS0`` for x86 platforms.

* ``enable_kvm``: Specifies if KVM will be used as accelerator in QEMU
or not. Valid only for x86 architectures. Enabled by default for
improving QEMU performance.

* ``boot_timeout``: Timeout for login prompt of guest in seconds.
It's set to ``60`` seconds by default.

* ``banner``: This is the system banner displayed at login which is set to
``Welcome to Buildroot`` by default.

:param target_class: :class:`Target` based object which indicates type of
target OS. Default target class is :class:`LinuxTarget`.

:param target_params: The dictionary which stores parameters for
:param:`target_class`.


ChromeOS Target
---------------

Expand Down
12 changes: 11 additions & 1 deletion tests/spin_targets.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

import os
from unittest import TestCase
from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget
from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget, QEMUTargetRunner
from devlib.utils.misc import get_random_string
from devlib.exception import TargetStableError

Expand Down Expand Up @@ -88,6 +88,16 @@ def create_targets():
working_directory='/tmp/devlib-target')
targets.append(ll_target)

qemu_runner = QEMUTargetRunner(qemu_params={
'kernel_image': '/home/username/devlib/tools/buildroot/buildroot-v2023.11.1/output/images/Image'},
connection_settings={'host': '127.0.0.1',
'port': 8022,
'username': 'root',
'password': 'root',
'strict_host_check': False},
target_params={'working_directory': '/tmp/devlib-target'})
targets.append(qemu_runner.target)

return targets

method_name = self.__class__.test_read_multiline_values.__qualname__
Expand Down

0 comments on commit ab2b6c1

Please sign in to comment.