qemu-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [RFC PATCH] python: add qmp-send program to send raw qmp commands to


From: Daniel P . Berrangé
Subject: Re: [RFC PATCH] python: add qmp-send program to send raw qmp commands to qemu
Date: Wed, 16 Mar 2022 10:24:51 +0000
User-agent: Mutt/2.1.5 (2021-12-30)

On Wed, Mar 16, 2022 at 10:54:55AM +0100, Damien Hedde wrote:
> It takes an input file containing raw qmp commands (concatenated json
> dicts) and send all commands one by one to a qmp server. When one
> command fails, it exits.
> 
> As a convenience, it can also wrap the qemu process to avoid having
> to start qemu in background. When wrapping qemu, the program returns
> only when the qemu process terminates.
> 
> Signed-off-by: Damien Hedde <damien.hedde@greensocs.com>
> ---
> 
> Hi all,
> 
> Following our discussion, I've started this. What do you think ?
> 
> I tried to follow Daniel's qmp-shell-wrap. I think it is
> better to have similar options (eg: logging). There is also room
> for factorizing code if we want to keep them aligned and ease
> maintenance.

Having CLI similarity to the existing scripts is a good idea.

As a proof of usefulness, it might be worth trying to illustrate
this qmp-send command by converting an I/O test.

Quite a few I/O tests have code that look like:

do_run_qemu()
{
    echo Testing: "$@" | _filter_imgfmt
    $QEMU -nographic -qmp stdio -serial none "$@"
    echo
}


run_qemu()
{
    do_run_qemu "$@" 2>&1 | _filter_testdir | _filter_qemu | _filter_qmp | 
_filter_qemu_io
}

run_qemu <<EOF
{ "execute": "qmp_capabilities" }
{ "execute": "blockdev-add",
   ....
}
{ "execute": "quit" }
EOF

(eg iotests 71)

I would hope this qmp-send command to be able to satisfy that
use case by modifying do_run_qemu like this:

do_run_qemu()
{
    echo Testing: "$@" | _filter_imgfmt
    qmp-send --wrap $QEMU -nographic -serial none "$@"
    echo
}


> There are still some pylint issues (too many branches in main and it
> does not like my context manager if else line). But it's kind of a
> mess to fix theses so I think it's enough for a first version.
> 
> I name that qmp-send as Daniel proposed, maybe qmp-test matches better
> what I'm doing there ?

'qmp-test' is a use case specific name. I think it is better to
name it based on functionality provided rather than anticipated
use case, since use cases evolve over time, hence 'qmp-send'.

> 
> Thanks,
> Damien
> ---
>  python/qemu/aqmp/qmp_send.py | 229 +++++++++++++++++++++++++++++++++++
>  scripts/qmp/qmp-send         |  11 ++
>  2 files changed, 240 insertions(+)
>  create mode 100644 python/qemu/aqmp/qmp_send.py
>  create mode 100755 scripts/qmp/qmp-send
> 
> diff --git a/python/qemu/aqmp/qmp_send.py b/python/qemu/aqmp/qmp_send.py
> new file mode 100644
> index 0000000000..cbca1d0205
> --- /dev/null
> +++ b/python/qemu/aqmp/qmp_send.py
> @@ -0,0 +1,229 @@
> +#
> +# Copyright (C) 2022 Greensocs
> +#
> +# This work is licensed under the terms of the GNU GPL, version 2 or
> +# later.  See the COPYING file in the top-level directory.
> +#
> +
> +"""
> +usage: qmp-send [-h] [-f FILE] [-s SOCKET] [-v] [-p] [--wrap ...]
> +
> +Send raw qmp commands to qemu as long as they succeed. It either connects to 
> a
> +remote qmp server using the provided socket or wrap the qemu process. It 
> stops
> +sending the provided commands when a command fails (disconnection or error
> +response).
> +
> +optional arguments:
> +  -h, --help            show this help message and exit
> +  -f FILE, --file FILE  Input file containing the commands
> +  -s SOCKET, --socket SOCKET
> +                        < UNIX socket path | TCP address:port >
> +  -v, --verbose         Verbose (echo commands sent and received)
> +  -p, --pretty          Pretty-print JSON
> +  --wrap ...            QEMU command line to invoke
> +
> +When qemu wrap option is used, this script waits for qemu to terminate but
> +never send any quit or kill command. This needs to be done manually.
> +"""
> +
> +import argparse
> +import contextlib
> +import json
> +import logging
> +import os
> +from subprocess import Popen
> +import sys
> +from typing import List, TextIO
> +
> +from qemu.aqmp import ConnectError, QMPError, SocketAddrT
> +from qemu.aqmp.legacy import (
> +    QEMUMonitorProtocol,
> +    QMPBadPortError,
> +    QMPMessage,
> +)
> +
> +
> +LOG = logging.getLogger(__name__)
> +
> +
> +class QmpRawDecodeError(Exception):
> +    """
> +    Exception for raw qmp decoding
> +
> +    msg: exception message
> +    lineno: input line of the error
> +    colno: input column of the error
> +    """
> +    def __init__(self, msg: str, lineno: int, colno: int):
> +        self.msg = msg
> +        self.lineno = lineno
> +        self.colno = colno
> +        super().__init__(f"{msg}: line {lineno} column {colno}")
> +
> +
> +class QMPSendError(QMPError):
> +    """
> +    QMP Send Base error class.
> +    """
> +
> +
> +class QMPSend(QEMUMonitorProtocol):
> +    """
> +    QMP Send class.
> +    """
> +    def __init__(self, address: SocketAddrT,
> +                 pretty: bool = False,
> +                 verbose: bool = False,
> +                 server: bool = False):
> +        super().__init__(address, server=server)
> +        self._verbose = verbose
> +        self._pretty = pretty
> +        self._server = server
> +
> +    def setup_connection(self) -> None:
> +        """Setup the connetion with the remote client/server."""
> +        if self._server:
> +            self.accept()
> +        else:
> +            self.connect()
> +
> +    def _print(self, qmp_message: object) -> None:
> +        jsobj = json.dumps(qmp_message,
> +                           indent=4 if self._pretty else None,
> +                           sort_keys=self._pretty)
> +        print(str(jsobj))
> +
> +    def execute_cmd(self, cmd: QMPMessage) -> None:
> +        """Execute a qmp command."""
> +        if self._verbose:
> +            self._print(cmd)
> +        resp = self.cmd_obj(cmd)
> +        if resp is None:
> +            raise QMPSendError("Disconnected")
> +        if self._verbose:
> +            self._print(resp)
> +        if 'error' in resp:
> +            raise QMPSendError(f"Command failed: {resp['error']}")
> +
> +
> +def raw_load(file: TextIO) -> List[QMPMessage]:
> +    """parse a raw qmp command file.
> +
> +    JSON formatted commands can expand on several lines but must
> +    be separated by an end-of-line (two commands can not share the
> +    same line).
> +    File must not end with empty lines.
> +    """
> +    cmds: List[QMPMessage] = []
> +    linecnt = 0
> +    while True:
> +        buf = file.readline()
> +        if not buf:
> +            return cmds
> +        prev_err_pos = None
> +        buf_linecnt = 1
> +        while True:
> +            try:
> +                cmds.append(json.loads(buf))
> +                break
> +            except json.JSONDecodeError as err:
> +                if prev_err_pos == err.pos:
> +                    # adding a line made no progress so
> +                    #  + either we're at EOF and json data is truncated
> +                    #  + or the parsing error is before
> +                    raise QmpRawDecodeError(err.msg, linecnt + err.lineno,
> +                                            err.colno) from err
> +                prev_err_pos = err.pos
> +            buf += file.readline()
> +            buf_linecnt += 1
> +        linecnt += buf_linecnt
> +
> +
> +def report_error(msg: str) -> None:
> +    """Write an error to stderr."""
> +    sys.stderr.write('ERROR: %s\n' % msg)
> +
> +
> +def main() -> None:
> +    """
> +    qmp-send entry point: parse command line arguments and start the REPL.
> +    """
> +    parser = argparse.ArgumentParser(
> +            description="""
> +            Send raw qmp commands to qemu as long as they succeed. It either
> +            connects to a remote qmp server using the provided socket or wrap
> +            the qemu process. It stops sending the provided commands when a
> +            command fails (disconnection or error response).
> +            """,
> +            epilog="""
> +            When qemu wrap option is used, this script waits for qemu
> +            to terminate but never send any quit or kill command. This
> +            needs to be done manually.
> +            """)
> +
> +    parser.add_argument('-f', '--file', action='store',
> +                        help='Input file containing the commands')
> +    parser.add_argument('-s', '--socket', action='store',
> +                        help='< UNIX socket path | TCP address:port >')
> +    parser.add_argument('-v', '--verbose', action='store_true',
> +                        help='Verbose (echo commands sent and received)')
> +    parser.add_argument('-p', '--pretty', action='store_true',
> +                        help='Pretty-print JSON')
> +
> +    parser.add_argument('--wrap', nargs=argparse.REMAINDER,
> +                        help='QEMU command line to invoke')
> +
> +    args = parser.parse_args()
> +
> +    socket = args.socket
> +    wrap_qemu = args.wrap is not None
> +
> +    if wrap_qemu:
> +        if len(args.wrap) != 0:
> +            qemu_cmdline = args.wrap
> +        else:
> +            qemu_cmdline = ["qemu-system-x86_64"]
> +        if socket is None:
> +            socket = "qmp-send-wrap-%d" % os.getpid()
> +        qemu_cmdline += ["-qmp", "unix:%s" % socket]
> +
> +    try:
> +        address = QMPSend.parse_address(socket)
> +    except QMPBadPortError:
> +        parser.error(f"Bad port number: {socket}")
> +        return  # pycharm doesn't know error() is noreturn
> +
> +    try:
> +        with open(args.file, mode='rt', encoding='utf8') as file:
> +            qmp_cmds = raw_load(file)
> +    except QmpRawDecodeError as err:
> +        report_error(str(err))
> +        sys.exit(1)
> +
> +    try:
> +        with QMPSend(address, args.pretty, args.verbose,
> +                     server=wrap_qemu) as qmp:
> +            # starting with python 3.7 we could use contextlib.nullcontext
> +            qemu = Popen(qemu_cmdline) if wrap_qemu else 
> contextlib.suppress()
> +            with qemu:
> +                try:
> +                    qmp.setup_connection()
> +                except ConnectError as err:
> +                    if isinstance(err.exc, OSError):
> +                        report_error(f"Couldn't connect to {socket}: 
> {err!s}")
> +                    else:
> +                        report_error(str(err))
> +                    sys.exit(1)
> +                try:
> +                    for cmd in qmp_cmds:
> +                        qmp.execute_cmd(cmd)
> +                except QMPError as err:
> +                    report_error(str(err))
> +                    sys.exit(1)
> +    finally:
> +        if wrap_qemu:
> +            os.unlink(socket)
> +
> +
> +if __name__ == '__main__':
> +    main()
> diff --git a/scripts/qmp/qmp-send b/scripts/qmp/qmp-send
> new file mode 100755
> index 0000000000..8d3063797c
> --- /dev/null
> +++ b/scripts/qmp/qmp-send
> @@ -0,0 +1,11 @@
> +#!/usr/bin/env python3
> +
> +import os
> +import sys
> +
> +sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 
> 'python'))
> +from qemu.aqmp import qmp_send
> +
> +
> +if __name__ == '__main__':
> +    qmp_send.main()
> -- 
> 2.35.1
> 

With regards,
Daniel
-- 
|: https://berrange.com      -o-    https://www.flickr.com/photos/dberrange :|
|: https://libvirt.org         -o-            https://fstop138.berrange.com :|
|: https://entangle-photo.org    -o-    https://www.instagram.com/dberrange :|




reply via email to

[Prev in Thread] Current Thread [Next in Thread]