|
25 | 25 | import time |
26 | 26 | import logging |
27 | 27 | import posixpath |
| 28 | +import signal |
28 | 29 | import subprocess |
29 | 30 | import tarfile |
30 | 31 | import tempfile |
|
39 | 40 | from past.types import basestring |
40 | 41 | from numbers import Number |
41 | 42 | from shlex import quote |
| 43 | +from platform import machine |
42 | 44 | try: |
43 | 45 | from collections.abc import Mapping |
44 | 46 | except ImportError: |
|
57 | 59 | from devlib.utils.ssh import SshConnection |
58 | 60 | from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, INTENT_FLAGS |
59 | 61 | 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 |
61 | 63 | from devlib.utils.misc import ABI_MAP, get_cpu_name, ranges_to_list |
62 | 64 | from devlib.utils.misc import batch_contextmanager, tls_property, _BoundTLSProperty, nullcontext |
63 | 65 | from devlib.utils.misc import safe_extract |
@@ -494,7 +496,7 @@ async def setup(self, executables=None): |
494 | 496 | # Check for platform dependent setup procedures |
495 | 497 | self.platform.setup(self) |
496 | 498 |
|
497 | | - # Initialize modules which requires Buxybox (e.g. shutil dependent tasks) |
| 499 | + # Initialize modules which requires Busybox (e.g. shutil dependent tasks) |
498 | 500 | self._update_modules('setup') |
499 | 501 |
|
500 | 502 | await self.execute.asyn('mkdir -p {}'.format(quote(self._file_transfer_cache))) |
@@ -2924,6 +2926,248 @@ def _resolve_paths(self): |
2924 | 2926 | self.executables_directory = '/tmp/devlib-target/bin' |
2925 | 2927 |
|
2926 | 2928 |
|
| 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 | + |
2927 | 3171 | def _get_model_name(section): |
2928 | 3172 | name_string = section['model name'] |
2929 | 3173 | parts = name_string.split('@')[0].strip().split() |
|
0 commit comments