summaryrefslogtreecommitdiffstats
path: root/meta/lib/oeqa
diff options
context:
space:
mode:
authorSaul Wold <Saul.Wold@windriver.com>2021-04-26 07:45:10 -0700
committerRichard Purdie <richard.purdie@linuxfoundation.org>2021-04-27 15:11:47 +0100
commit3acbec85b00d693d2d731bc2b09cc40be1cc68e9 (patch)
tree3b86b76cc4932ff2d9fbde194f70057a5e99079f /meta/lib/oeqa
parent2c86aba6f0eeb1fc747de2f518d1ec982398c54a (diff)
downloadpoky-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.py6
-rw-r--r--meta/lib/oeqa/core/target/ssh.py17
-rw-r--r--meta/lib/oeqa/targetcontrol.py3
-rw-r--r--meta/lib/oeqa/utils/dump.py32
-rw-r--r--meta/lib/oeqa/utils/qemurunner.py70
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
13from .ssh import OESSHTarget 13from .ssh import OESSHTarget
14from oeqa.utils.qemurunner import QemuRunner 14from oeqa.utils.qemurunner import QemuRunner
15from oeqa.utils.dump import MonitorDumper
15from oeqa.utils.dump import TargetDumper 16from oeqa.utils.dump import TargetDumper
16 17
17supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic'] 18supported_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
17from oeqa.utils.qemurunner import QemuRunner 17from oeqa.utils.qemurunner import QemuRunner
18from oeqa.utils.qemutinyrunner import QemuTinyRunner 18from oeqa.utils.qemutinyrunner import QemuTinyRunner
19from oeqa.utils.dump import TargetDumper 19from oeqa.utils.dump import TargetDumper
20from oeqa.utils.dump import MonitorDumper
20from oeqa.controllers.testtargetloader import TestTargetLoader 21from oeqa.controllers.testtargetloader import TestTargetLoader
21from abc import ABCMeta, abstractmethod 22from 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
5import os 5import os
6import sys 6import sys
7import json
7import errno 8import errno
8import datetime 9import datetime
9import itertools 10import 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
65class HostDumper(BaseDumper): 71class 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
107class 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
20import threading 20import threading
21import codecs 21import codecs
22import logging 22import logging
23import tempfile
23from oeqa.utils.dump import HostDumper 24from oeqa.utils.dump import HostDumper
24from collections import defaultdict 25from collections import defaultdict
26import importlib
25 27
26# Get Unicode non printable control chars 28# Get Unicode non printable control chars
27control_range = list(range(0,32))+list(range(127,160)) 29control_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: