mirror of
https://github.com/ARM-software/devlib.git
synced 2025-01-31 02:00:45 +00:00
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>
This commit is contained in:
parent
1431bebd80
commit
228baeb317
@ -22,6 +22,8 @@ from devlib.target import (
|
|||||||
ChromeOsTarget,
|
ChromeOsTarget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from devlib.target_runner import QEMUTargetRunner
|
||||||
|
|
||||||
from devlib.host import (
|
from devlib.host import (
|
||||||
PACKAGE_BIN_DIRECTORY,
|
PACKAGE_BIN_DIRECTORY,
|
||||||
LocalConnection,
|
LocalConnection,
|
||||||
|
@ -494,7 +494,7 @@ class Target(object):
|
|||||||
# Check for platform dependent setup procedures
|
# Check for platform dependent setup procedures
|
||||||
self.platform.setup(self)
|
self.platform.setup(self)
|
||||||
|
|
||||||
# Initialize modules which requires Buxybox (e.g. shutil dependent tasks)
|
# Initialize modules which requires Busybox (e.g. shutil dependent tasks)
|
||||||
self._update_modules('setup')
|
self._update_modules('setup')
|
||||||
|
|
||||||
await self.execute.asyn('mkdir -p {}'.format(quote(self._file_transfer_cache)))
|
await self.execute.asyn('mkdir -p {}'.format(quote(self._file_transfer_cache)))
|
||||||
|
267
devlib/target_runner.py
Normal file
267
devlib/target_runner.py
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
# Copyright 2024 ARM Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
"""
|
||||||
|
Target runner and related classes are implemented here.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from platform import machine
|
||||||
|
|
||||||
|
from devlib.exception import (TargetStableError, HostError)
|
||||||
|
from devlib.target import LinuxTarget
|
||||||
|
from devlib.utils.misc import get_subprocess, which
|
||||||
|
from devlib.utils.ssh import SshConnection
|
||||||
|
|
||||||
|
|
||||||
|
class TargetRunner:
|
||||||
|
"""
|
||||||
|
A generic class for interacting with targets runners.
|
||||||
|
|
||||||
|
It mainly aims to provide framework support for QEMU like target runners
|
||||||
|
(e.g., :class:`QEMUTargetRunner`).
|
||||||
|
|
||||||
|
:param runner_cmd: The command to start runner process (e.g.,
|
||||||
|
``qemu-system-aarch64 -kernel Image -append "console=ttyAMA0" ...``).
|
||||||
|
:type runner_cmd: str
|
||||||
|
|
||||||
|
:param target: Specifies type of target per :class:`Target` based classes.
|
||||||
|
:type target: Target
|
||||||
|
|
||||||
|
:param connect: Specifies if :class:`TargetRunner` should try to connect
|
||||||
|
target after launching it, defaults to True.
|
||||||
|
:type connect: bool, optional
|
||||||
|
|
||||||
|
:param boot_timeout: Timeout for target's being ready for SSH access in
|
||||||
|
seconds, defaults to 60.
|
||||||
|
:type boot_timeout: int, optional
|
||||||
|
|
||||||
|
:raises HostError: if it cannot execute runner command successfully.
|
||||||
|
|
||||||
|
:raises TargetStableError: if Target is inaccessible.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
runner_cmd,
|
||||||
|
target,
|
||||||
|
connect=True,
|
||||||
|
boot_timeout=60):
|
||||||
|
self.boot_timeout = boot_timeout
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
self.logger = logging.getLogger(self.__class__.__name__)
|
||||||
|
|
||||||
|
self.logger.info('runner_cmd: %s', runner_cmd)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.runner_process = get_subprocess(list(runner_cmd.split()))
|
||||||
|
except Exception as ex:
|
||||||
|
raise HostError(f'Error while running "{runner_cmd}": {ex}') from ex
|
||||||
|
|
||||||
|
if connect:
|
||||||
|
self.wait_boot_complete()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
"""
|
||||||
|
Complementary method for contextmanager.
|
||||||
|
|
||||||
|
:return: Self object.
|
||||||
|
:rtype: TargetRunner
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_):
|
||||||
|
"""
|
||||||
|
Exit routine for contextmanager.
|
||||||
|
|
||||||
|
Ensure :attr:`TargetRunner.runner_process` is terminated on exit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.terminate()
|
||||||
|
|
||||||
|
def wait_boot_complete(self):
|
||||||
|
"""
|
||||||
|
Wait for target OS to finish boot up and become accessible over SSH in at most
|
||||||
|
:attr:`TargetRunner.boot_timeout` seconds.
|
||||||
|
|
||||||
|
:raises TargetStableError: In case of timeout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
elapsed = 0
|
||||||
|
while self.boot_timeout >= elapsed:
|
||||||
|
try:
|
||||||
|
self.target.connect(timeout=self.boot_timeout - elapsed)
|
||||||
|
self.logger.info('Target is ready.')
|
||||||
|
return
|
||||||
|
# pylint: disable=broad-except
|
||||||
|
except BaseException as ex:
|
||||||
|
self.logger.info('Cannot connect target: %s', ex)
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
|
||||||
|
self.terminate()
|
||||||
|
raise TargetStableError(f'Target is inaccessible for {self.boot_timeout} seconds!')
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
"""
|
||||||
|
Terminate :attr:`TargetRunner.runner_process`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self.runner_process is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.runner_process.stdin.close()
|
||||||
|
self.runner_process.stdout.close()
|
||||||
|
self.runner_process.stderr.close()
|
||||||
|
|
||||||
|
if self.runner_process.poll() is None:
|
||||||
|
self.logger.debug('Terminating target runner...')
|
||||||
|
os.killpg(self.runner_process.pid, signal.SIGTERM)
|
||||||
|
# Wait 3 seconds before killing the runner.
|
||||||
|
self.runner_process.wait(timeout=3)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
self.logger.info('Killing target runner...')
|
||||||
|
os.killpg(self.runner_process.pid, signal.SIGKILL)
|
||||||
|
|
||||||
|
|
||||||
|
class QEMUTargetRunner(TargetRunner):
|
||||||
|
"""
|
||||||
|
Class for interacting with QEMU runners.
|
||||||
|
|
||||||
|
:class:`QEMUTargetRunner` is a subclass of :class:`TargetRunner` which performs necessary
|
||||||
|
groundwork for launching a guest OS on QEMU.
|
||||||
|
|
||||||
|
:param qemu_settings: 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., ``Image``) which
|
||||||
|
will be used as target's kernel.
|
||||||
|
|
||||||
|
* ``arch``: Architecture type. Defaults to ``aarch64``.
|
||||||
|
|
||||||
|
* ``cpu_types``: List of CPU ids for QEMU. The list only contains ``cortex-a72`` by
|
||||||
|
default. This parameter is valid for Arm architectures only.
|
||||||
|
|
||||||
|
* ``initrd_image``: This 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 by default.
|
||||||
|
|
||||||
|
* ``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.
|
||||||
|
May be changed to ``ttyS0`` for x86 platforms.
|
||||||
|
|
||||||
|
* ``enable_kvm``: Specifies if KVM will be used as accelerator in QEMU or not.
|
||||||
|
Enabled by default if host architecture matches with target's for improving
|
||||||
|
QEMU performance.
|
||||||
|
:type qemu_settings: Dict
|
||||||
|
|
||||||
|
:param connection_settings: the dictionary to store connection settings
|
||||||
|
of :attr:`Target.connection_settings`, defaults to None.
|
||||||
|
:type connection_settings: Dict, optional
|
||||||
|
|
||||||
|
:param make_target: Lambda function for creating :class:`Target` based
|
||||||
|
object, defaults to :func:`lambda **kwargs: LinuxTarget(**kwargs)`.
|
||||||
|
:type make_target: func, optional
|
||||||
|
|
||||||
|
:Variable positional arguments: Forwarded to :class:`TargetRunner`.
|
||||||
|
|
||||||
|
:raises FileNotFoundError: if QEMU executable, kernel or initrd image cannot be found.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
qemu_settings,
|
||||||
|
connection_settings=None,
|
||||||
|
# pylint: disable=unnecessary-lambda
|
||||||
|
make_target=lambda **kwargs: LinuxTarget(**kwargs),
|
||||||
|
**args):
|
||||||
|
self.connection_settings = {
|
||||||
|
'host': '127.0.0.1',
|
||||||
|
'port': 8022,
|
||||||
|
'username': 'root',
|
||||||
|
'password': 'root',
|
||||||
|
'strict_host_check': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if connection_settings is not None:
|
||||||
|
self.connection_settings = self.connection_settings | connection_settings
|
||||||
|
|
||||||
|
qemu_args = {
|
||||||
|
'kernel_image': '',
|
||||||
|
'arch': 'aarch64',
|
||||||
|
'cpu_type': 'cortex-a72',
|
||||||
|
'initrd_image': '',
|
||||||
|
'mem_size': 512,
|
||||||
|
'num_cores': 2,
|
||||||
|
'num_threads': 2,
|
||||||
|
'cmdline': 'console=ttyAMA0',
|
||||||
|
'enable_kvm': True,
|
||||||
|
}
|
||||||
|
|
||||||
|
qemu_args = qemu_args | qemu_settings
|
||||||
|
|
||||||
|
qemu_executable = f'qemu-system-{qemu_args["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_args["kernel_image"]):
|
||||||
|
raise FileNotFoundError(f'{qemu_args["kernel_image"]} does not exist!')
|
||||||
|
|
||||||
|
# pylint: disable=consider-using-f-string
|
||||||
|
qemu_cmd = '''\
|
||||||
|
{} -kernel {} -append "{}" -m {} -smp cores={},threads={} -netdev user,id=net0,hostfwd=tcp::{}-:22 \
|
||||||
|
-device virtio-net-pci,netdev=net0 --nographic\
|
||||||
|
'''.format(
|
||||||
|
qemu_path,
|
||||||
|
qemu_args["kernel_image"],
|
||||||
|
qemu_args["cmdline"],
|
||||||
|
qemu_args["mem_size"],
|
||||||
|
qemu_args["num_cores"],
|
||||||
|
qemu_args["num_threads"],
|
||||||
|
self.connection_settings["port"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if qemu_args["initrd_image"]:
|
||||||
|
if not os.path.exists(qemu_args["initrd_image"]):
|
||||||
|
raise FileNotFoundError(f'{qemu_args["initrd_image"]} does not exist!')
|
||||||
|
|
||||||
|
qemu_cmd += f' -initrd {qemu_args["initrd_image"]}'
|
||||||
|
|
||||||
|
if qemu_args["arch"] == machine():
|
||||||
|
if qemu_args["enable_kvm"]:
|
||||||
|
qemu_cmd += ' --enable-kvm'
|
||||||
|
else:
|
||||||
|
qemu_cmd += f' -machine virt -cpu {qemu_args["cpu_type"]}'
|
||||||
|
|
||||||
|
self.target = make_target(connect=False,
|
||||||
|
conn_cls=SshConnection,
|
||||||
|
connection_settings=self.connection_settings)
|
||||||
|
|
||||||
|
super().__init__(runner_cmd=qemu_cmd,
|
||||||
|
target=self.target,
|
||||||
|
**args)
|
@ -15,3 +15,16 @@ LocalLinuxTarget:
|
|||||||
connection_settings:
|
connection_settings:
|
||||||
unrooted: True
|
unrooted: True
|
||||||
|
|
||||||
|
QEMUTargetRunner:
|
||||||
|
entry-0:
|
||||||
|
qemu_settings:
|
||||||
|
kernel_image: '/path/to/devlib/tools/buildroot/buildroot-v2023.11.1-aarch64/output/images/Image'
|
||||||
|
|
||||||
|
entry-1:
|
||||||
|
connection_settings:
|
||||||
|
port : 8023
|
||||||
|
|
||||||
|
qemu_settings:
|
||||||
|
kernel_image: '/path/to/devlib/tools/buildroot/buildroot-v2023.11.1-x86_64/output/images/bzImage'
|
||||||
|
arch: 'x86_64'
|
||||||
|
cmdline: 'console=ttyS0'
|
||||||
|
@ -20,7 +20,7 @@ import os
|
|||||||
from pprint import pp
|
from pprint import pp
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget
|
from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget, QEMUTargetRunner
|
||||||
from devlib.utils.android import AdbConnection
|
from devlib.utils.android import AdbConnection
|
||||||
from devlib.utils.misc import load_struct_from_yaml
|
from devlib.utils.misc import load_struct_from_yaml
|
||||||
|
|
||||||
@ -44,32 +44,49 @@ def build_targets():
|
|||||||
connection_settings=entry['connection_settings'],
|
connection_settings=entry['connection_settings'],
|
||||||
conn_cls=lambda **kwargs: AdbConnection(adb_as_root=True, **kwargs),
|
conn_cls=lambda **kwargs: AdbConnection(adb_as_root=True, **kwargs),
|
||||||
)
|
)
|
||||||
targets.append(a_target)
|
targets.append((a_target, None))
|
||||||
|
|
||||||
if target_configs.get('LinuxTarget') is not None:
|
if target_configs.get('LinuxTarget') is not None:
|
||||||
print('> Linux targets:')
|
print('> Linux targets:')
|
||||||
for entry in target_configs['LinuxTarget'].values():
|
for entry in target_configs['LinuxTarget'].values():
|
||||||
pp(entry)
|
pp(entry)
|
||||||
l_target = LinuxTarget(connection_settings=entry['connection_settings'])
|
l_target = LinuxTarget(connection_settings=entry['connection_settings'])
|
||||||
targets.append(l_target)
|
targets.append((l_target, None))
|
||||||
|
|
||||||
if target_configs.get('LocalLinuxTarget') is not None:
|
if target_configs.get('LocalLinuxTarget') is not None:
|
||||||
print('> LocalLinux targets:')
|
print('> LocalLinux targets:')
|
||||||
for entry in target_configs['LocalLinuxTarget'].values():
|
for entry in target_configs['LocalLinuxTarget'].values():
|
||||||
pp(entry)
|
pp(entry)
|
||||||
ll_target = LocalLinuxTarget(connection_settings=entry['connection_settings'])
|
ll_target = LocalLinuxTarget(connection_settings=entry['connection_settings'])
|
||||||
targets.append(ll_target)
|
targets.append((ll_target, None))
|
||||||
|
|
||||||
|
if target_configs.get('QEMUTargetRunner') is not None:
|
||||||
|
print('> QEMU target runners:')
|
||||||
|
for entry in target_configs['QEMUTargetRunner'].values():
|
||||||
|
pp(entry)
|
||||||
|
qemu_settings = entry.get('qemu_settings') and entry['qemu_settings']
|
||||||
|
connection_settings = entry.get(
|
||||||
|
'connection_settings') and entry['connection_settings']
|
||||||
|
|
||||||
|
qemu_runner = QEMUTargetRunner(
|
||||||
|
qemu_settings=qemu_settings,
|
||||||
|
connection_settings=connection_settings,
|
||||||
|
)
|
||||||
|
targets.append((qemu_runner.target, qemu_runner))
|
||||||
|
|
||||||
return targets
|
return targets
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("target", build_targets())
|
@pytest.mark.parametrize("target, target_runner", build_targets())
|
||||||
def test_read_multiline_values(target):
|
def test_read_multiline_values(target, target_runner):
|
||||||
"""
|
"""
|
||||||
Test Target.read_tree_values_flat()
|
Test Target.read_tree_values_flat()
|
||||||
|
|
||||||
:param target: Type of target per :class:`Target` based classes.
|
:param target: Type of target per :class:`Target` based classes.
|
||||||
:type target: Target
|
:type target: Target
|
||||||
|
|
||||||
|
:param target_runner: Target runner object to terminate target (if necessary).
|
||||||
|
:type target: TargetRunner
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@ -96,4 +113,8 @@ def test_read_multiline_values(target):
|
|||||||
print(f'Removing {target.working_directory}...')
|
print(f'Removing {target.working_directory}...')
|
||||||
target.remove(target.working_directory)
|
target.remove(target.working_directory)
|
||||||
|
|
||||||
|
if target_runner is not None:
|
||||||
|
print('Terminating target runner...')
|
||||||
|
target_runner.terminate()
|
||||||
|
|
||||||
assert {k: v.strip() for k, v in data.items()} == result
|
assert {k: v.strip() for k, v in data.items()} == result
|
||||||
|
Loading…
x
Reference in New Issue
Block a user