Skip to content

Commit e9e48b1

Browse files
committed
target: Implement target runner classes
Add support for launching emulated targets on QEMU. The base class ``TargetRunner`` has groundwork for target runners like ``QEMUTargetRunner``. ``TargetRunner`` is a contextmanager which starts runner process (e.g., QEMU), makes sure the target is accessible over SSH (if ``connect=True``), and terminates the runner process once it's done. The other newly introduced ``QEMUTargetRunner`` class: - performs sanity checks to ensure QEMU executable, kernel, and initrd images exist, - builds QEMU parameters properly, - creates ``Target`` object, - and lets ``TargetRunner`` manage the QEMU instance. Also add a new test case in ``tests/test_target.py`` to ensure devlib can run a QEMU target and execute some basic commands on it. While we are in neighborhood, fix a typo in ``Target.setup()``. Signed-off-by: Metin Kaya <metin.kaya@arm.com>
1 parent 2955a10 commit e9e48b1

File tree

4 files changed

+259
-4
lines changed

4 files changed

+259
-4
lines changed

devlib/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
from devlib.target import (
2121
Target, LinuxTarget, AndroidTarget, LocalLinuxTarget,
22-
ChromeOsTarget,
22+
ChromeOsTarget, QEMUTargetRunner,
2323
)
2424

2525
from devlib.host import (

devlib/target.py

Lines changed: 246 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import time
2626
import logging
2727
import posixpath
28+
import signal
2829
import subprocess
2930
import tarfile
3031
import tempfile
@@ -39,6 +40,7 @@
3940
from past.types import basestring
4041
from numbers import Number
4142
from shlex import quote
43+
from platform import machine
4244
try:
4345
from collections.abc import Mapping
4446
except ImportError:
@@ -57,7 +59,7 @@
5759
from devlib.utils.ssh import SshConnection
5860
from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, INTENT_FLAGS
5961
from devlib.utils.misc import memoized, isiterable, convert_new_lines, groupby_value
60-
from devlib.utils.misc import commonprefix, merge_lists
62+
from devlib.utils.misc import get_subprocess, commonprefix, merge_lists, which
6163
from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list
6264
from devlib.utils.misc import batch_contextmanager, tls_property, _BoundTLSProperty, nullcontext
6365
from devlib.utils.misc import safe_extract
@@ -494,7 +496,7 @@ async def setup(self, executables=None):
494496
# Check for platform dependent setup procedures
495497
self.platform.setup(self)
496498

497-
# Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
499+
# Initialize modules which requires Busybox (e.g. shutil dependent tasks)
498500
self._update_modules('setup')
499501

500502
await self.execute.asyn('mkdir -p {}'.format(quote(self._file_transfer_cache)))
@@ -2924,6 +2926,248 @@ def _resolve_paths(self):
29242926
self.executables_directory = '/tmp/devlib-target/bin'
29252927

29262928

2929+
class TargetRunner:
2930+
'''
2931+
A generic class for interacting with targets runners.
2932+
2933+
It mainly aims to provide framework support for QEMU like target runners
2934+
(e.g., :class:`QEMUTargetRunner`).
2935+
'''
2936+
2937+
def __init__(self,
2938+
runner_cmd,
2939+
target,
2940+
connect=True,
2941+
boot_timeout=60):
2942+
'''
2943+
Initialization procedure for :class:`TargetRunner` objects.
2944+
2945+
Args:
2946+
runner_cmd (str): The command to start runner process
2947+
(e.g., ``qemu-system-aarch64 -kernel Image -append "console=ttyAMA0" ...``).
2948+
target (Target): Specifies type of target per :class:`Target` based classes.
2949+
connect (bool, optional): Specifies if :class:`TargetRunner` should try to connect
2950+
target after launching it. Defaults to ``True``.
2951+
boot_timeout (int, optional): Timeout for target's being ready for SSH access.
2952+
Defaults to ``60`` seconds.
2953+
2954+
Raises:
2955+
HostError: if it cannot execute runner command successfully.
2956+
'''
2957+
2958+
self.boot_timeout = boot_timeout
2959+
self.target = target
2960+
2961+
self.logger = logging.getLogger(self.__class__.__name__)
2962+
2963+
self.logger.info('runner_cmd: %s', runner_cmd)
2964+
2965+
try:
2966+
self.runner_process = get_subprocess(list(runner_cmd.split()))
2967+
except Exception as ex:
2968+
raise HostError(f'Error while running "{runner_cmd}": {ex}') from ex
2969+
2970+
if connect:
2971+
self.wait_boot_complete()
2972+
2973+
def __enter__(self):
2974+
'''
2975+
Complementary method for contextmanager.
2976+
2977+
Returns:
2978+
TargetRunner: Self object.
2979+
'''
2980+
2981+
return self
2982+
2983+
def __exit__(self, *_):
2984+
'''
2985+
Exit routine for contextmanager.
2986+
2987+
Ensure :attr:`TargetRunner.runner_process` is terminated on exit.
2988+
'''
2989+
2990+
self.terminate_target()
2991+
2992+
def wait_boot_complete(self):
2993+
'''
2994+
Wait for target OS to finish boot up and become accessible over SSH in at most
2995+
:attr:`TargetRunner.boot_timeout` seconds.
2996+
2997+
Raises:
2998+
TargetStableError: In case of timeout.
2999+
'''
3000+
3001+
start_time = time.time()
3002+
elapsed = 0
3003+
while self.boot_timeout >= elapsed:
3004+
try:
3005+
self.target.connect(timeout=self.boot_timeout - elapsed)
3006+
self.logger.info('Target is ready.')
3007+
return
3008+
# pylint: disable=broad-except
3009+
except BaseException as ex:
3010+
self.logger.info('Cannot connect target: %s', ex)
3011+
3012+
time.sleep(1)
3013+
elapsed = time.time() - start_time
3014+
3015+
self.terminate_target()
3016+
raise TargetStableError(f'Target is inaccessible for {self.boot_timeout} seconds!')
3017+
3018+
def terminate_target(self):
3019+
'''
3020+
Terminate :attr:`TargetRunner.runner_process`.
3021+
'''
3022+
3023+
if self.runner_process is None:
3024+
return
3025+
3026+
try:
3027+
self.runner_process.stdin.close()
3028+
self.runner_process.stdout.close()
3029+
self.runner_process.stderr.close()
3030+
3031+
if self.runner_process.poll() is None:
3032+
self.logger.debug('Terminating target runner...')
3033+
os.killpg(self.runner_process.pid, signal.SIGTERM)
3034+
# Wait 3 seconds before killing the runner.
3035+
self.runner_process.wait(timeout=3)
3036+
except subprocess.TimeoutExpired:
3037+
self.logger.info('Killing target runner...')
3038+
os.killpg(self.runner_process.pid, signal.SIGKILL)
3039+
3040+
3041+
class QEMUTargetRunner(TargetRunner):
3042+
'''
3043+
Class for interacting with QEMU runners.
3044+
3045+
:class:`QEMUTargetRunner` is a subclass of :class:`TargetRunner` which performs necessary
3046+
groundwork for launching a guest OS on QEMU.
3047+
'''
3048+
3049+
def __init__(self,
3050+
qemu_params,
3051+
connection_settings=None,
3052+
# pylint: disable=unnecessary-lambda
3053+
make_target=lambda **kwargs: LinuxTarget(**kwargs),
3054+
**args):
3055+
'''
3056+
Init procedure for :class:`QEMUTargetRunner` class.
3057+
3058+
Args:
3059+
qemu_params (dict): A dictionary which has QEMU related parameters. The full list of
3060+
QEMU parameters is below:
3061+
* ``kernel_image``: This is the location of kernel image (e.g., ``Image``) which
3062+
will be used as target's kernel.
3063+
3064+
* ``arch``: Architecture type. Defaults to ``aarch64``.
3065+
3066+
* ``cpu_types``: List of CPU ids for QEMU. The list only contains ``cortex-a72`` by
3067+
default. This parameter is valid for Arm architectures only.
3068+
3069+
* ``initrd_image``: This points to the location of initrd image (e.g.,
3070+
``rootfs.cpio.xz``) which will be used as target's root filesystem if kernel
3071+
does not include one already.
3072+
3073+
* ``mem_size``: Size of guest memory in MiB.
3074+
3075+
* ``num_cores``: Number of CPU cores. Guest will have ``2`` cores by default.
3076+
3077+
* ``num_threads``: Number of CPU threads. Set to ``2`` by defaults.
3078+
3079+
* ``cmdline``: Kernel command line parameter. It only specifies console device in
3080+
default (i.e., ``console=ttyAMA0``) which is valid for Arm architectures.
3081+
May be changed to ``ttyS0`` for x86 platforms.
3082+
3083+
* ``enable_kvm``: Specifies if KVM will be used as accelerator in QEMU or not.
3084+
Enabled by default if host architecture matches with target's for improving
3085+
QEMU performance.
3086+
3087+
connection_settings (dict, optional): The dictionary which stores connection settings
3088+
of :attr:`Target.connection_settings`. Defaults to ``None``.
3089+
make_target (func, optional): Lambda function for creating :class:`Target` based
3090+
object. Defaults to :func:`lambda **kwargs: LinuxTarget(**kwargs)`.
3091+
args (optional): Arguments for :class:`TargetRunner` class.
3092+
3093+
Raises:
3094+
FileNotFoundError: if QEMU executable, kernel or initrd image cannot be found.
3095+
'''
3096+
3097+
connection_settings_default = {
3098+
'host': '127.0.0.1',
3099+
'port': 8022,
3100+
'username': 'root',
3101+
'password': 'root',
3102+
'strict_host_check': False,
3103+
}
3104+
3105+
# Update default connection settings with :param:`connection_settings` (if exists).
3106+
if connection_settings is not None:
3107+
connection_settings_default = { **connection_settings_default, **connection_settings }
3108+
3109+
qemu_default_args = {
3110+
'kernel_image': '',
3111+
'arch': 'aarch64',
3112+
'cpu_type': 'cortex-a72',
3113+
'initrd_image': '',
3114+
'mem_size': 512,
3115+
'num_cores': 2,
3116+
'num_threads': 2,
3117+
'cmdline': 'console=ttyAMA0',
3118+
'enable_kvm': True,
3119+
}
3120+
3121+
# Update default QEMU parameters with :param:`qemu_params`.
3122+
qemu_default_args.update(
3123+
(key, value)
3124+
for key, value in qemu_params.items()
3125+
if key in qemu_default_args
3126+
)
3127+
3128+
qemu_executable = f'qemu-system-{qemu_default_args["arch"]}'
3129+
qemu_path = which(qemu_executable)
3130+
if qemu_path is None:
3131+
raise FileNotFoundError(f'Cannot find {qemu_executable} executable!')
3132+
3133+
if not os.path.exists(qemu_default_args["kernel_image"]):
3134+
raise FileNotFoundError(f'{qemu_default_args["kernel_image"]} does not exist!')
3135+
3136+
# pylint: disable=consider-using-f-string
3137+
qemu_cmd = '''\
3138+
{} -kernel {} -append "{}" -m {} -smp cores={},threads={} -netdev user,id=net0,hostfwd=tcp::{}-:22 \
3139+
-device virtio-net-pci,netdev=net0 --nographic\
3140+
'''.format(
3141+
qemu_path,
3142+
qemu_default_args["kernel_image"],
3143+
qemu_default_args["cmdline"],
3144+
qemu_default_args["mem_size"],
3145+
qemu_default_args["num_cores"],
3146+
qemu_default_args["num_threads"],
3147+
connection_settings_default["port"],
3148+
)
3149+
3150+
if qemu_default_args["initrd_image"]:
3151+
if not os.path.exists(qemu_default_args["initrd_image"]):
3152+
raise FileNotFoundError(f'{qemu_default_args["initrd_image"]} does not exist!')
3153+
3154+
qemu_cmd += f' -initrd {qemu_default_args["initrd_image"]}'
3155+
3156+
if qemu_default_args["arch"] == machine():
3157+
if qemu_default_args["enable_kvm"]:
3158+
qemu_cmd += ' --enable-kvm'
3159+
else:
3160+
qemu_cmd += f' -machine virt -cpu {qemu_default_args["cpu_type"]}'
3161+
3162+
self.target = make_target(connect=False,
3163+
conn_cls=SshConnection,
3164+
connection_settings=connection_settings_default)
3165+
3166+
super().__init__(runner_cmd=qemu_cmd,
3167+
target=self.target,
3168+
**args)
3169+
3170+
29273171
def _get_model_name(section):
29283172
name_string = section['model name']
29293173
parts = name_string.split('@')[0].strip().split()

tests/target_configs.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,7 @@ LinuxTarget:
88
username: 'username'
99
password: 'password'
1010

11+
QEMUTargetRunner:
12+
qemu_params:
13+
kernel_image: '/home/username/devlib/buildroot/output/images/Image'
14+

tests/test_target.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
from unittest import TestCase
2020

21-
from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget
21+
from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget, QEMUTargetRunner
2222
from devlib.utils.android import AdbConnection
2323
from devlib.utils.misc import load_struct_from_yaml
2424

@@ -83,3 +83,10 @@ def run_test(target):
8383
for target in filter(None, targets):
8484
run_test(target)
8585

86+
if target_configs.get('QEMUTargetRunner'):
87+
with QEMUTargetRunner(
88+
qemu_params=target_configs['QEMUTargetRunner']['qemu_params'],
89+
connection_settings=target_configs['QEMUTargetRunner']['connection_settings'],
90+
) as qemu_target:
91+
run_test(qemu_target.target)
92+

0 commit comments

Comments
 (0)