|
1 | 1 | """
|
2 | 2 | .. module:: qemu
|
3 | 3 | :platform: Linux
|
4 |
| - :synopsis: module containing the base for qemu SUT implementation |
| 4 | + :synopsis: module containing qemu SUT implementation |
5 | 5 |
|
6 | 6 | .. moduleauthor:: Andrea Cervesato <[email protected]>
|
7 | 7 | """
|
|
11 | 11 | import signal
|
12 | 12 | import select
|
13 | 13 | import string
|
| 14 | +import shutil |
14 | 15 | import secrets
|
15 | 16 | import logging
|
16 | 17 | import threading
|
|
24 | 25 | from ltp.utils import LTPTimeoutError
|
25 | 26 |
|
26 | 27 |
|
27 |
| -class QemuBase(SUT): |
| 28 | +# pylint: disable=too-many-instance-attributes |
| 29 | +class QemuSUT(SUT): |
28 | 30 | """
|
29 |
| - This is a base class for qemu based SUT implementations. |
| 31 | + Qemu SUT spawn a new VM using qemu and execute commands inside it. |
| 32 | + This SUT implementation can be used to run commands inside |
| 33 | + a protected, virtualized environment. |
30 | 34 | """
|
31 | 35 |
|
32 | 36 | def __init__(self) -> None:
|
33 | 37 | self._logger = logging.getLogger("ltp.qemu")
|
34 | 38 | self._comm_lock = threading.Lock()
|
35 | 39 | self._cmd_lock = threading.Lock()
|
36 | 40 | self._fetch_lock = threading.Lock()
|
| 41 | + self._tmpdir = None |
| 42 | + self._env = None |
| 43 | + self._cwd = None |
37 | 44 | self._proc = None
|
38 | 45 | self._poller = None
|
39 | 46 | self._stop = False
|
40 | 47 | self._logged_in = False
|
41 | 48 | self._last_pos = 0
|
| 49 | + self._image = None |
| 50 | + self._image_overlay = None |
| 51 | + self._ro_image = None |
| 52 | + self._password = None |
| 53 | + self._ram = None |
| 54 | + self._smp = None |
| 55 | + self._virtfs = None |
| 56 | + self._serial_type = None |
| 57 | + self._qemu_cmd = None |
| 58 | + self._opts = None |
42 | 59 | self._last_read = ""
|
43 | 60 |
|
44 |
| - def _get_command(self) -> str: |
45 |
| - """ |
46 |
| - Return the full qemu command to execute. |
47 |
| - """ |
48 |
| - raise NotImplementedError() |
49 |
| - |
50 |
| - def _login(self, timeout: float, iobuffer: IOBuffer) -> None: |
| 61 | + @staticmethod |
| 62 | + def _generate_string(length: int = 10) -> str: |
51 | 63 | """
|
52 |
| - Method that implements login after starting the qemu process. |
| 64 | + Generate a random string of the given length. |
53 | 65 | """
|
54 |
| - raise NotImplementedError() |
| 66 | + out = ''.join(secrets.choice(string.ascii_letters + string.digits) |
| 67 | + for _ in range(length)) |
| 68 | + return out |
55 | 69 |
|
56 |
| - def _get_transport(self) -> tuple: |
| 70 | + def _get_transport(self) -> str: |
57 | 71 | """
|
58 | 72 | Return a couple of transport_dev and transport_file used by
|
59 | 73 | qemu instance for transport configuration.
|
60 | 74 | """
|
61 |
| - raise NotImplementedError() |
| 75 | + pid = os.getpid() |
| 76 | + transport_file = os.path.join(self._tmpdir, f"transport-{pid}") |
| 77 | + transport_dev = "" |
62 | 78 |
|
63 |
| - @staticmethod |
64 |
| - def _generate_string(length: int = 10) -> str: |
| 79 | + if self._serial_type == "isa": |
| 80 | + transport_dev = "/dev/ttyS1" |
| 81 | + elif self._serial_type == "virtio": |
| 82 | + transport_dev = "/dev/vport1p1" |
| 83 | + |
| 84 | + return transport_dev, transport_file |
| 85 | + |
| 86 | + def _get_command(self) -> str: |
65 | 87 | """
|
66 |
| - Generate a random string of the given length. |
| 88 | + Return the full qemu command to execute. |
67 | 89 | """
|
68 |
| - out = ''.join(secrets.choice(string.ascii_letters + string.digits) |
69 |
| - for _ in range(length)) |
70 |
| - return out |
| 90 | + pid = os.getpid() |
| 91 | + tty_log = os.path.join(self._tmpdir, f"ttyS0-{pid}.log") |
| 92 | + |
| 93 | + image = self._image |
| 94 | + if self._image_overlay: |
| 95 | + shutil.copyfile( |
| 96 | + self._image, |
| 97 | + self._image_overlay) |
| 98 | + image = self._image_overlay |
| 99 | + |
| 100 | + params = [] |
| 101 | + params.append("-enable-kvm") |
| 102 | + params.append("-display none") |
| 103 | + params.append(f"-m {self._ram}") |
| 104 | + params.append(f"-smp {self._smp}") |
| 105 | + params.append("-device virtio-rng-pci") |
| 106 | + params.append(f"-drive if=virtio,cache=unsafe,file={image}") |
| 107 | + params.append(f"-chardev stdio,id=tty,logfile={tty_log}") |
| 108 | + |
| 109 | + if self._serial_type == "isa": |
| 110 | + params.append("-serial chardev:tty") |
| 111 | + params.append("-serial chardev:transport") |
| 112 | + elif self._serial_type == "virtio": |
| 113 | + params.append("-device virtio-serial") |
| 114 | + params.append("-device virtconsole,chardev=tty") |
| 115 | + params.append("-device virtserialport,chardev=transport") |
| 116 | + else: |
| 117 | + raise SUTError( |
| 118 | + f"Unsupported serial device type {self._serial_type}") |
| 119 | + |
| 120 | + _, transport_file = self._get_transport() |
| 121 | + params.append(f"-chardev file,id=transport,path={transport_file}") |
| 122 | + |
| 123 | + if self._ro_image: |
| 124 | + params.append( |
| 125 | + "-drive read-only," |
| 126 | + "if=virtio," |
| 127 | + "cache=unsafe," |
| 128 | + f"file={self._ro_image}") |
| 129 | + |
| 130 | + if self._virtfs: |
| 131 | + params.append( |
| 132 | + "-virtfs local," |
| 133 | + f"path={self._virtfs}," |
| 134 | + "mount_tag=host0," |
| 135 | + "security_model=mapped-xattr," |
| 136 | + "readonly=on") |
| 137 | + |
| 138 | + if self._opts: |
| 139 | + params.append(self._opts) |
| 140 | + |
| 141 | + cmd = f"{self._qemu_cmd} {' '.join(params)}" |
| 142 | + |
| 143 | + return cmd |
| 144 | + |
| 145 | + def setup(self, **kwargs: dict) -> None: |
| 146 | + self._logger.info("Initialize SUT") |
| 147 | + |
| 148 | + self._env = kwargs.get("env", None) |
| 149 | + self._cwd = kwargs.get("cwd", None) |
| 150 | + self._tmpdir = kwargs.get("tmpdir", None) |
| 151 | + self._image = kwargs.get("image", None) |
| 152 | + self._image_overlay = kwargs.get("image_overlay", None) |
| 153 | + self._ro_image = kwargs.get("ro_image", None) |
| 154 | + self._password = kwargs.get("password", "root") |
| 155 | + self._ram = kwargs.get("ram", "2G") |
| 156 | + self._smp = kwargs.get("smp", "2") |
| 157 | + self._virtfs = kwargs.get("virtfs", None) |
| 158 | + self._serial_type = kwargs.get("serial", "isa") |
| 159 | + self._opts = kwargs.get("options", None) |
| 160 | + |
| 161 | + system = kwargs.get("system", "x86_64") |
| 162 | + self._qemu_cmd = f"qemu-system-{system}" |
| 163 | + |
| 164 | + if not self._tmpdir or not os.path.isdir(self._tmpdir): |
| 165 | + raise SUTError( |
| 166 | + f"Temporary directory doesn't exist: {self._tmpdir}") |
| 167 | + |
| 168 | + if not self._image or not os.path.isfile(self._image): |
| 169 | + raise SUTError( |
| 170 | + f"Image location doesn't exist: {self._image}") |
| 171 | + |
| 172 | + if self._ro_image and not os.path.isfile(self._ro_image): |
| 173 | + raise SUTError( |
| 174 | + f"Read-only image location doesn't exist: {self._ro_image}") |
| 175 | + |
| 176 | + if not self._ram: |
| 177 | + raise SUTError("RAM is not defined") |
| 178 | + |
| 179 | + if not self._smp: |
| 180 | + raise SUTError("CPU is not defined") |
| 181 | + |
| 182 | + if self._virtfs and not os.path.isdir(self._virtfs): |
| 183 | + raise SUTError( |
| 184 | + f"Virtual FS directory doesn't exist: {self._virtfs}") |
| 185 | + |
| 186 | + if self._serial_type not in ["isa", "virtio"]: |
| 187 | + raise SUTError("Serial protocol must be isa or virtio") |
| 188 | + |
| 189 | + @property |
| 190 | + def config_help(self) -> dict: |
| 191 | + return { |
| 192 | + "image": "qcow2 image location", |
| 193 | + "image_overlay": "image_overlay: image copy location", |
| 194 | + "password": "root password (default: root)", |
| 195 | + "system": "system architecture (default: x86_64)", |
| 196 | + "ram": "RAM of the VM (default: 2G)", |
| 197 | + "smp": "number of CPUs (default: 2)", |
| 198 | + "serial": "type of serial protocol. isa|virtio (default: isa)", |
| 199 | + "virtfs": "directory to mount inside VM", |
| 200 | + "ro_image": "path of the image that will exposed as read only", |
| 201 | + "options": "user defined options", |
| 202 | + } |
| 203 | + |
| 204 | + @property |
| 205 | + def name(self) -> str: |
| 206 | + return "qemu" |
71 | 207 |
|
72 | 208 | @property
|
73 | 209 | def is_running(self) -> bool:
|
@@ -334,6 +470,9 @@ def communicate(
|
334 | 470 | self,
|
335 | 471 | timeout: float = 3600,
|
336 | 472 | iobuffer: IOBuffer = None) -> None:
|
| 473 | + if not shutil.which(self._qemu_cmd): |
| 474 | + raise SUTError(f"Command not found: {self._qemu_cmd}") |
| 475 | + |
337 | 476 | if self.is_running:
|
338 | 477 | raise SUTError("Virtual machine is already running")
|
339 | 478 |
|
@@ -364,9 +503,48 @@ def communicate(
|
364 | 503 | select.POLLERR)
|
365 | 504 |
|
366 | 505 | try:
|
367 |
| - self._login(timeout, iobuffer) |
| 506 | + self._wait_for("login:", timeout, iobuffer) |
| 507 | + self._write_stdin("root\n") |
| 508 | + |
| 509 | + if self._password: |
| 510 | + self._wait_for("Password:", 5, iobuffer) |
| 511 | + self._write_stdin(f"{self._password}\n") |
| 512 | + |
| 513 | + time.sleep(0.2) |
| 514 | + |
| 515 | + self._wait_for("#", 5, iobuffer) |
| 516 | + time.sleep(0.2) |
| 517 | + |
| 518 | + self._write_stdin("stty -echo; stty cols 1024\n") |
| 519 | + self._wait_for("#", 5, None) |
| 520 | + |
| 521 | + _, retcode, _ = self._exec("export PS1=''", 5, None) |
| 522 | + if retcode != 0: |
| 523 | + raise SUTError("Can't setup prompt string") |
| 524 | + |
| 525 | + if self._virtfs: |
| 526 | + _, retcode, _ = self._exec( |
| 527 | + "mount -t 9p -o trans=virtio host0 /mnt", |
| 528 | + 10, None) |
| 529 | + if retcode != 0: |
| 530 | + raise SUTError("Failed to mount virtfs") |
| 531 | + |
| 532 | + if self._cwd: |
| 533 | + _, retcode, _ = self._exec(f"cd {self._cwd}", 5, None) |
| 534 | + if retcode != 0: |
| 535 | + raise SUTError("Can't setup current working directory") |
| 536 | + |
| 537 | + if self._env: |
| 538 | + for key, value in self._env.items(): |
| 539 | + _, retcode, _ = self._exec( |
| 540 | + f"export {key}={value}", |
| 541 | + 5, None) |
| 542 | + if retcode != 0: |
| 543 | + raise SUTError(f"Can't setup env {key}={value}") |
| 544 | + |
368 | 545 | self._logged_in = True
|
369 |
| - self._logger.info("Logged inside virtual machine") |
| 546 | + |
| 547 | + self._logger.info("Virtual machine started") |
370 | 548 | except SUTError as err:
|
371 | 549 | error = err
|
372 | 550 |
|
|
0 commit comments