|
| 1 | +# Copyright 2024 ARM Limited |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +# |
| 15 | + |
| 16 | +""" |
| 17 | +Target runner and related classes are implemented here. |
| 18 | +""" |
| 19 | + |
| 20 | +import logging |
| 21 | +import os |
| 22 | +import signal |
| 23 | +import subprocess |
| 24 | +import time |
| 25 | +from platform import machine |
| 26 | + |
| 27 | +from devlib.exception import (TargetStableError, HostError) |
| 28 | +from devlib.target import LinuxTarget |
| 29 | +from devlib.utils.misc import get_subprocess, which |
| 30 | +from devlib.utils.ssh import SshConnection |
| 31 | + |
| 32 | + |
| 33 | +class TargetRunner: |
| 34 | + """ |
| 35 | + A generic class for interacting with targets runners. |
| 36 | +
|
| 37 | + It mainly aims to provide framework support for QEMU like target runners |
| 38 | + (e.g., :class:`QEMUTargetRunner`). |
| 39 | +
|
| 40 | + :param runner_cmd: The command to start runner process (e.g., |
| 41 | + ``qemu-system-aarch64 -kernel Image -append "console=ttyAMA0" ...``). |
| 42 | + :type runner_cmd: str |
| 43 | +
|
| 44 | + :param target: Specifies type of target per :class:`Target` based classes. |
| 45 | + :type target: Target |
| 46 | +
|
| 47 | + :param connect: Specifies if :class:`TargetRunner` should try to connect |
| 48 | + target after launching it, defaults to True. |
| 49 | + :type connect: bool, optional |
| 50 | +
|
| 51 | + :param boot_timeout: Timeout for target's being ready for SSH access in |
| 52 | + seconds, defaults to 60. |
| 53 | + :type boot_timeout: int, optional |
| 54 | +
|
| 55 | + :raises HostError: if it cannot execute runner command successfully. |
| 56 | +
|
| 57 | + :raises TargetStableError: if Target is inaccessible. |
| 58 | + """ |
| 59 | + |
| 60 | + def __init__(self, |
| 61 | + runner_cmd, |
| 62 | + target, |
| 63 | + connect=True, |
| 64 | + boot_timeout=60): |
| 65 | + self.boot_timeout = boot_timeout |
| 66 | + self.target = target |
| 67 | + |
| 68 | + self.logger = logging.getLogger(self.__class__.__name__) |
| 69 | + |
| 70 | + self.logger.info('runner_cmd: %s', runner_cmd) |
| 71 | + |
| 72 | + try: |
| 73 | + self.runner_process = get_subprocess(list(runner_cmd.split())) |
| 74 | + except Exception as ex: |
| 75 | + raise HostError(f'Error while running "{runner_cmd}": {ex}') from ex |
| 76 | + |
| 77 | + if connect: |
| 78 | + self.wait_boot_complete() |
| 79 | + |
| 80 | + def __enter__(self): |
| 81 | + """ |
| 82 | + Complementary method for contextmanager. |
| 83 | +
|
| 84 | + :return: Self object. |
| 85 | + :rtype: TargetRunner |
| 86 | + """ |
| 87 | + |
| 88 | + return self |
| 89 | + |
| 90 | + def __exit__(self, *_): |
| 91 | + """ |
| 92 | + Exit routine for contextmanager. |
| 93 | +
|
| 94 | + Ensure :attr:`TargetRunner.runner_process` is terminated on exit. |
| 95 | + """ |
| 96 | + |
| 97 | + self.terminate() |
| 98 | + |
| 99 | + def wait_boot_complete(self): |
| 100 | + """ |
| 101 | + Wait for target OS to finish boot up and become accessible over SSH in at most |
| 102 | + :attr:`TargetRunner.boot_timeout` seconds. |
| 103 | +
|
| 104 | + :raises TargetStableError: In case of timeout. |
| 105 | + """ |
| 106 | + |
| 107 | + start_time = time.time() |
| 108 | + elapsed = 0 |
| 109 | + while self.boot_timeout >= elapsed: |
| 110 | + try: |
| 111 | + self.target.connect(timeout=self.boot_timeout - elapsed) |
| 112 | + self.logger.info('Target is ready.') |
| 113 | + return |
| 114 | + # pylint: disable=broad-except |
| 115 | + except BaseException as ex: |
| 116 | + self.logger.info('Cannot connect target: %s', ex) |
| 117 | + |
| 118 | + time.sleep(1) |
| 119 | + elapsed = time.time() - start_time |
| 120 | + |
| 121 | + self.terminate() |
| 122 | + raise TargetStableError(f'Target is inaccessible for {self.boot_timeout} seconds!') |
| 123 | + |
| 124 | + def terminate(self): |
| 125 | + """ |
| 126 | + Terminate :attr:`TargetRunner.runner_process`. |
| 127 | + """ |
| 128 | + |
| 129 | + if self.runner_process is None: |
| 130 | + return |
| 131 | + |
| 132 | + try: |
| 133 | + self.runner_process.stdin.close() |
| 134 | + self.runner_process.stdout.close() |
| 135 | + self.runner_process.stderr.close() |
| 136 | + |
| 137 | + if self.runner_process.poll() is None: |
| 138 | + self.logger.debug('Terminating target runner...') |
| 139 | + os.killpg(self.runner_process.pid, signal.SIGTERM) |
| 140 | + # Wait 3 seconds before killing the runner. |
| 141 | + self.runner_process.wait(timeout=3) |
| 142 | + except subprocess.TimeoutExpired: |
| 143 | + self.logger.info('Killing target runner...') |
| 144 | + os.killpg(self.runner_process.pid, signal.SIGKILL) |
| 145 | + |
| 146 | + |
| 147 | +class QEMUTargetRunner(TargetRunner): |
| 148 | + """ |
| 149 | + Class for interacting with QEMU runners. |
| 150 | +
|
| 151 | + :class:`QEMUTargetRunner` is a subclass of :class:`TargetRunner` which performs necessary |
| 152 | + groundwork for launching a guest OS on QEMU. |
| 153 | +
|
| 154 | + :param qemu_settings: A dictionary which has QEMU related parameters. The full list |
| 155 | + of QEMU parameters is below: |
| 156 | + * ``kernel_image``: This is the location of kernel image (e.g., ``Image``) which |
| 157 | + will be used as target's kernel. |
| 158 | +
|
| 159 | + * ``arch``: Architecture type. Defaults to ``aarch64``. |
| 160 | +
|
| 161 | + * ``cpu_types``: List of CPU ids for QEMU. The list only contains ``cortex-a72`` by |
| 162 | + default. This parameter is valid for Arm architectures only. |
| 163 | +
|
| 164 | + * ``initrd_image``: This points to the location of initrd image (e.g., |
| 165 | + ``rootfs.cpio.xz``) which will be used as target's root filesystem if kernel |
| 166 | + does not include one already. |
| 167 | +
|
| 168 | + * ``mem_size``: Size of guest memory in MiB. |
| 169 | +
|
| 170 | + * ``num_cores``: Number of CPU cores. Guest will have ``2`` cores by default. |
| 171 | +
|
| 172 | + * ``num_threads``: Number of CPU threads. Set to ``2`` by defaults. |
| 173 | +
|
| 174 | + * ``cmdline``: Kernel command line parameter. It only specifies console device in |
| 175 | + default (i.e., ``console=ttyAMA0``) which is valid for Arm architectures. |
| 176 | + May be changed to ``ttyS0`` for x86 platforms. |
| 177 | +
|
| 178 | + * ``enable_kvm``: Specifies if KVM will be used as accelerator in QEMU or not. |
| 179 | + Enabled by default if host architecture matches with target's for improving |
| 180 | + QEMU performance. |
| 181 | + :type qemu_settings: Dict |
| 182 | +
|
| 183 | + :param connection_settings: the dictionary to store connection settings |
| 184 | + of :attr:`Target.connection_settings`, defaults to None. |
| 185 | + :type connection_settings: Dict, optional |
| 186 | +
|
| 187 | + :param make_target: Lambda function for creating :class:`Target` based |
| 188 | + object, defaults to :func:`lambda **kwargs: LinuxTarget(**kwargs)`. |
| 189 | + :type make_target: func, optional |
| 190 | +
|
| 191 | + :Variable positional arguments: Forwarded to :class:`TargetRunner`. |
| 192 | +
|
| 193 | + :raises FileNotFoundError: if QEMU executable, kernel or initrd image cannot be found. |
| 194 | + """ |
| 195 | + |
| 196 | + def __init__(self, |
| 197 | + qemu_settings, |
| 198 | + connection_settings=None, |
| 199 | + # pylint: disable=unnecessary-lambda |
| 200 | + make_target=lambda **kwargs: LinuxTarget(**kwargs), |
| 201 | + **args): |
| 202 | + self.connection_settings = { |
| 203 | + 'host': '127.0.0.1', |
| 204 | + 'port': 8022, |
| 205 | + 'username': 'root', |
| 206 | + 'password': 'root', |
| 207 | + 'strict_host_check': False, |
| 208 | + } |
| 209 | + |
| 210 | + # Update default connection settings with :param:`connection_settings` (if exists). |
| 211 | + if connection_settings is not None: |
| 212 | + self.connection_settings = {**self.connection_settings, **connection_settings} |
| 213 | + |
| 214 | + qemu_args = { |
| 215 | + 'kernel_image': '', |
| 216 | + 'arch': 'aarch64', |
| 217 | + 'cpu_type': 'cortex-a72', |
| 218 | + 'initrd_image': '', |
| 219 | + 'mem_size': 512, |
| 220 | + 'num_cores': 2, |
| 221 | + 'num_threads': 2, |
| 222 | + 'cmdline': 'console=ttyAMA0', |
| 223 | + 'enable_kvm': True, |
| 224 | + } |
| 225 | + |
| 226 | + # Update QEMU arguments with :param:`qemu_settings`. |
| 227 | + qemu_args.update( |
| 228 | + (key, value) |
| 229 | + for key, value in qemu_settings.items() |
| 230 | + if key in qemu_args |
| 231 | + ) |
| 232 | + |
| 233 | + qemu_executable = f'qemu-system-{qemu_args["arch"]}' |
| 234 | + qemu_path = which(qemu_executable) |
| 235 | + if qemu_path is None: |
| 236 | + raise FileNotFoundError(f'Cannot find {qemu_executable} executable!') |
| 237 | + |
| 238 | + if not os.path.exists(qemu_args["kernel_image"]): |
| 239 | + raise FileNotFoundError(f'{qemu_args["kernel_image"]} does not exist!') |
| 240 | + |
| 241 | + # pylint: disable=consider-using-f-string |
| 242 | + qemu_cmd = '''\ |
| 243 | +{} -kernel {} -append "{}" -m {} -smp cores={},threads={} -netdev user,id=net0,hostfwd=tcp::{}-:22 \ |
| 244 | +-device virtio-net-pci,netdev=net0 --nographic\ |
| 245 | +'''.format( |
| 246 | + qemu_path, |
| 247 | + qemu_args["kernel_image"], |
| 248 | + qemu_args["cmdline"], |
| 249 | + qemu_args["mem_size"], |
| 250 | + qemu_args["num_cores"], |
| 251 | + qemu_args["num_threads"], |
| 252 | + self.connection_settings["port"], |
| 253 | + ) |
| 254 | + |
| 255 | + if qemu_args["initrd_image"]: |
| 256 | + if not os.path.exists(qemu_args["initrd_image"]): |
| 257 | + raise FileNotFoundError(f'{qemu_args["initrd_image"]} does not exist!') |
| 258 | + |
| 259 | + qemu_cmd += f' -initrd {qemu_args["initrd_image"]}' |
| 260 | + |
| 261 | + if qemu_args["arch"] == machine(): |
| 262 | + if qemu_args["enable_kvm"]: |
| 263 | + qemu_cmd += ' --enable-kvm' |
| 264 | + else: |
| 265 | + qemu_cmd += f' -machine virt -cpu {qemu_args["cpu_type"]}' |
| 266 | + |
| 267 | + self.target = make_target(connect=False, |
| 268 | + conn_cls=SshConnection, |
| 269 | + connection_settings=self.connection_settings) |
| 270 | + |
| 271 | + super().__init__(runner_cmd=qemu_cmd, |
| 272 | + target=self.target, |
| 273 | + **args) |
0 commit comments