From: "Saul Wold" <Saul.Wold@windriver.com>
To: openembedded-core@lists.openembedded.org
Subject: [WIP v2 1/1] qemurunner: add support for qmp cmds
Date: Tue, 10 Nov 2020 20:10:01 -0800 [thread overview]
Message-ID: <20201111041003.864509-2-saul.wold@windriver.com> (raw)
In-Reply-To: <20201111041003.864509-1-saul.wold@windriver.com>
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.
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
Current Issue, the first command succeeds, but the following commands
seem to fail. I think it's something to do with JSON and quoting. I
think the next step is to try and use JSON (which I am not super
familiare with).
[0] https://github.com/qemu/qemu/blob/master/docs/interop/qmp-spec.txt
Signed-off-by: Saul Wold <saul.wold@windriver.com>
---
meta/classes/testimage.bbclass | 7 ++++
meta/lib/oeqa/core/target/qemu.py | 6 +++
meta/lib/oeqa/core/target/ssh.py | 22 ++++++++--
meta/lib/oeqa/targetcontrol.py | 5 +++
meta/lib/oeqa/utils/dump.py | 20 +++++++++
meta/lib/oeqa/utils/qemurunner.py | 70 ++++++++++++++++++++++++++++++-
6 files changed, 126 insertions(+), 4 deletions(-)
diff --git a/meta/classes/testimage.bbclass b/meta/classes/testimage.bbclass
index e3feef02f8..a274865955 100644
--- a/meta/classes/testimage.bbclass
+++ b/meta/classes/testimage.bbclass
@@ -127,6 +127,12 @@ testimage_dump_host () {
netstat -an
}
+testimage_dump_monitor () {
+ '{"execute":"status"}\n'
+ '{"execute":"query-status"}\n'
+ '{"execute":"query-block"}\n'
+}
+
python do_testimage() {
testimage_main(d)
}
@@ -319,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):
diff --git a/meta/lib/oeqa/core/target/qemu.py b/meta/lib/oeqa/core/target/qemu.py
index 0f29414df5..a73d82d9af 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
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)
+ 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")
diff --git a/meta/lib/oeqa/core/target/ssh.py b/meta/lib/oeqa/core/target/ssh.py
index 461448dbc5..faffc8acdf 100644
--- a/meta/lib/oeqa/core/target/ssh.py
+++ b/meta/lib/oeqa/core/target/ssh.py
@@ -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,11 +51,20 @@ 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.
"""
- self.logger.debug("[Running]$ %s" % " ".join(command))
+ self.logger.debug("sgw-[Running]$ %s" % " ".join(command))
starttime = time.time()
status, output = SSHCall(command, self.logger, timeout)
@@ -87,9 +97,15 @@ class OESSHTarget(OETarget):
processTimeout = self.timeout
status, output = self._run(sshCmd, processTimeout, True)
- self.logger.debug('Command: %s\nOutput: %s\n' % (command, output))
- if (status == 255) and (('No route to host') in output):
+ self.logger.debug('Command: %s\nStatus: %d Output: %s\n' % (command, status, output))
+# if (status == 255) and (('No route to host') in output):
+ # for testing right now
+ 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):
diff --git a/meta/lib/oeqa/targetcontrol.py b/meta/lib/oeqa/targetcontrol.py
index 19f5a4ea7e..0d070531c3 100644
--- a/meta/lib/oeqa/targetcontrol.py
+++ b/meta/lib/oeqa/targetcontrol.py
@@ -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')
@@ -147,6 +149,9 @@ 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)
+ self.logger.debug("sgw monitor: %s" % self.monitor_dumper)
+
def deploy(self):
bb.utils.mkdirhier(self.testdir)
diff --git a/meta/lib/oeqa/utils/dump.py b/meta/lib/oeqa/utils/dump.py
index 09a44329e0..4d0357a155 100644
--- a/meta/lib/oeqa/utils/dump.py
+++ b/meta/lib/oeqa/utils/dump.py
@@ -96,3 +96,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 dump_dir:
+ self.dump_dir = dump_dir
+ for cmd in self.cmds:
+ try:
+ output = self.runner.run_monitor(cmd)
+ self._write_dump("qemu_monitor", (cmd + "\n" + output))
+ except:
+ print("Failed to dump montor data")
+ print("Failed CMD: %s" % (cmd))
+
diff --git a/meta/lib/oeqa/utils/qemurunner.py b/meta/lib/oeqa/utils/qemurunner.py
index 77ec939ad7..0ca0d78470 100644
--- a/meta/lib/oeqa/utils/qemurunner.py
+++ b/meta/lib/oeqa/utils/qemurunner.py
@@ -20,6 +20,7 @@ import string
import threading
import codecs
import logging
+from contextlib import closing
from oeqa.utils.dump import HostDumper
from collections import defaultdict
@@ -84,6 +85,12 @@ class QemuRunner:
default_boot_patterns['send_login_user'] = 'root\n'
default_boot_patterns['search_login_succeeded'] = r"root@[a-zA-Z0-9\-]+:~#"
default_boot_patterns['search_cmd_finished'] = r"[a-zA-Z0-9]+@[a-zA-Z0-9\-]+:~#"
+ monitor_cmds = defaultdict(str)
+ monitor_cmds['qmp_cap'] = '{"execute":"qmp_capabilities","arguments":{"enable":["oob"]}}\n'
+ monitor_cmds['cont'] = '{"execute":"cont"}\n'
+ monitor_cmds['quit'] = '{"execute":"quit"}\n'
+ monitor_cmds['preconfig'] = '{"execute":"x-exit-preconfig"}\n'
+ self.monitor_cmds = monitor_cmds
# Only override patterns that were set e.g. login user TESTIMAGE_BOOT_PATTERNS[send_login_user] = "webserver\n"
for pattern in accepted_patterns:
@@ -168,10 +175,17 @@ 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):
+ # Find a free socket port that can be used by the QEMU Monitor console
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
+ s.bind(('', 0))
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ qmp_port = s.getsockname()[1]
+
try:
if self.serial_ports >= 2:
self.threadsock, threadport = self.create_socket()
self.server_socket, self.serverport = self.create_socket()
+
except socket.error as msg:
self.logger.error("Failed to create listening socket: %s" % msg[1])
return False
@@ -185,6 +199,9 @@ class QemuRunner:
if os.path.exists(self.qemu_pidfile):
os.remove(self.qemu_pidfile)
self.qemuparams = 'bootparams="{0}" qemuparams="-pidfile {1}"'.format(bootparams, self.qemu_pidfile)
+ qemuparams += ' -S -qmp tcp:localhost:%s,server,wait' % (qmp_port)
+ qemuparams += ' -monitor tcp:localhost:4444,server,nowait'
+
if qemuparams:
self.qemuparams = self.qemuparams[:-1] + " " + qemuparams + " " + '\"'
@@ -250,6 +267,28 @@ class QemuRunner:
if self.runqemu_exited:
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
+ try:
+ self.monitor_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.monitor_socket.connect(("127.0.0.1", qmp_port))
+ self.monitor_socket.setblocking(False)
+
+ except socket.error as msg:
+ self.logger.error("Failed to connect qemu monitor socket: %s" % msg[1])
+ return False
+
+ # Run an empty command to get the initial connection details, then
+ # send the qmp_capabilities command, this is required to initialize
+ # the monitor console
+ mon_output = self.run_monitor("")
+ self.logger.debug("Monitor: %s" % mon_output)
+ mon_output = self.run_monitor(self.monitor_cmds['qmp_cap'], timeout=120)
+ self.logger.debug("Monitor: %s" % mon_output)
+ mon_output = self.run_monitor(self.monitor_cmds['cont'], timeout=120)
+ self.logger.debug("Monitor: %s" % mon_output)
if not self.is_alive():
self.logger.error("Qemu pid didn't appear in %s seconds (%s)" %
@@ -338,6 +377,7 @@ class QemuRunner:
reachedlogin = False
stopread = False
qemusock = None
+ monsock = None
bootlog = b''
data = b''
while time.time() < endtime and not stopread:
@@ -376,7 +416,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)" %
@@ -437,6 +476,9 @@ class QemuRunner:
self.runqemu.stdout.close()
self.runqemu_exited = True
+ if hasattr(self, 'monitor_socket') and self.monitor_socket:
+ self.monitor_socket.close()
+ self.monitor_socket = None
if hasattr(self, 'server_socket') and self.server_socket:
self.server_socket.close()
self.server_socket = None
@@ -495,6 +537,32 @@ class QemuRunner:
return True
return False
+ def run_monitor(self, command, timeout=60):
+ data = ''
+ self.monitor_socket.sendall(command.encode('utf-8'))
+ start = time.time()
+ end = start + timeout
+ while True:
+ now = time.time()
+ if now >= end:
+ data += "<<< run_monitor(): command timed out after %d seconds without output >>>\r\n\r\n" % timeout
+ break
+ try:
+ sread, _, _ = select.select([self.monitor_socket],[],[], end - now)
+ except InterruptedError:
+ continue
+ if sread:
+ answer = self.monitor_socket.recv(1024)
+ if answer:
+ data += answer.decode('utf-8')
+ if data.rfind('\r\n') != -1:
+ break;
+ else:
+ raise Exception("No data on monitor socket")
+
+ if data:
+ return (str(data))
+
def run_serial(self, command, raw=False, timeout=60):
# We assume target system have echo to get command status
if not raw:
--
2.25.1
next prev parent reply other threads:[~2020-11-11 4:10 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2020-11-11 4:10 [WIP v2 0/1] Add Qemu Monitor Support Saul Wold
2020-11-11 4:10 ` Saul Wold [this message]
2020-11-11 14:35 ` [OE-core] [WIP v2 1/1] qemurunner: add support for qmp cmds Joshua Watt
2020-11-11 14:54 ` Joshua Watt
2020-11-11 4:10 ` [PATCH] [WIP] qemurunner.py: qemu as client Saul Wold
2020-11-11 4:10 ` [PATCH] qemurunner: add support for qmp cmds Saul Wold
[not found] ` <164658B07603FC62.32470@lists.openembedded.org>
2020-11-11 4:19 ` [OE-core] [PATCH] [WIP] qemurunner.py: qemu as client Saul Wold
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20201111041003.864509-2-saul.wold@windriver.com \
--to=saul.wold@windriver.com \
--cc=openembedded-core@lists.openembedded.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox