From 918c19ce2c56cb90299bce5591283fd61898d337 Mon Sep 17 00:00:00 2001 From: Metin Kaya Date: Wed, 31 Jan 2024 12:01:52 +0000 Subject: [PATCH 1/5] utils/ssh: Try to free up resources during client creation SshConnection._make_client() may throw exceptions for several reasons (e.g., target is not ready yet). The client should be closed if that is the case. Otherwise Python unittest like tools report resource warning for 'unclosed socket', etc. Signed-off-by: Douglas Raillard Signed-off-by: Metin Kaya --- devlib/utils/ssh.py | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/devlib/utils/ssh.py b/devlib/utils/ssh.py index 4e1d82e4c..a998b2674 100644 --- a/devlib/utils/ssh.py +++ b/devlib/utils/ssh.py @@ -367,25 +367,32 @@ def __init__(self, else: logger.debug('Using SFTP for file transfer') - self.client = self._make_client() - atexit.register(self.close) - - # Use a marker in the output so that we will be able to differentiate - # target connection issues with "password needed". - # Also, sudo might not be installed at all on the target (but - # everything will work as long as we login as root). If sudo is still - # needed, it will explode when someone tries to use it. After all, the - # user might not be interested in being root at all. - self._sudo_needs_password = ( - 'NEED_PASSWORD' in - self.execute( - # sudo -n is broken on some versions on MacOSX, revisit that if - # someone ever cares - 'sudo -n true || echo NEED_PASSWORD', - as_root=False, - check_exit_code=False, + self.client = None + try: + self.client = self._make_client() + atexit.register(self.close) + + # Use a marker in the output so that we will be able to differentiate + # target connection issues with "password needed". + # Also, sudo might not be installed at all on the target (but + # everything will work as long as we login as root). If sudo is still + # needed, it will explode when someone tries to use it. After all, the + # user might not be interested in being root at all. + self._sudo_needs_password = ( + 'NEED_PASSWORD' in + self.execute( + # sudo -n is broken on some versions on MacOSX, revisit that if + # someone ever cares + 'sudo -n true || echo NEED_PASSWORD', + as_root=False, + check_exit_code=False, + ) ) - ) + + except BaseException: + if self.client is not None: + self.client.close() + raise def _make_client(self): if self.strict_host_check: From 3b7fb5b9c2892c4f299a38802f8fa6bc58aea519 Mon Sep 17 00:00:00 2001 From: Metin Kaya Date: Wed, 31 Jan 2024 12:48:31 +0000 Subject: [PATCH 2/5] target: Introduce make_temp() for creating temp file/folder on target ``Target.make_temp()`` employs ``mktemp`` command to create a temporary file or folder. This method will be used in unit tests. Signed-off-by: Metin Kaya --- devlib/target.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/devlib/target.py b/devlib/target.py index 0b3f6ed0e..8e279dd70 100644 --- a/devlib/target.py +++ b/devlib/target.py @@ -14,6 +14,7 @@ # import asyncio +from contextlib import contextmanager import io import base64 import functools @@ -1071,6 +1072,38 @@ async def write_value(self, path, value, verify=True, as_root=True): else: raise + @contextmanager + def make_temp(self, is_directory=True, directory='', prefix='devlib-test'): + """ + Creates temporary file/folder on target and deletes it once it's done. + + :param is_directory: Specifies if temporary object is a directory, defaults to True. + :type is_directory: bool, optional + + :param directory: Temp object will be created under this directory, + defaults to :attr:`Target.working_directory`. + :type directory: str, optional + + :param prefix: Prefix of temp object's name, defaults to 'devlib-test'. + :type prefix: str, optional + + :yield: Full path of temp object. + :rtype: str + """ + + directory = directory or self.working_directory + temp_obj = None + try: + cmd = f'mktemp -p {directory} {prefix}-XXXXXX' + if is_directory: + cmd += ' -d' + + temp_obj = self.execute(cmd).strip() + yield temp_obj + finally: + if temp_obj is not None: + self.remove(temp_obj) + def reset(self): try: self.execute('reboot', as_root=self.needs_su, timeout=2) From 125390319cc3437fbcd505f986f6ddb8594f73f1 Mon Sep 17 00:00:00 2001 From: Metin Kaya Date: Wed, 10 Jan 2024 15:59:42 +0000 Subject: [PATCH 3/5] tests/test_target: Test more targets Test Android and Linux targets as well in addition to LocalLinux target. In order to keep basic verification easy, list complete list of test targets in tests/target_configs.yaml.example and keep the default configuration file for targets simple. Also: - Create a test folder on target's working directory. - Remove all devlib artefacts after execution of the test. - Add logs to show progress of operations. Signed-off-by: Metin Kaya --- tests/target_configs.yaml.example | 17 ++++++++++++ tests/test_target.py | 45 +++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 11 deletions(-) create mode 100644 tests/target_configs.yaml.example diff --git a/tests/target_configs.yaml.example b/tests/target_configs.yaml.example new file mode 100644 index 000000000..c3de592f6 --- /dev/null +++ b/tests/target_configs.yaml.example @@ -0,0 +1,17 @@ +AndroidTarget: + entry-0: + connection_settings: + device: 'emulator-5554' + +LinuxTarget: + entry-0: + connection_settings: + host: 'example.com' + username: 'username' + password: 'password' + +LocalLinuxTarget: + entry-0: + connection_settings: + unrooted: True + diff --git a/tests/test_target.py b/tests/test_target.py index 0fa3fbf43..c07953cb0 100644 --- a/tests/test_target.py +++ b/tests/test_target.py @@ -17,12 +17,11 @@ """Module for testing targets.""" import os -import shutil -import tempfile from pprint import pp import pytest -from devlib import LocalLinuxTarget +from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget +from devlib.utils.android import AdbConnection from devlib.utils.misc import load_struct_from_yaml @@ -37,6 +36,23 @@ def build_targets(): targets = [] + if target_configs.get('AndroidTarget') is not None: + print('> Android targets:') + for entry in target_configs['AndroidTarget'].values(): + pp(entry) + a_target = AndroidTarget( + connection_settings=entry['connection_settings'], + conn_cls=lambda **kwargs: AdbConnection(adb_as_root=True, **kwargs), + ) + targets.append(a_target) + + if target_configs.get('LinuxTarget') is not None: + print('> Linux targets:') + for entry in target_configs['LinuxTarget'].values(): + pp(entry) + l_target = LinuxTarget(connection_settings=entry['connection_settings']) + targets.append(l_target) + if target_configs.get('LocalLinuxTarget') is not None: print('> LocalLinux targets:') for entry in target_configs['LocalLinuxTarget'].values(): @@ -62,15 +78,22 @@ def test_read_multiline_values(target): 'test3': '3\n\n4\n\n', } - tempdir = tempfile.mkdtemp(prefix='devlib-test-') - for key, value in data.items(): - path = os.path.join(tempdir, key) - with open(path, 'w', encoding='utf-8') as wfh: - wfh.write(value) + print(f'target={target.__class__.__name__} os={target.os} hostname={target.hostname}') + + with target.make_temp() as tempdir: + print(f'Created {tempdir}.') + + for key, value in data.items(): + path = os.path.join(tempdir, key) + print(f'Writing {value!r} to {path}...') + target.write_value(path, value, verify=False, + as_root=target.conn.connected_as_root) - raw_result = target.read_tree_values_flat(tempdir) - result = {os.path.basename(k): v for k, v in raw_result.items()} + print('Reading values from target...') + raw_result = target.read_tree_values_flat(tempdir) + result = {os.path.basename(k): v for k, v in raw_result.items()} - shutil.rmtree(tempdir) + print(f'Removing {target.working_directory}...') + target.remove(target.working_directory) assert {k: v.strip() for k, v in data.items()} == result From 82a722db1c7e81a8f0bd755c9dbefbf641f5f815 Mon Sep 17 00:00:00 2001 From: Metin Kaya Date: Fri, 19 Jan 2024 16:15:14 +0000 Subject: [PATCH 4/5] tools/buildroot: Add support for generating Linux target system images Integrate buildroot into devlib in order to ease building kernel and root filesystem images via 'generate-kernel-initrd.sh' helper script. As its name suggests, the script builds kernel image which also includes an initial RAM disk per default config files located under configs//. Provide config files for buildroot and Linux kernel as well as a post-build.sh script which tweaks (e.g., allowing root login on SSH) target's root filesystem. doc/tools.rst talks about details of kernel and rootfs configuration. Signed-off-by: Metin Kaya --- doc/index.rst | 1 + doc/tools.rst | 42 ++++++ tools/buildroot/.gitignore | 1 + .../aarch64/arm-power_aarch64_defconfig | 17 +++ tools/buildroot/configs/aarch64/linux.config | 36 ++++++ tools/buildroot/configs/post-build.sh | 15 +++ .../configs/x86_64/arm-power_x86_64_defconfig | 16 +++ tools/buildroot/configs/x86_64/linux.config | 31 +++++ tools/buildroot/generate-kernel-initrd.sh | 120 ++++++++++++++++++ 9 files changed, 279 insertions(+) create mode 100644 tools/buildroot/.gitignore create mode 100644 tools/buildroot/configs/aarch64/arm-power_aarch64_defconfig create mode 100644 tools/buildroot/configs/aarch64/linux.config create mode 100755 tools/buildroot/configs/post-build.sh create mode 100644 tools/buildroot/configs/x86_64/arm-power_x86_64_defconfig create mode 100644 tools/buildroot/configs/x86_64/linux.config create mode 100755 tools/buildroot/generate-kernel-initrd.sh diff --git a/doc/index.rst b/doc/index.rst index 7888feb2c..ae89bccc1 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -25,6 +25,7 @@ Contents: derived_measurements platform connection + tools Indices and tables ================== diff --git a/doc/tools.rst b/doc/tools.rst index 593152f8d..c2134142c 100644 --- a/doc/tools.rst +++ b/doc/tools.rst @@ -41,3 +41,45 @@ Android emulator: # After ~30 seconds, the emulated device will be ready: adb -s emulator-5554 shell "lsmod" + +Building buildroot +------------------ + +``buildroot/generate-kernel-initrd.sh`` helper script downloads and builds +``buildroot`` per config files located under ``tools/buildroot/configs`` +for the specified architecture. + +The script roughly checks out ``2023.11.1`` tag of ``buildroot``, copies config +files for buildroot (e.g., ``configs/aarch64/arm-power_aarch64_defconfig``) and +kernel (e.g., ``configs/aarch64/linux.config``) to necessary places under +buildroot directory, and runs ``make arm-power_aarch64_defconfig && make`` +commands. + +As its name suggests, ``generate-kernel-initrd.sh`` builds kernel image with an +initial RAM disk per default config files. + +There is also ``post-build.sh`` script in order to make following tunings on +root filesystem generated by ``buildroot``: +- allow root login on SSH. +- increase number of concurrent SSH connections/channels to let devlib + consumers hammering the target system. + +In order to keep rootfs minimal, only OpenSSH and util-linux packages +are enabled in the default configuration files. + +DHCP client and SSH server services are enabled on target system startup. + +SCHED_MC, SCHED_SMT and UCLAMP_TASK scheduler features are enabled for aarch64 +kernel. + +If you need to make changes on ``buildroot``, rootfs or kernel of target +system, you may want to run commands similar to these: + +.. code:: shell + + $ cd tools/buildroot/buildroot-v2023.11.1-aarch64 + $ make menuconfig # or 'make linux-menuconfig' if you want to configure kernel + $ make + +See https://buildroot.org/downloads/manual/manual.html for details. + diff --git a/tools/buildroot/.gitignore b/tools/buildroot/.gitignore new file mode 100644 index 000000000..181163ef3 --- /dev/null +++ b/tools/buildroot/.gitignore @@ -0,0 +1 @@ +buildroot-v2023.11.1-*/ diff --git a/tools/buildroot/configs/aarch64/arm-power_aarch64_defconfig b/tools/buildroot/configs/aarch64/arm-power_aarch64_defconfig new file mode 100644 index 000000000..eaa9752c1 --- /dev/null +++ b/tools/buildroot/configs/aarch64/arm-power_aarch64_defconfig @@ -0,0 +1,17 @@ +BR2_aarch64=y +BR2_cortex_a73_a53=y +BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_MDEV=y +BR2_TARGET_GENERIC_ROOT_PASSWD="root" +BR2_SYSTEM_DHCP="eth0" +BR2_ROOTFS_POST_BUILD_SCRIPT="board/arm-power/post-build.sh" +BR2_LINUX_KERNEL=y +BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y +BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="board/arm-power/aarch64/linux.config" +BR2_LINUX_KERNEL_XZ=y +BR2_PACKAGE_OPENSSH=y +# BR2_PACKAGE_OPENSSH_SANDBOX is not set +BR2_PACKAGE_UTIL_LINUX=y +BR2_PACKAGE_UTIL_LINUX_BINARIES=y +BR2_TARGET_ROOTFS_CPIO_XZ=y +BR2_TARGET_ROOTFS_INITRAMFS=y +# BR2_TARGET_ROOTFS_TAR is not set diff --git a/tools/buildroot/configs/aarch64/linux.config b/tools/buildroot/configs/aarch64/linux.config new file mode 100644 index 000000000..1c91ce751 --- /dev/null +++ b/tools/buildroot/configs/aarch64/linux.config @@ -0,0 +1,36 @@ +CONFIG_SCHED_MC=y +CONFIG_UCLAMP_TASK=y +CONFIG_SCHED_SMT=y +CONFIG_KERNEL_XZ=y +CONFIG_SYSVIPC=y +CONFIG_IKCONFIG=y +CONFIG_IKCONFIG_PROC=y +CONFIG_CGROUPS=y +CONFIG_BLK_DEV_INITRD=y +CONFIG_INITRAMFS_SOURCE="${BR_BINARIES_DIR}/rootfs.cpio" +# CONFIG_RD_GZIP is not set +# CONFIG_RD_BZIP2 is not set +# CONFIG_RD_LZMA is not set +# CONFIG_RD_LZO is not set +# CONFIG_RD_LZ4 is not set +# CONFIG_RD_ZSTD is not set +CONFIG_SMP=y +# CONFIG_GCC_PLUGINS is not set +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_NET=y +CONFIG_PACKET=y +CONFIG_UNIX=y +CONFIG_INET=y +CONFIG_PCI=y +CONFIG_PCI_HOST_GENERIC=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_NETDEVICES=y +CONFIG_VIRTIO_NET=y +CONFIG_INPUT_EVDEV=y +CONFIG_SERIAL_AMBA_PL011=y +CONFIG_SERIAL_AMBA_PL011_CONSOLE=y +CONFIG_VIRTIO_CONSOLE=y +CONFIG_VIRTIO_PCI=y +CONFIG_TMPFS=y diff --git a/tools/buildroot/configs/post-build.sh b/tools/buildroot/configs/post-build.sh new file mode 100755 index 000000000..c73735644 --- /dev/null +++ b/tools/buildroot/configs/post-build.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -eux + +# Enable root login on SSH +sed -i 's/#PermitRootLogin.*/PermitRootLogin yes/' "${TARGET_DIR}/etc/ssh/sshd_config" + +# Increase the number of available channels so that devlib async code can +# exploit concurrency better. +sed -i 's/#MaxSessions.*/MaxSessions 100/' "${TARGET_DIR}/etc/ssh/sshd_config" +sed -i 's/#MaxStartups.*/MaxStartups 100/' "${TARGET_DIR}/etc/ssh/sshd_config" + +# To test Android bindings of ChromeOsTarget +mkdir -p "${TARGET_DIR}/opt/google/containers/android" + diff --git a/tools/buildroot/configs/x86_64/arm-power_x86_64_defconfig b/tools/buildroot/configs/x86_64/arm-power_x86_64_defconfig new file mode 100644 index 000000000..d76b878d7 --- /dev/null +++ b/tools/buildroot/configs/x86_64/arm-power_x86_64_defconfig @@ -0,0 +1,16 @@ +BR2_x86_64=y +BR2_ROOTFS_DEVICE_CREATION_DYNAMIC_MDEV=y +BR2_TARGET_GENERIC_ROOT_PASSWD="root" +BR2_SYSTEM_DHCP="eth0" +BR2_ROOTFS_POST_BUILD_SCRIPT="board/arm-power/post-build.sh" +BR2_LINUX_KERNEL=y +BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG=y +BR2_LINUX_KERNEL_CUSTOM_CONFIG_FILE="board/arm-power/x86_64/linux.config" +BR2_LINUX_KERNEL_XZ=y +BR2_PACKAGE_OPENSSH=y +# BR2_PACKAGE_OPENSSH_SANDBOX is not set +BR2_PACKAGE_UTIL_LINUX=y +BR2_PACKAGE_UTIL_LINUX_BINARIES=y +BR2_TARGET_ROOTFS_CPIO_XZ=y +BR2_TARGET_ROOTFS_INITRAMFS=y +# BR2_TARGET_ROOTFS_TAR is not set diff --git a/tools/buildroot/configs/x86_64/linux.config b/tools/buildroot/configs/x86_64/linux.config new file mode 100644 index 000000000..010cdaca0 --- /dev/null +++ b/tools/buildroot/configs/x86_64/linux.config @@ -0,0 +1,31 @@ +CONFIG_KERNEL_XZ=y +CONFIG_SYSVIPC=y +CONFIG_IKCONFIG=y +CONFIG_IKCONFIG_PROC=y +CONFIG_CGROUPS=y +CONFIG_BLK_DEV_INITRD=y +CONFIG_INITRAMFS_SOURCE="${BR_BINARIES_DIR}/rootfs.cpio" +# CONFIG_RD_GZIP is not set +# CONFIG_RD_BZIP2 is not set +# CONFIG_RD_LZMA is not set +# CONFIG_RD_LZO is not set +# CONFIG_RD_LZ4 is not set +# CONFIG_RD_ZSTD is not set +CONFIG_SMP=y +# CONFIG_GCC_PLUGINS is not set +CONFIG_MODULES=y +CONFIG_MODULE_UNLOAD=y +CONFIG_NET=y +CONFIG_PACKET=y +CONFIG_UNIX=y +CONFIG_INET=y +CONFIG_PCI=y +CONFIG_DEVTMPFS=y +CONFIG_DEVTMPFS_MOUNT=y +CONFIG_NETDEVICES=y +CONFIG_VIRTIO_NET=y +CONFIG_INPUT_EVDEV=y +CONFIG_SERIAL_8250=y +CONFIG_SERIAL_8250_CONSOLE=y +CONFIG_VIRTIO_PCI=y +CONFIG_TMPFS=y diff --git a/tools/buildroot/generate-kernel-initrd.sh b/tools/buildroot/generate-kernel-initrd.sh new file mode 100755 index 000000000..d052d7c6e --- /dev/null +++ b/tools/buildroot/generate-kernel-initrd.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright (C) 2024, ARM Limited and contributors. +# +# 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. +# +# Forked from LISA/tools/lisa-buildroot-create-rootfs. +# + +set -eu + +ARCH="aarch64" +BUILDROOT_URI="git://git.busybox.net/buildroot" +KERNEL_IMAGE_NAME="Image" + +function print_usage +{ + echo "Usage: ${0} [options]" + echo " options:" + echo " -a: set arch (default is aarch64, x86_64 is also supported)" + echo " -p: purge buildroot to force a fresh build" + echo " -h: print this help message" +} + +function set_arch +{ + if [[ "${1}" == "aarch64" ]]; then + return 0 + elif [[ "${1}" == "x86_64" ]]; then + ARCH="x86_64" + KERNEL_IMAGE_NAME="bzImage" + return 0 + fi + + return 1 +} + +while getopts "ahp" opt; do + case ${opt} in + a) + shift + if ! set_arch "${1}"; then + echo "Invalid arch \"${1}\"." + exit 1 + fi + ;; + p) + rm -rf "${BUILDROOT_DIR}" + exit 0 + ;; + h) + print_usage + exit 0 + ;; + *) + print_usage + exit 1 + ;; + esac +done + +# Execute function for once +function do_once +{ + FILE="${BUILDROOT_DIR}/.devlib_${1}" + if [ ! -e "${FILE}" ]; then + eval "${1}" + touch "${FILE}" + fi +} + +function br_clone +{ + git clone -b ${BUILDROOT_VERSION} -v ${BUILDROOT_URI} "${BUILDROOT_DIR}" +} + +function br_apply_config +{ + pushd "${BUILDROOT_DIR}" >/dev/null + + mkdir -p "board/arm-power/${ARCH}/" + cp -f "../configs/post-build.sh" "board/arm-power/" + cp -f "../configs/${ARCH}/arm-power_${ARCH}_defconfig" "configs/" + cp -f "../configs/${ARCH}/linux.config" "board/arm-power/${ARCH}/" + + make "arm-power_${ARCH}_defconfig" + + popd >/dev/null +} + +function br_build +{ + pushd "${BUILDROOT_DIR}" >/dev/null + make + popd >/dev/null +} + + +BUILDROOT_VERSION=${BUILDROOT_VERSION:-"2023.11.1"} +BUILDROOT_DIR="$(dirname "$0")/buildroot-v${BUILDROOT_VERSION}-${ARCH}" + +do_once br_clone + +do_once br_apply_config + +br_build + +echo "Kernel image \"${BUILDROOT_DIR}/output/images/${KERNEL_IMAGE_NAME}\" is ready." From a4b3547d8df6247544b25c81f60af1addb9f6eff Mon Sep 17 00:00:00 2001 From: Metin Kaya Date: Mon, 15 Jan 2024 12:53:45 +0000 Subject: [PATCH 5/5] 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 --- devlib/__init__.py | 2 + devlib/target.py | 2 +- devlib/target_runner.py | 267 ++++++++++++++++++++++++++++++ tests/target_configs.yaml.example | 13 ++ tests/test_target.py | 33 +++- 5 files changed, 310 insertions(+), 7 deletions(-) create mode 100644 devlib/target_runner.py diff --git a/devlib/__init__.py b/devlib/__init__.py index e496299b1..dceda0d62 100644 --- a/devlib/__init__.py +++ b/devlib/__init__.py @@ -22,6 +22,8 @@ ChromeOsTarget, ) +from devlib.target_runner import QEMUTargetRunner + from devlib.host import ( PACKAGE_BIN_DIRECTORY, LocalConnection, diff --git a/devlib/target.py b/devlib/target.py index 8e279dd70..2bd4d85ac 100644 --- a/devlib/target.py +++ b/devlib/target.py @@ -494,7 +494,7 @@ async def setup(self, executables=None): # Check for platform dependent setup procedures 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') await self.execute.asyn('mkdir -p {}'.format(quote(self._file_transfer_cache))) diff --git a/devlib/target_runner.py b/devlib/target_runner.py new file mode 100644 index 000000000..c08c3a1ab --- /dev/null +++ b/devlib/target_runner.py @@ -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) diff --git a/tests/target_configs.yaml.example b/tests/target_configs.yaml.example index c3de592f6..6ad859a8c 100644 --- a/tests/target_configs.yaml.example +++ b/tests/target_configs.yaml.example @@ -15,3 +15,16 @@ LocalLinuxTarget: connection_settings: 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' diff --git a/tests/test_target.py b/tests/test_target.py index c07953cb0..63f806f82 100644 --- a/tests/test_target.py +++ b/tests/test_target.py @@ -20,7 +20,7 @@ from pprint import pp import pytest -from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget +from devlib import AndroidTarget, LinuxTarget, LocalLinuxTarget, QEMUTargetRunner from devlib.utils.android import AdbConnection from devlib.utils.misc import load_struct_from_yaml @@ -44,32 +44,49 @@ def build_targets(): connection_settings=entry['connection_settings'], 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: print('> Linux targets:') for entry in target_configs['LinuxTarget'].values(): pp(entry) 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: print('> LocalLinux targets:') for entry in target_configs['LocalLinuxTarget'].values(): pp(entry) 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 -@pytest.mark.parametrize("target", build_targets()) -def test_read_multiline_values(target): +@pytest.mark.parametrize("target, target_runner", build_targets()) +def test_read_multiline_values(target, target_runner): """ Test Target.read_tree_values_flat() :param target: Type of target per :class:`Target` based classes. :type target: Target + + :param target_runner: Target runner object to terminate target (if necessary). + :type target: TargetRunner """ data = { @@ -96,4 +113,8 @@ def test_read_multiline_values(target): print(f'Removing {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