[libvirt PATCH] tools: add virt-qmp-proxy for proxying QMP clients to libvirt QEMU guests

Peter Krempa pkrempa at redhat.com
Fri May 27 14:14:27 UTC 2022


On Fri, May 27, 2022 at 10:47:58 +0100, Daniel P. Berrangé wrote:
> Libvirt provides QMP passthrough APIs for the QEMU driver and these are
> exposed in virsh. It is not especially pleasant, however, using the raw
> QMP JSON syntax. QEMU has a tool 'qmp-shell' which can speak QMP and
> exposes a human friendly interactive shell. It is not possible to use
> this with libvirt managed guest, however, since only one client can
> attach to he QMP socket at any point in time.
> 
> The virt-qmp-proxy tool aims to solve this problem. It opens a UNIX
> socket and listens for incoming client connections, speaking QMP on
> the connected socket. It will forward any QMP commands received onto
> the running libvirt QEMU guest, and forward any replies back to the
> QMP client.
> 
>   $ virsh start demo
>   $ virt-qmp-proxy demo demo.qmp &
>   $ qmp-shell demo.qmp
>   Welcome to the QMP low-level shell!
>   Connected to QEMU 6.2.0
> 
>   (QEMU) query-kvm
>   {
>       "return": {
>           "enabled": true,
>           "present": true
>       }
>   }
> 
> Note this tool of course has the same risks as the raw libvirt
> QMP passthrough. It is safe to run query commands to fetch information
> but commands which change the QEMU state risk disrupting libvirt's
> management of QEMU, potentially resulting in data loss/corruption in
> the worst case.
> 
> Signed-off-by: Daniel P. Berrangé <berrange at redhat.com>
> ---
> 
> CC'ing QEMU since this is likely of interest to maintainers and users
> who work with QEMU and libvirt
> 
> Note this impl is fairly crude in that it assumes it is receiving
> the QMP commands linewise one at a time. None the less it is good
> enough to work with qmp-shell already, so I figured it was worth
> exposing to the world. It also lacks support for forwarding events
> back to the QMP client.
> 
>  docs/manpages/meson.build        |   1 +
>  docs/manpages/virt-qmp-proxy.rst | 123 ++++++++++++++++++++++++++++
>  tools/meson.build                |   5 ++
>  tools/virt-qmp-proxy             | 133 +++++++++++++++++++++++++++++++
>  4 files changed, 262 insertions(+)
>  create mode 100644 docs/manpages/virt-qmp-proxy.rst
>  create mode 100755 tools/virt-qmp-proxy

[...]

> diff --git a/docs/manpages/virt-qmp-proxy.rst b/docs/manpages/virt-qmp-proxy.rst
> new file mode 100644
> index 0000000000..94679406ab
> --- /dev/null
> +++ b/docs/manpages/virt-qmp-proxy.rst
> @@ -0,0 +1,123 @@
> +==============
> +virt-qmp-proxy
> +==============
> +
> +--------------------------------------------------
> +Expose a QMP proxy server for a libvirt QEMU guest
> +--------------------------------------------------
> +
> +:Manual section: 1
> +:Manual group: Virtualization Support
> +
> +.. contents::
> +
> +
> +SYNOPSIS
> +========
> +
> +``virt-qmp-proxy`` [*OPTION*]... *DOMAIN* *QMP-SOCKET-PATH*
> +
> +
> +DESCRIPTION
> +===========
> +
> +This tool provides a way to expose a QMP proxy server that communicates
> +with a QEMU guest managed by libvirt. This enables standard QMP client
> +tools to interact with libvirt managed guests.
> +
> +**NOTE: use of this tool will result in the running QEMU guest being
> +marked as tainted.** It is strongly recommended that this tool *only be
> +used to send commands which query information* about the running guest.
> +If this tool is used to make changes to the state of the guest, this
> +may have negative interactions with the QEMU driver, resulting in an
> +inability to manage the guest operation thereafter, and in the worst
> +case **potentially lead to data loss or corruption**.
> +
> +The ``virt-qmp-proxy`` program will listen on a UNIX socket for incoming
> +client connections, and run the QMP protocol over the connection. Any
> +commands received will be sent to the running libvirt guest, and replies
> +sent back.
> +
> +The ``virt-qemu-proxy`` program may be interrupted (eg Ctrl-C) when it
> +is no longer required. The libvirt QEMU guest will continue running.
> +
> +
> +OPTIONS
> +=======
> +
> +*DOMAIN*
> +
> +The ID or UUID or Name of the libvirt QEMU guest.
> +
> +*QMP-SOCKET-PATH*
> +
> +The filesystem path at which to run the QMP server, listening for
> +incoming connections.
> +
> +``-c`` *CONNECTION-URI*
> +``--connect``\ =\ *CONNECTION-URI*
> +
> +The URI for the connection to the libvirt QEMU driver. If omitted,
> +a URI will be auto-detected.
> +
> +``-v``, ``--verbose``
> +
> +Run in verbose mode, printing all QMP commands and replies that
> +are handled.
> +
> +``-h``, ``--help``
> +
> +Display the command line help.
> +
> +
> +EXIT STATUS
> +===========
> +
> +Upon successful shutdown, an exit status of 0 will be set. Upon
> +failure a non-zero status will be set.
> +
> +
> +AUTHOR
> +======
> +
> +Daniel P. Berrangé
> +
> +
> +BUGS
> +====
> +
> +Please report all bugs you discover.  This should be done via either:
> +
> +#. the mailing list
> +
> +   `https://libvirt.org/contact.html <https://libvirt.org/contact.html>`_
> +
> +#. the bug tracker
> +
> +   `https://libvirt.org/bugs.html <https://libvirt.org/bugs.html>`_
> +
> +Alternatively, you may report bugs to your software distributor / vendor.
> +
> +NOTE: at this time there is no support for forwarding QMP events back
> +to the clients

Also add caveat about FD passing support.

[...]

> diff --git a/tools/virt-qmp-proxy b/tools/virt-qmp-proxy
> new file mode 100755
> index 0000000000..57f9759fab
> --- /dev/null
> +++ b/tools/virt-qmp-proxy
> @@ -0,0 +1,133 @@
> +#!/usr/bin/env python3
> +
> +import argparse
> +import libvirt
> +import libvirt_qemu
> +import os
> +import re
> +import socket
> +import sys
> +import json
> +
> +
> +def get_domain(uri, domstr):
> +    conn = libvirt.open(uri)
> +
> +    dom = None
> +    if re.match(r'^\d+$', domstr):
> +        dom = conn.lookupByID(int(domstr))
> +    elif re.match(r'^[+a-f0-9]+$', domstr):

This works very poorly if you have a VM named for example 'cd' or any
combination of just letters abcdef.

> +        dom = conn.lookupByUUIDString(domstr)
> +    else:
> +        dom = conn.lookupByName(domstr)
> +
> +    if not dom.isActive():
> +        raise Exception(
> +            "Domain must be running to validate measurement")

This should mention the current usage or a generic error ;)

> +
> +    return conn, dom
> +
> +
> +def qmp_server(conn, dom, client, verbose):
> +    ver = conn.getVersion()

So this gets the version of the "default" emulator version, but if your
VM is using a custom one this will report it wrong.

E.g in my case I have a git qemu for a VM:

 517 2022-05-27 14:01:09.604+0000: 373562: debug : qemuMonitorJSONIOProcessLine:199 : Line [{"QMP": {"version": {"qemu": {"micro": 50, "minor": 0, "major": 7}, "package": "v7.0.0-1253-g2417cbd591"}, "capabilities": ["oob"]}}]



> +    major = int(ver / 1000000) % 1000
> +    minor = int(ver / 1000) % 1000
> +    micro = ver % 1000
> +
> +    greetingobj = {
> +        "QMP": {
> +            "version": {
> +                "qemu": {
> +                    "major": major,
> +                    "minor": minor,
> +                    "micro": micro,
> +                },
> +                "package": f"qemu-{major}.{minor}.{micro}",
> +            },
> +            "capabilities": [
> +                "oob"
> +            ],
> +        }
> +    }

But when I conect I get:

{"QMP": {"version": {"qemu": {"major": 7, "minor": 0, "micro": 0}, "package": "qemu-7.0.0"}, "capabilities": ["oob"]}}


At the very least this should be documented.

> +    greeting = json.dumps(greetingobj) + "\r\n"
> +    if verbose:
> +        print(greeting, end='')
> +    client.send(greeting.encode("utf-8"))
> +
> +    while True:
> +        # XXX shouldn't blindly assume this one read
> +        # will fully capture one-and-only-one cmd
> +        cmd = client.recv(1024).decode('utf8')

IIUC this limits the buffer to 1k max. Libvirt's RPC supports up to 4M.
1k could be limiting with some commands such as blockdev-add.

> +        if verbose:
> +            print(cmd)
> +
> +        if cmd == "":
> +            break
> +
> +        if "qmp_capabilities" in cmd:
> +            capabilitiesobj = {
> +                "return": {},
> +            }
> +            capabilities = json.dumps(capabilitiesobj) + "\r\n"
> +            if verbose:
> +                print(capabilities, end='')
> +            client.send(capabilities.encode("utf-8"))
> +            continue
> +
> +        id = None
> +        if "id" in cmd:
> +            id = cmd[id]
> +
> +        res = libvirt_qemu.qemuMonitorCommand(dom, cmd, 0)

If 'cmd' is not JSON this breaks horribly:

$ tools/virt-qmp-proxy 2 /tmp/asdf
libvirt:  error : internal error: cannot parse json test
: lexical error: invalid string in json text.
                                       test
                     (right here) ------^
tools/virt-qmp-proxy: internal error: cannot parse json test
: lexical error: invalid string in json text.
                                       test
                     (right here) ------^

and stops working, while real qemu behaves differently:

$ qemu-system-x86_64 -qmp stdio
{"QMP": {"version": {"qemu": {"micro": 0, "minor": 0, "major": 7}, "package": "qemu-7.0.0-2.fc35"}, "capabilities": ["oob"]}}


help
{"error": {"class": "GenericError", "desc": "JSON parse error, invalid keyword 'help'"}}


Also since it's just a simple loop without event handling from qemu.
E.g. if I destroy the VM while it's running it simply waits. When I
issue another command, then the proxy exits:

$ tools/virt-qmp-proxy 2 /tmp/asdf
libvirt: Domain Config error : Requested operation is not valid: domain is not running
tools/virt-qmp-proxy: Requested operation is not valid: domain is not running


but the client itself just sees a closed socket.

Given the use case it's not a big problem but it should be at least
mentioned in the docs.

> +
> +        resobj = json.loads(res)
> +        del resobj["id"]
> +        if id is not None:
> +            resobj["id"] = id
> +        res = json.dumps(resobj) + "\r\n"
> +        if verbose:
> +            print(res, end='')
> +
> +        client.send(res.encode('utf8'))
> +
> +
> +def parse_commandline():
> +    parser = argparse.ArgumentParser(description="Libvirt QMP proxy")
> +    parser.add_argument("--connect", "-c",
> +                        help="Libvirt QEMU driver connection URI")
> +    parser.add_argument("--verbose", "-v", action='store_true',
> +                        help="Display QMP traffic")
> +    parser.add_argument("domain", metavar="DOMAIN",
> +                        help="Libvirt guest domain ID/UUID/Name")
> +    parser.add_argument("sockpath", metavar="QMP-SOCK-PATH",
> +                        help="UNIX socket path for QMP server")
> +
> +    return parser.parse_args()
> +
> +
> +def main():
> +    args = parse_commandline()
> +
> +    conn, dom = get_domain(args.connect, args.domain)
> +
> +    if conn.getType() != "QEMU":
> +        raise Exception("QMP proxy requires a QEMU driver connection not %s" %
> +                        conn.getType())
> +
> +    sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
> +    if os.path.exists(args.sockpath):
> +        os.unlink(args.sockpath)
> +    sock.bind(args.sockpath)
> +    sock.listen(1)
> +
> +    while True:
> +        client, peeraddr = sock.accept()
> +        qmp_server(conn, dom, client, args.verbose)
> +
> +
> +try:
> +    main()
> +    sys.exit(0)
> +except Exception as e:
> +    print("%s: %s" % (sys.argv[0], str(e)))
> +    sys.exit(1)
> -- 
> 2.36.1
> 


More information about the libvir-list mailing list