diff options
author | Saul Wold <Saul.Wold@windriver.com> | 2021-04-26 07:45:10 -0700 |
---|---|---|
committer | Richard Purdie <richard.purdie@linuxfoundation.org> | 2021-04-27 15:11:47 +0100 |
commit | 3acbec85b00d693d2d731bc2b09cc40be1cc68e9 (patch) | |
tree | 3b86b76cc4932ff2d9fbde194f70057a5e99079f /meta/lib/oeqa | |
parent | 2c86aba6f0eeb1fc747de2f518d1ec982398c54a (diff) | |
download | poky-3acbec85b00d693d2d731bc2b09cc40be1cc68e9.tar.gz |
qemurunner: Add support for qmp commands
This adds support for the Qemu Machine Protocol [0] extending
the current dump process for Host and Target. The commands are
added in the testimage.bbclass.
Currently, we setup qemu to stall until qmp gets connected and
sends the initialization and continue commands, this works
correctly. If the UNIX Socket does not exist, we wait an timeout
to ensure to socket file is created.
With this version, the monitor_dumper is created in OEQemuTarget
but then set in OESSHTarget as that's where we get the SSH failure
happens. Python's @property is used to create a setter/getter type
of setup in OESSHTarget to get overridden by OEQemuTarget.
By default the data is currently dumped to files for each command in
TMPDIR/log/runtime-hostdump/<date>_qmp/unknown_<seq>_qemu_monitor as
this is the naming convenstion in the dump.py code.
We use the qmp.py from qemu, which needs to get installed in the
recipe-sysroot-native of the target image.
[0] https://github.com/qemu/qemu/blob/master/docs/interop/qmp-spec.txt
(From OE-Core rev: 42af4cd2df72fc8ed9deb3fde4312909842fcf91)
Signed-off-by: Saul Wold <saul.wold@windriver.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'meta/lib/oeqa')
-rw-r--r-- | meta/lib/oeqa/core/target/qemu.py | 6 | ||||
-rw-r--r-- | meta/lib/oeqa/core/target/ssh.py | 17 | ||||
-rw-r--r-- | meta/lib/oeqa/targetcontrol.py | 3 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/dump.py | 32 | ||||
-rw-r--r-- | meta/lib/oeqa/utils/qemurunner.py | 70 |
5 files changed, 122 insertions, 6 deletions
diff --git a/meta/lib/oeqa/core/target/qemu.py b/meta/lib/oeqa/core/target/qemu.py index 792efca1f8..4a5df4a9a8 100644 --- a/meta/lib/oeqa/core/target/qemu.py +++ b/meta/lib/oeqa/core/target/qemu.py | |||
@@ -12,6 +12,7 @@ from collections import defaultdict | |||
12 | 12 | ||
13 | from .ssh import OESSHTarget | 13 | from .ssh import OESSHTarget |
14 | from oeqa.utils.qemurunner import QemuRunner | 14 | from oeqa.utils.qemurunner import QemuRunner |
15 | from oeqa.utils.dump import MonitorDumper | ||
15 | from oeqa.utils.dump import TargetDumper | 16 | from oeqa.utils.dump import TargetDumper |
16 | 17 | ||
17 | supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic'] | 18 | supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic'] |
@@ -43,6 +44,11 @@ class OEQemuTarget(OESSHTarget): | |||
43 | dump_host_cmds=dump_host_cmds, logger=logger, | 44 | dump_host_cmds=dump_host_cmds, logger=logger, |
44 | serial_ports=serial_ports, boot_patterns = boot_patterns, | 45 | serial_ports=serial_ports, boot_patterns = boot_patterns, |
45 | use_ovmf=ovmf, tmpfsdir=tmpfsdir) | 46 | use_ovmf=ovmf, tmpfsdir=tmpfsdir) |
47 | dump_monitor_cmds = kwargs.get("testimage_dump_monitor") | ||
48 | self.monitor_dumper = MonitorDumper(dump_monitor_cmds, dump_dir, self.runner) | ||
49 | if self.monitor_dumper: | ||
50 | self.monitor_dumper.create_dir("qmp") | ||
51 | |||
46 | dump_target_cmds = kwargs.get("testimage_dump_target") | 52 | dump_target_cmds = kwargs.get("testimage_dump_target") |
47 | self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner) | 53 | self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner) |
48 | self.target_dumper.create_dir("qemu") | 54 | self.target_dumper.create_dir("qemu") |
diff --git a/meta/lib/oeqa/core/target/ssh.py b/meta/lib/oeqa/core/target/ssh.py index 461448dbc5..923a223b25 100644 --- a/meta/lib/oeqa/core/target/ssh.py +++ b/meta/lib/oeqa/core/target/ssh.py | |||
@@ -43,6 +43,7 @@ class OESSHTarget(OETarget): | |||
43 | if port: | 43 | if port: |
44 | self.ssh = self.ssh + [ '-p', port ] | 44 | self.ssh = self.ssh + [ '-p', port ] |
45 | self.scp = self.scp + [ '-P', port ] | 45 | self.scp = self.scp + [ '-P', port ] |
46 | self._monitor_dumper = None | ||
46 | 47 | ||
47 | def start(self, **kwargs): | 48 | def start(self, **kwargs): |
48 | pass | 49 | pass |
@@ -50,6 +51,15 @@ class OESSHTarget(OETarget): | |||
50 | def stop(self, **kwargs): | 51 | def stop(self, **kwargs): |
51 | pass | 52 | pass |
52 | 53 | ||
54 | @property | ||
55 | def monitor_dumper(self): | ||
56 | return self._monitor_dumper | ||
57 | |||
58 | @monitor_dumper.setter | ||
59 | def monitor_dumper(self, dumper): | ||
60 | self._monitor_dumper = dumper | ||
61 | self.monitor_dumper.dump_monitor() | ||
62 | |||
53 | def _run(self, command, timeout=None, ignore_status=True): | 63 | def _run(self, command, timeout=None, ignore_status=True): |
54 | """ | 64 | """ |
55 | Runs command in target using SSHProcess. | 65 | Runs command in target using SSHProcess. |
@@ -87,9 +97,14 @@ class OESSHTarget(OETarget): | |||
87 | processTimeout = self.timeout | 97 | processTimeout = self.timeout |
88 | 98 | ||
89 | status, output = self._run(sshCmd, processTimeout, True) | 99 | status, output = self._run(sshCmd, processTimeout, True) |
90 | self.logger.debug('Command: %s\nOutput: %s\n' % (command, output)) | 100 | self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output)) |
91 | if (status == 255) and (('No route to host') in output): | 101 | if (status == 255) and (('No route to host') in output): |
102 | if self.monitor_dumper: | ||
103 | self.monitor_dumper.dump_monitor() | ||
104 | if status == 255: | ||
92 | self.target_dumper.dump_target() | 105 | self.target_dumper.dump_target() |
106 | if self.monitor_dumper: | ||
107 | self.monitor_dumper.dump_monitor() | ||
93 | return (status, output) | 108 | return (status, output) |
94 | 109 | ||
95 | def copyTo(self, localSrc, remoteDst): | 110 | def copyTo(self, localSrc, remoteDst): |
diff --git a/meta/lib/oeqa/targetcontrol.py b/meta/lib/oeqa/targetcontrol.py index 12057f855a..005ebaa7f3 100644 --- a/meta/lib/oeqa/targetcontrol.py +++ b/meta/lib/oeqa/targetcontrol.py | |||
@@ -17,6 +17,7 @@ from oeqa.utils.sshcontrol import SSHControl | |||
17 | from oeqa.utils.qemurunner import QemuRunner | 17 | from oeqa.utils.qemurunner import QemuRunner |
18 | from oeqa.utils.qemutinyrunner import QemuTinyRunner | 18 | from oeqa.utils.qemutinyrunner import QemuTinyRunner |
19 | from oeqa.utils.dump import TargetDumper | 19 | from oeqa.utils.dump import TargetDumper |
20 | from oeqa.utils.dump import MonitorDumper | ||
20 | from oeqa.controllers.testtargetloader import TestTargetLoader | 21 | from oeqa.controllers.testtargetloader import TestTargetLoader |
21 | from abc import ABCMeta, abstractmethod | 22 | from abc import ABCMeta, abstractmethod |
22 | 23 | ||
@@ -108,6 +109,7 @@ class QemuTarget(BaseTarget): | |||
108 | self.qemulog = os.path.join(self.testdir, "qemu_boot_log.%s" % self.datetime) | 109 | self.qemulog = os.path.join(self.testdir, "qemu_boot_log.%s" % self.datetime) |
109 | dump_target_cmds = d.getVar("testimage_dump_target") | 110 | dump_target_cmds = d.getVar("testimage_dump_target") |
110 | dump_host_cmds = d.getVar("testimage_dump_host") | 111 | dump_host_cmds = d.getVar("testimage_dump_host") |
112 | dump_monitor_cmds = d.getVar("testimage_dump_monitor") | ||
111 | dump_dir = d.getVar("TESTIMAGE_DUMP_DIR") | 113 | dump_dir = d.getVar("TESTIMAGE_DUMP_DIR") |
112 | if not dump_dir: | 114 | if not dump_dir: |
113 | dump_dir = os.path.join(d.getVar('LOG_DIR'), 'runtime-hostdump') | 115 | dump_dir = os.path.join(d.getVar('LOG_DIR'), 'runtime-hostdump') |
@@ -149,6 +151,7 @@ class QemuTarget(BaseTarget): | |||
149 | serial_ports = len(d.getVar("SERIAL_CONSOLES").split())) | 151 | serial_ports = len(d.getVar("SERIAL_CONSOLES").split())) |
150 | 152 | ||
151 | self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner) | 153 | self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner) |
154 | self.monitor_dumper = MonitorDumper(dump_monitor_cmds, dump_dir, self.runner) | ||
152 | 155 | ||
153 | def deploy(self): | 156 | def deploy(self): |
154 | bb.utils.mkdirhier(self.testdir) | 157 | bb.utils.mkdirhier(self.testdir) |
diff --git a/meta/lib/oeqa/utils/dump.py b/meta/lib/oeqa/utils/dump.py index 09a44329e0..843e19fe8a 100644 --- a/meta/lib/oeqa/utils/dump.py +++ b/meta/lib/oeqa/utils/dump.py | |||
@@ -4,6 +4,7 @@ | |||
4 | 4 | ||
5 | import os | 5 | import os |
6 | import sys | 6 | import sys |
7 | import json | ||
7 | import errno | 8 | import errno |
8 | import datetime | 9 | import datetime |
9 | import itertools | 10 | import itertools |
@@ -51,6 +52,8 @@ class BaseDumper(object): | |||
51 | prefix = "host" | 52 | prefix = "host" |
52 | elif isinstance(self, TargetDumper): | 53 | elif isinstance(self, TargetDumper): |
53 | prefix = "target" | 54 | prefix = "target" |
55 | elif isinstance(self, MonitorDumper): | ||
56 | prefix = "qmp" | ||
54 | else: | 57 | else: |
55 | prefix = "unknown" | 58 | prefix = "unknown" |
56 | for i in itertools.count(): | 59 | for i in itertools.count(): |
@@ -58,9 +61,12 @@ class BaseDumper(object): | |||
58 | fullname = os.path.join(self.dump_dir, filename) | 61 | fullname = os.path.join(self.dump_dir, filename) |
59 | if not os.path.exists(fullname): | 62 | if not os.path.exists(fullname): |
60 | break | 63 | break |
61 | with open(fullname, 'w') as dump_file: | 64 | if isinstance(self, MonitorDumper): |
62 | dump_file.write(output) | 65 | with open(fullname, 'w') as json_file: |
63 | 66 | json.dump(output, json_file, indent=4) | |
67 | else: | ||
68 | with open(fullname, 'w') as dump_file: | ||
69 | dump_file.write(output) | ||
64 | 70 | ||
65 | class HostDumper(BaseDumper): | 71 | class HostDumper(BaseDumper): |
66 | """ Class to get dumps from the host running the tests """ | 72 | """ Class to get dumps from the host running the tests """ |
@@ -96,3 +102,23 @@ class TargetDumper(BaseDumper): | |||
96 | except: | 102 | except: |
97 | print("Tried to dump info from target but " | 103 | print("Tried to dump info from target but " |
98 | "serial console failed") | 104 | "serial console failed") |
105 | print("Failed CMD: %s" % (cmd)) | ||
106 | |||
107 | class MonitorDumper(BaseDumper): | ||
108 | """ Class to get dumps via the Qemu Monitor, it only works with QemuRunner """ | ||
109 | |||
110 | def __init__(self, cmds, parent_dir, runner): | ||
111 | super(MonitorDumper, self).__init__(cmds, parent_dir) | ||
112 | self.runner = runner | ||
113 | |||
114 | def dump_monitor(self, dump_dir=""): | ||
115 | if self.runner is None: | ||
116 | return | ||
117 | if dump_dir: | ||
118 | self.dump_dir = dump_dir | ||
119 | for cmd in self.cmds: | ||
120 | try: | ||
121 | output = self.runner.run_monitor(cmd) | ||
122 | self._write_dump(cmd, output) | ||
123 | except: | ||
124 | print("Failed to dump QMP CMD: %s" % (cmd)) | ||
diff --git a/meta/lib/oeqa/utils/qemurunner.py b/meta/lib/oeqa/utils/qemurunner.py index 278904ba0b..f6e1007288 100644 --- a/meta/lib/oeqa/utils/qemurunner.py +++ b/meta/lib/oeqa/utils/qemurunner.py | |||
@@ -20,8 +20,10 @@ import string | |||
20 | import threading | 20 | import threading |
21 | import codecs | 21 | import codecs |
22 | import logging | 22 | import logging |
23 | import tempfile | ||
23 | from oeqa.utils.dump import HostDumper | 24 | from oeqa.utils.dump import HostDumper |
24 | from collections import defaultdict | 25 | from collections import defaultdict |
26 | import importlib | ||
25 | 27 | ||
26 | # Get Unicode non printable control chars | 28 | # Get Unicode non printable control chars |
27 | control_range = list(range(0,32))+list(range(127,160)) | 29 | control_range = list(range(0,32))+list(range(127,160)) |
@@ -172,6 +174,21 @@ class QemuRunner: | |||
172 | return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env) | 174 | return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env) |
173 | 175 | ||
174 | def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None): | 176 | def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None): |
177 | # use logfile to determine the recipe-sysroot-native path and | ||
178 | # then add in the site-packages path components and add that | ||
179 | # to the python sys.path so qmp.py can be found. | ||
180 | python_path = os.path.dirname(os.path.dirname(self.logfile)) | ||
181 | python_path += "/recipe-sysroot-native/usr/lib/python3.9/site-packages" | ||
182 | sys.path.append(python_path) | ||
183 | importlib.invalidate_caches() | ||
184 | try: | ||
185 | qmp = importlib.import_module("qmp") | ||
186 | except: | ||
187 | self.logger.error("qemurunner: qmp.py missing, please ensure it's installed") | ||
188 | return False | ||
189 | qmp_port = self.tmpdir + "/." + next(tempfile._get_candidate_names()) | ||
190 | qmp_param = ' -S -qmp unix:%s,server,wait' % (qmp_port) | ||
191 | |||
175 | try: | 192 | try: |
176 | if self.serial_ports >= 2: | 193 | if self.serial_ports >= 2: |
177 | self.threadsock, threadport = self.create_socket() | 194 | self.threadsock, threadport = self.create_socket() |
@@ -188,7 +205,8 @@ class QemuRunner: | |||
188 | # and analyze descendents in order to determine it. | 205 | # and analyze descendents in order to determine it. |
189 | if os.path.exists(self.qemu_pidfile): | 206 | if os.path.exists(self.qemu_pidfile): |
190 | os.remove(self.qemu_pidfile) | 207 | os.remove(self.qemu_pidfile) |
191 | self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1}"'.format(bootparams, self.qemu_pidfile) | 208 | self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1} {2}"'.format(bootparams, self.qemu_pidfile, qmp_param) |
209 | |||
192 | if qemuparams: | 210 | if qemuparams: |
193 | self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"' | 211 | self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"' |
194 | 212 | ||
@@ -242,6 +260,7 @@ class QemuRunner: | |||
242 | while not self.is_alive() and time.time() < endtime: | 260 | while not self.is_alive() and time.time() < endtime: |
243 | if self.runqemu.poll(): | 261 | if self.runqemu.poll(): |
244 | if self.runqemu_exited: | 262 | if self.runqemu_exited: |
263 | self.logger.warning("runqemu during is_alive() test") | ||
245 | return False | 264 | return False |
246 | if self.runqemu.returncode: | 265 | if self.runqemu.returncode: |
247 | # No point waiting any longer | 266 | # No point waiting any longer |
@@ -253,9 +272,51 @@ class QemuRunner: | |||
253 | time.sleep(0.5) | 272 | time.sleep(0.5) |
254 | 273 | ||
255 | if self.runqemu_exited: | 274 | if self.runqemu_exited: |
275 | self.logger.warning("runqemu after timeout") | ||
276 | return False | ||
277 | |||
278 | if self.runqemu.returncode: | ||
279 | self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode) | ||
256 | return False | 280 | return False |
257 | 281 | ||
258 | if not self.is_alive(): | 282 | if not self.is_alive(): |
283 | self.logger.warning('is_alive() failed later') | ||
284 | return False | ||
285 | |||
286 | # Create the client socket for the QEMU Monitor Control Socket | ||
287 | # This will allow us to read status from Qemu if the the process | ||
288 | # is still alive | ||
289 | self.logger.debug("QMP Initializing to %s" % (qmp_port)) | ||
290 | try: | ||
291 | self.qmp = qmp.QEMUMonitorProtocol(qmp_port) | ||
292 | except OSError as msg: | ||
293 | self.logger.warning("Failed to initialize qemu monitor socket: %s File: %s" % (msg, msg.filename)) | ||
294 | return False | ||
295 | |||
296 | self.logger.debug("QMP Connecting to %s" % (qmp_port)) | ||
297 | if not os.path.exists(qmp_port) and self.is_alive(): | ||
298 | self.logger.debug("QMP Port does not exist waiting for it to be created") | ||
299 | endtime = time.time() + self.runqemutime | ||
300 | while not os.path.exists(qmp_port) and self.is_alive() and time.time() < endtime: | ||
301 | self.logger.warning("QMP port does not exist yet!") | ||
302 | time.sleep(0.5) | ||
303 | if not os.path.exists(qmp_port) and self.is_alive(): | ||
304 | self.logger.warning("QMP Port still does not exist but QEMU is alive") | ||
305 | return False | ||
306 | |||
307 | try: | ||
308 | self.qmp.connect() | ||
309 | except OSError as msg: | ||
310 | self.logger.warning("Failed to connect qemu monitor socket: %s File: %s" % (msg, msg.filename)) | ||
311 | return False | ||
312 | except qmp.QMPConnectError as msg: | ||
313 | self.logger.warning("Failed to communicate with qemu monitor: %s" % (msg)) | ||
314 | return False | ||
315 | |||
316 | # Release the qemu porcess to continue running | ||
317 | self.run_monitor('cont') | ||
318 | |||
319 | if not self.is_alive(): | ||
259 | self.logger.error("Qemu pid didn't appear in %s seconds (%s)" % | 320 | self.logger.error("Qemu pid didn't appear in %s seconds (%s)" % |
260 | (self.runqemutime, time.strftime("%D %H:%M:%S"))) | 321 | (self.runqemutime, time.strftime("%D %H:%M:%S"))) |
261 | 322 | ||
@@ -380,7 +441,6 @@ class QemuRunner: | |||
380 | sock.close() | 441 | sock.close() |
381 | stopread = True | 442 | stopread = True |
382 | 443 | ||
383 | |||
384 | if not reachedlogin: | 444 | if not reachedlogin: |
385 | if time.time() >= endtime: | 445 | if time.time() >= endtime: |
386 | self.logger.warning("Target didn't reach login banner in %d seconds (%s)" % | 446 | self.logger.warning("Target didn't reach login banner in %d seconds (%s)" % |
@@ -441,6 +501,9 @@ class QemuRunner: | |||
441 | self.runqemu.stdout.close() | 501 | self.runqemu.stdout.close() |
442 | self.runqemu_exited = True | 502 | self.runqemu_exited = True |
443 | 503 | ||
504 | if hasattr(self, 'qmp') and self.qmp: | ||
505 | self.qmp.close() | ||
506 | self.qmp = None | ||
444 | if hasattr(self, 'server_socket') and self.server_socket: | 507 | if hasattr(self, 'server_socket') and self.server_socket: |
445 | self.server_socket.close() | 508 | self.server_socket.close() |
446 | self.server_socket = None | 509 | self.server_socket = None |
@@ -499,6 +562,9 @@ class QemuRunner: | |||
499 | return True | 562 | return True |
500 | return False | 563 | return False |
501 | 564 | ||
565 | def run_monitor(self, command, timeout=60): | ||
566 | return self.qmp.cmd(command) | ||
567 | |||
502 | def run_serial(self, command, raw=False, timeout=60): | 568 | def run_serial(self, command, raw=False, timeout=60): |
503 | # We assume target system have echo to get command status | 569 | # We assume target system have echo to get command status |
504 | if not raw: | 570 | if not raw: |