Skip to content

Commit 28ac822

Browse files
committed
target: Implement QEMU Linux target
Add support for launching Linux targets on QEMU. This requires having buildroot and QEMU packages installed on host machine. Newly introduced QEMULinuxTarget class is a simple wrapper around LinuxTarget: - Perform sanity checks to ensure QEMU executable, rootfs and kernel images exist. - Spin QEMU process to launch a Linux guest and connect it to over SSH. Also added a test case (tests/spin_targets.py) to ensure devlib can launch a QEMU target and run some basic commands on it. Signed-off-by: Metin Kaya <metin.kaya@arm.com>
1 parent 88955df commit 28ac822

File tree

5 files changed

+152
-3
lines changed

5 files changed

+152
-3
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, QEMULinuxTarget,
2323
)
2424

2525
from devlib.host import (

devlib/target.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,20 @@
1414
#
1515

1616
import asyncio
17+
import atexit
1718
import io
1819
import base64
1920
import functools
2021
import gzip
2122
import glob
2223
import os
2324
import re
25+
import sys
2426
import time
2527
import logging
2628
import posixpath
29+
import select
30+
import signal
2731
import subprocess
2832
import tarfile
2933
import tempfile
@@ -38,6 +42,7 @@
3842
from past.types import basestring
3943
from numbers import Number
4044
from shlex import quote
45+
from platform import machine
4146
try:
4247
from collections.abc import Mapping
4348
except ImportError:
@@ -56,7 +61,7 @@
5661
from devlib.utils.ssh import SshConnection
5762
from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, INTENT_FLAGS
5863
from devlib.utils.misc import memoized, isiterable, convert_new_lines, groupby_value
59-
from devlib.utils.misc import commonprefix, merge_lists
64+
from devlib.utils.misc import check_output, get_subprocess, commonprefix, merge_lists, which
6065
from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list
6166
from devlib.utils.misc import batch_contextmanager, tls_property, _BoundTLSProperty, nullcontext
6267
from devlib.utils.misc import safe_extract
@@ -2892,6 +2897,95 @@ def _resolve_paths(self):
28922897
if self.executables_directory is None:
28932898
self.executables_directory = '/tmp/devlib-target/bin'
28942899

2900+
class QEMULinuxTarget(LinuxTarget):
2901+
'''
2902+
Class for launching Linux target on QEMU.
2903+
'''
2904+
2905+
# pylint: disable=too-many-locals,too-many-arguments
2906+
def __init__(self,
2907+
kernel_image,
2908+
arch='aarch64',
2909+
cpu_types='-cpu cortex-a72',
2910+
initrd_image=str(),
2911+
mem_size=512,
2912+
num_cores=2,
2913+
num_threads=2,
2914+
cmdline='console=ttyAMA0',
2915+
enable_kvm=True,
2916+
boot_timeout=60,
2917+
banner='Welcome to Buildroot',
2918+
connect=True,
2919+
connection_settings=None,
2920+
**kwargs):
2921+
super().__init__(connect=False,
2922+
conn_cls=SshConnection,
2923+
connection_settings=connection_settings,
2924+
**kwargs)
2925+
2926+
qemu_executable = f'qemu-system-{arch}'
2927+
qemu_path = which(qemu_executable)
2928+
if qemu_path is None:
2929+
raise FileNotFoundError(f'Cannot find {qemu_executable} executable!')
2930+
2931+
if not os.path.exists(kernel_image):
2932+
raise FileNotFoundError(f'{kernel_image} does not exist!')
2933+
2934+
qemu_cmd = f'{qemu_path} -kernel {quote(kernel_image)} -append "{quote(cmdline)}" ' \
2935+
f'-smp cores={num_cores},threads={num_threads} -m {mem_size} ' \
2936+
f'-netdev user,id=net0,hostfwd=tcp::{connection_settings["port"]}-:22 ' \
2937+
'-device virtio-net-pci,netdev=net0 --nographic'
2938+
2939+
if initrd_image:
2940+
if not os.path.exists(initrd_image):
2941+
raise FileNotFoundError(f'{initrd_image} does not exist!')
2942+
qemu_cmd = f'{qemu_cmd} -initrd {quote(initrd_image)}'
2943+
2944+
if arch == machine():
2945+
qemu_cmd = f'{qemu_cmd} {"--enable-kvm" if enable_kvm else ""}'
2946+
else:
2947+
qemu_cmd = f'{qemu_cmd} -machine virt {cpu_types}'
2948+
2949+
self.logger.info('qemu_cmd: %s', qemu_cmd)
2950+
2951+
self.banner = banner
2952+
2953+
try:
2954+
self.qemu_process = get_subprocess(qemu_cmd, shell=True)
2955+
except Exception as ex:
2956+
raise HostError(f'Error while running "{qemu_cmd}": {ex}') from ex
2957+
2958+
atexit.register(self.terminate_qemu)
2959+
2960+
self.wait_boot_complete(timeout=boot_timeout)
2961+
2962+
if connect:
2963+
super().connect(check_boot_completed=False)
2964+
2965+
def wait_boot_complete(self, timeout=10):
2966+
start_time = time.time()
2967+
boot_completed = False
2968+
poll_obj = select.poll()
2969+
poll_obj.register(self.qemu_process.stdout, select.POLLIN)
2970+
2971+
while not boot_completed and timeout >= (time.time() - start_time):
2972+
poll_result = poll_obj.poll(0)
2973+
if poll_result:
2974+
line = self.qemu_process.stdout.readline().rstrip(b'\r\n')
2975+
line = line.decode(sys.stdout.encoding or 'utf-8', "replace") if line else ''
2976+
self.logger.debug(line)
2977+
if self.banner in line:
2978+
self.logger.info('Target is ready.')
2979+
boot_completed = True
2980+
break
2981+
2982+
if not boot_completed:
2983+
raise TargetStableError(f'Target could not finish boot up in {timeout} seconds!')
2984+
2985+
def terminate_qemu(self):
2986+
self.logger.debug('Terminating QEMU...')
2987+
os.killpg(self.qemu_process.pid, signal.SIGTERM)
2988+
28952989

28962990
def _get_model_name(section):
28972991
name_string = section['model name']

doc/overview.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ There are currently four target interfaces:
99
- :class:`~devlib.target.ChromeOsTarget`: for interacting with ChromeOS devices
1010
over SSH, and their Android containers over adb.
1111
- :class:`~devlib.target.LocalLinuxTarget`: for interacting with the local Linux host.
12+
- :class:`~devlib.target.QEMULinuxTarget`: for interacting with an emulated Linux
13+
target on QEMU.
1214

1315
They all work in more-or-less the same way, with the major difference being in
1416
how connection settings are specified; though there may also be a few APIs

doc/target.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,6 +780,50 @@ Android Target
780780
support the majority of newer devices.
781781

782782

783+
QEMU Linux Target
784+
---------------
785+
786+
.. class:: QEMULinuxTarget(kernel_image, 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', connect=True, connection_settings=None)
787+
788+
:class:`QEMULinuxTarget` is a subclass of :class:`LinuxTarget` with
789+
additional features necessary for launching an emulated Linux guest on QEMU.
790+
It implements :meth:`wait_boot_complete` to ensure spinned guest can show
791+
login prompt in :param:`boot_timeout` seconds.
792+
793+
:param kernel_image: This is the location of kernel image
794+
(e.g., ``bzImage``) which will be used as target's kernel.
795+
796+
:param arch: Architecture type. Defaults to ``aarch64``.
797+
798+
:param cpu_types: List of CPU ids for QEMU. Defaults to ``-cpu cortex-a72``.
799+
Valid only for Arm architectures.
800+
801+
:param initrd_image: This is an optional parameter which points to the
802+
location of initrd image (e.g., ``rootfs.cpio.xz``) which will be used
803+
as target's root filesystem if kernel does not include one already.
804+
805+
:param mem_size: Size of guest memory in MiB.
806+
807+
:param num_cores: Number of CPU cores. Guest will have ``2`` cores in
808+
default settings.
809+
810+
:param num_threads: Number of CPU threads. Set to ``2`` by defaults.
811+
812+
:param cmdline: Kernel command line parameter. It only specifies console
813+
device in default (i.e., ``console=ttyAMA0``) which is valid for Arm
814+
architectures. It should be changed to ``ttyS0`` for x86 platforms.
815+
816+
:param enable_kvm: Specifies if KVM will be used as accelerator in QEMU
817+
or not. Valid only for x86 architectures. Enabled by default for
818+
improving QEMU performance.
819+
820+
:param boot_timeout: Timeout for login prompt of guest in seconds.
821+
It's set to ``60`` seconds by default.
822+
823+
:param banner: This is the system banner displayed at login which is set to
824+
``Welcome to Buildroot`` by default.
825+
826+
783827
ChromeOS Target
784828
---------------
785829

tests/spin_targets.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import os
2424
from unittest import TestCase
25-
from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget
25+
from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget, QEMULinuxTarget
2626
from devlib.utils.misc import get_random_string
2727
from devlib.exception import TargetStableError
2828

@@ -87,6 +87,15 @@ def create_targets():
8787
working_directory='/tmp/devlib-target')
8888
targets.append(ll_target)
8989

90+
q_target = QEMULinuxTarget(kernel_image='/home/user/devlib/tools/buildroot/buildroot-v2023.11.1/output/images/Image',
91+
connection_settings={'host': '127.0.0.1',
92+
'port': 8022,
93+
'username': 'root',
94+
'password': 'root',
95+
'strict_host_check': False},
96+
working_directory='/tmp/devlib-target')
97+
targets.append(q_target)
98+
9099
return targets
91100

92101
method_name = self.__class__.test_read_multiline_values.__qualname__

0 commit comments

Comments
 (0)