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>
This commit is contained in:
Saul Wold
2021-04-26 07:45:10 -07:00
committed by Richard Purdie
parent 2c86aba6f0
commit 3acbec85b0
7 changed files with 129 additions and 7 deletions

View File

@@ -127,6 +127,11 @@ testimage_dump_host () {
netstat -an
}
testimage_dump_monitor () {
query-status
query-block
}
python do_testimage() {
testimage_main(d)
}
@@ -320,6 +325,7 @@ def testimage_main(d):
target_kwargs['powercontrol_extra_args'] = d.getVar("TEST_POWERCONTROL_EXTRA_ARGS") or ""
target_kwargs['serialcontrol_cmd'] = d.getVar("TEST_SERIALCONTROL_CMD") or None
target_kwargs['serialcontrol_extra_args'] = d.getVar("TEST_SERIALCONTROL_EXTRA_ARGS") or ""
target_kwargs['testimage_dump_monitor'] = d.getVar("testimage_dump_monitor") or ""
target_kwargs['testimage_dump_target'] = d.getVar("testimage_dump_target") or ""
def export_ssh_agent(d):

View File

@@ -12,6 +12,7 @@ from collections import defaultdict
from .ssh import OESSHTarget
from oeqa.utils.qemurunner import QemuRunner
from oeqa.utils.dump import MonitorDumper
from oeqa.utils.dump import TargetDumper
supported_fstypes = ['ext3', 'ext4', 'cpio.gz', 'wic']
@@ -43,6 +44,11 @@ class OEQemuTarget(OESSHTarget):
dump_host_cmds=dump_host_cmds, logger=logger,
serial_ports=serial_ports, boot_patterns = boot_patterns,
use_ovmf=ovmf, tmpfsdir=tmpfsdir)
dump_monitor_cmds = kwargs.get("testimage_dump_monitor")
self.monitor_dumper = MonitorDumper(dump_monitor_cmds, dump_dir, self.runner)
if self.monitor_dumper:
self.monitor_dumper.create_dir("qmp")
dump_target_cmds = kwargs.get("testimage_dump_target")
self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner)
self.target_dumper.create_dir("qemu")

View File

@@ -43,6 +43,7 @@ class OESSHTarget(OETarget):
if port:
self.ssh = self.ssh + [ '-p', port ]
self.scp = self.scp + [ '-P', port ]
self._monitor_dumper = None
def start(self, **kwargs):
pass
@@ -50,6 +51,15 @@ class OESSHTarget(OETarget):
def stop(self, **kwargs):
pass
@property
def monitor_dumper(self):
return self._monitor_dumper
@monitor_dumper.setter
def monitor_dumper(self, dumper):
self._monitor_dumper = dumper
self.monitor_dumper.dump_monitor()
def _run(self, command, timeout=None, ignore_status=True):
"""
Runs command in target using SSHProcess.
@@ -87,9 +97,14 @@ class OESSHTarget(OETarget):
processTimeout = self.timeout
status, output = self._run(sshCmd, processTimeout, True)
self.logger.debug('Command: %s\nOutput: %s\n' % (command, output))
self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output))
if (status == 255) and (('No route to host') in output):
if self.monitor_dumper:
self.monitor_dumper.dump_monitor()
if status == 255:
self.target_dumper.dump_target()
if self.monitor_dumper:
self.monitor_dumper.dump_monitor()
return (status, output)
def copyTo(self, localSrc, remoteDst):

View File

@@ -17,6 +17,7 @@ from oeqa.utils.sshcontrol import SSHControl
from oeqa.utils.qemurunner import QemuRunner
from oeqa.utils.qemutinyrunner import QemuTinyRunner
from oeqa.utils.dump import TargetDumper
from oeqa.utils.dump import MonitorDumper
from oeqa.controllers.testtargetloader import TestTargetLoader
from abc import ABCMeta, abstractmethod
@@ -108,6 +109,7 @@ class QemuTarget(BaseTarget):
self.qemulog = os.path.join(self.testdir, "qemu_boot_log.%s" % self.datetime)
dump_target_cmds = d.getVar("testimage_dump_target")
dump_host_cmds = d.getVar("testimage_dump_host")
dump_monitor_cmds = d.getVar("testimage_dump_monitor")
dump_dir = d.getVar("TESTIMAGE_DUMP_DIR")
if not dump_dir:
dump_dir = os.path.join(d.getVar('LOG_DIR'), 'runtime-hostdump')
@@ -149,6 +151,7 @@ class QemuTarget(BaseTarget):
serial_ports = len(d.getVar("SERIAL_CONSOLES").split()))
self.target_dumper = TargetDumper(dump_target_cmds, dump_dir, self.runner)
self.monitor_dumper = MonitorDumper(dump_monitor_cmds, dump_dir, self.runner)
def deploy(self):
bb.utils.mkdirhier(self.testdir)

View File

@@ -4,6 +4,7 @@
import os
import sys
import json
import errno
import datetime
import itertools
@@ -51,6 +52,8 @@ class BaseDumper(object):
prefix = "host"
elif isinstance(self, TargetDumper):
prefix = "target"
elif isinstance(self, MonitorDumper):
prefix = "qmp"
else:
prefix = "unknown"
for i in itertools.count():
@@ -58,9 +61,12 @@ class BaseDumper(object):
fullname = os.path.join(self.dump_dir, filename)
if not os.path.exists(fullname):
break
with open(fullname, 'w') as dump_file:
dump_file.write(output)
if isinstance(self, MonitorDumper):
with open(fullname, 'w') as json_file:
json.dump(output, json_file, indent=4)
else:
with open(fullname, 'w') as dump_file:
dump_file.write(output)
class HostDumper(BaseDumper):
""" Class to get dumps from the host running the tests """
@@ -96,3 +102,23 @@ class TargetDumper(BaseDumper):
except:
print("Tried to dump info from target but "
"serial console failed")
print("Failed CMD: %s" % (cmd))
class MonitorDumper(BaseDumper):
""" Class to get dumps via the Qemu Monitor, it only works with QemuRunner """
def __init__(self, cmds, parent_dir, runner):
super(MonitorDumper, self).__init__(cmds, parent_dir)
self.runner = runner
def dump_monitor(self, dump_dir=""):
if self.runner is None:
return
if dump_dir:
self.dump_dir = dump_dir
for cmd in self.cmds:
try:
output = self.runner.run_monitor(cmd)
self._write_dump(cmd, output)
except:
print("Failed to dump QMP CMD: %s" % (cmd))

View File

@@ -20,8 +20,10 @@ import string
import threading
import codecs
import logging
import tempfile
from oeqa.utils.dump import HostDumper
from collections import defaultdict
import importlib
# Get Unicode non printable control chars
control_range = list(range(0,32))+list(range(127,160))
@@ -172,6 +174,21 @@ class QemuRunner:
return self.launch(launch_cmd, qemuparams=qemuparams, get_ip=get_ip, extra_bootparams=extra_bootparams, env=env)
def launch(self, launch_cmd, get_ip = True, qemuparams = None, extra_bootparams = None, env = None):
# use logfile to determine the recipe-sysroot-native path and
# then add in the site-packages path components and add that
# to the python sys.path so qmp.py can be found.
python_path = os.path.dirname(os.path.dirname(self.logfile))
python_path += "/recipe-sysroot-native/usr/lib/python3.9/site-packages"
sys.path.append(python_path)
importlib.invalidate_caches()
try:
qmp = importlib.import_module("qmp")
except:
self.logger.error("qemurunner: qmp.py missing, please ensure it's installed")
return False
qmp_port = self.tmpdir + "/." + next(tempfile._get_candidate_names())
qmp_param = ' -S -qmp unix:%s,server,wait' % (qmp_port)
try:
if self.serial_ports >= 2:
self.threadsock, threadport = self.create_socket()
@@ -188,7 +205,8 @@ class QemuRunner:
# and analyze descendents in order to determine it.
if os.path.exists(self.qemu_pidfile):
os.remove(self.qemu_pidfile)
self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1}"'.format(bootparams, self.qemu_pidfile)
self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1} {2}"'.format(bootparams, self.qemu_pidfile, qmp_param)
if qemuparams:
self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
@@ -242,6 +260,7 @@ class QemuRunner:
while not self.is_alive() and time.time() < endtime:
if self.runqemu.poll():
if self.runqemu_exited:
self.logger.warning("runqemu during is_alive() test")
return False
if self.runqemu.returncode:
# No point waiting any longer
@@ -253,8 +272,50 @@ class QemuRunner:
time.sleep(0.5)
if self.runqemu_exited:
self.logger.warning("runqemu after timeout")
return False
if self.runqemu.returncode:
self.logger.warning('runqemu exited with code %d' % self.runqemu.returncode)
return False
if not self.is_alive():
self.logger.warning('is_alive() failed later')
return False
# Create the client socket for the QEMU Monitor Control Socket
# This will allow us to read status from Qemu if the the process
# is still alive
self.logger.debug("QMP Initializing to %s" % (qmp_port))
try:
self.qmp = qmp.QEMUMonitorProtocol(qmp_port)
except OSError as msg:
self.logger.warning("Failed to initialize qemu monitor socket: %s File: %s" % (msg, msg.filename))
return False
self.logger.debug("QMP Connecting to %s" % (qmp_port))
if not os.path.exists(qmp_port) and self.is_alive():
self.logger.debug("QMP Port does not exist waiting for it to be created")
endtime = time.time() + self.runqemutime
while not os.path.exists(qmp_port) and self.is_alive() and time.time() < endtime:
self.logger.warning("QMP port does not exist yet!")
time.sleep(0.5)
if not os.path.exists(qmp_port) and self.is_alive():
self.logger.warning("QMP Port still does not exist but QEMU is alive")
return False
try:
self.qmp.connect()
except OSError as msg:
self.logger.warning("Failed to connect qemu monitor socket: %s File: %s" % (msg, msg.filename))
return False
except qmp.QMPConnectError as msg:
self.logger.warning("Failed to communicate with qemu monitor: %s" % (msg))
return False
# Release the qemu porcess to continue running
self.run_monitor('cont')
if not self.is_alive():
self.logger.error("Qemu pid didn't appear in %s seconds (%s)" %
(self.runqemutime, time.strftime("%D %H:%M:%S")))
@@ -380,7 +441,6 @@ class QemuRunner:
sock.close()
stopread = True
if not reachedlogin:
if time.time() >= endtime:
self.logger.warning("Target didn't reach login banner in %d seconds (%s)" %
@@ -441,6 +501,9 @@ class QemuRunner:
self.runqemu.stdout.close()
self.runqemu_exited = True
if hasattr(self, 'qmp') and self.qmp:
self.qmp.close()
self.qmp = None
if hasattr(self, 'server_socket') and self.server_socket:
self.server_socket.close()
self.server_socket = None
@@ -499,6 +562,9 @@ class QemuRunner:
return True
return False
def run_monitor(self, command, timeout=60):
return self.qmp.cmd(command)
def run_serial(self, command, raw=False, timeout=60):
# We assume target system have echo to get command status
if not raw:

View File

@@ -30,7 +30,7 @@ SRC_URI = "https://download.qemu.org/${BPN}-${PV}.tar.xz \
file://mmap2.patch \
file://determinism.patch \
file://0001-tests-meson.build-use-relative-path-to-refer-to-file.patch \
file://CVE-2021-20203.patch \
file://CVE-2021-20203.patch \
file://CVE-2020-35517_1.patch \
file://CVE-2020-35517_2.patch \
file://CVE-2020-35517_3.patch \