All of lore.kernel.org
 help / color / mirror / Atom feed
From: John Snow <jsnow@redhat.com>
To: qemu-devel@nongnu.org
Cc: Peter Maydell <peter.maydell@linaro.org>,
	Markus Armbruster <armbru@redhat.com>,
	John Snow <jsnow@redhat.com>,
	Eduardo Habkost <ehabkost@redhat.com>,
	Cleber Rosa <crosa@redhat.com>
Subject: [PULL 04/72] python/qmp: Add qom script rewrites
Date: Fri, 18 Jun 2021 19:03:47 -0400	[thread overview]
Message-ID: <20210618230455.2891199-5-jsnow@redhat.com> (raw)
In-Reply-To: <20210618230455.2891199-1-jsnow@redhat.com>

Inspired by qom-set, qom-get, qom-tree and qom-list; combine all four of
those scripts into a single script.

A later addition of qom-fuse as an 'extension' necessitates that some
common features are split out and shared between them.

Signed-off-by: John Snow <jsnow@redhat.com>
Message-id: 20210603003719.1321369-5-jsnow@redhat.com
Signed-off-by: John Snow <jsnow@redhat.com>
---
 python/qemu/qmp/qom.py        | 262 ++++++++++++++++++++++++++++++++++
 python/qemu/qmp/qom_common.py | 178 +++++++++++++++++++++++
 2 files changed, 440 insertions(+)
 create mode 100644 python/qemu/qmp/qom.py
 create mode 100644 python/qemu/qmp/qom_common.py

diff --git a/python/qemu/qmp/qom.py b/python/qemu/qmp/qom.py
new file mode 100644
index 0000000000..7fe1448b5d
--- /dev/null
+++ b/python/qemu/qmp/qom.py
@@ -0,0 +1,262 @@
+"""
+QEMU Object Model testing tools.
+
+usage: qom [-h] {set,get,list,tree} ...
+
+Query and manipulate QOM data
+
+optional arguments:
+  -h, --help           show this help message and exit
+
+QOM commands:
+  {set,get,list,tree}
+    set                Set a QOM property value
+    get                Get a QOM property value
+    list               List QOM properties at a given path
+    tree               Show QOM tree from a given path
+"""
+##
+# Copyright John Snow 2020, for Red Hat, Inc.
+# Copyright IBM, Corp. 2011
+#
+# Authors:
+#  John Snow <jsnow@redhat.com>
+#  Anthony Liguori <aliguori@amazon.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2 or later.
+# See the COPYING file in the top-level directory.
+#
+# Based on ./scripts/qmp/qom-[set|get|tree|list]
+##
+
+import argparse
+
+from . import QMPResponseError
+from .qom_common import QOMCommand
+
+
+class QOMSet(QOMCommand):
+    """
+    QOM Command - Set a property to a given value.
+
+    usage: qom-set [-h] [--socket SOCKET] <path>.<property> <value>
+
+    Set a QOM property value
+
+    positional arguments:
+      <path>.<property>     QOM path and property, separated by a period '.'
+      <value>               new QOM property value
+
+    optional arguments:
+      -h, --help            show this help message and exit
+      --socket SOCKET, -s SOCKET
+                            QMP socket path or address (addr:port). May also be
+                            set via QMP_SOCKET environment variable.
+    """
+    name = 'set'
+    help = 'Set a QOM property value'
+
+    @classmethod
+    def configure_parser(cls, parser: argparse.ArgumentParser) -> None:
+        super().configure_parser(parser)
+        cls.add_path_prop_arg(parser)
+        parser.add_argument(
+            'value',
+            metavar='<value>',
+            action='store',
+            help='new QOM property value'
+        )
+
+    def __init__(self, args: argparse.Namespace):
+        super().__init__(args)
+        self.path, self.prop = args.path_prop.rsplit('.', 1)
+        self.value = args.value
+
+    def run(self) -> int:
+        rsp = self.qmp.command(
+            'qom-set',
+            path=self.path,
+            property=self.prop,
+            value=self.value
+        )
+        print(rsp)
+        return 0
+
+
+class QOMGet(QOMCommand):
+    """
+    QOM Command - Get a property's current value.
+
+    usage: qom-get [-h] [--socket SOCKET] <path>.<property>
+
+    Get a QOM property value
+
+    positional arguments:
+      <path>.<property>     QOM path and property, separated by a period '.'
+
+    optional arguments:
+      -h, --help            show this help message and exit
+      --socket SOCKET, -s SOCKET
+                            QMP socket path or address (addr:port). May also be
+                            set via QMP_SOCKET environment variable.
+    """
+    name = 'get'
+    help = 'Get a QOM property value'
+
+    @classmethod
+    def configure_parser(cls, parser: argparse.ArgumentParser) -> None:
+        super().configure_parser(parser)
+        cls.add_path_prop_arg(parser)
+
+    def __init__(self, args: argparse.Namespace):
+        super().__init__(args)
+        try:
+            tmp = args.path_prop.rsplit('.', 1)
+        except ValueError as err:
+            raise ValueError('Invalid format for <path>.<property>') from err
+        self.path = tmp[0]
+        self.prop = tmp[1]
+
+    def run(self) -> int:
+        rsp = self.qmp.command(
+            'qom-get',
+            path=self.path,
+            property=self.prop
+        )
+        if isinstance(rsp, dict):
+            for key, value in rsp.items():
+                print(f"{key}: {value}")
+        else:
+            print(rsp)
+        return 0
+
+
+class QOMList(QOMCommand):
+    """
+    QOM Command - List the properties at a given path.
+
+    usage: qom-list [-h] [--socket SOCKET] <path>
+
+    List QOM properties at a given path
+
+    positional arguments:
+      <path>                QOM path
+
+    optional arguments:
+      -h, --help            show this help message and exit
+      --socket SOCKET, -s SOCKET
+                            QMP socket path or address (addr:port). May also be
+                            set via QMP_SOCKET environment variable.
+    """
+    name = 'list'
+    help = 'List QOM properties at a given path'
+
+    @classmethod
+    def configure_parser(cls, parser: argparse.ArgumentParser) -> None:
+        super().configure_parser(parser)
+        parser.add_argument(
+            'path',
+            metavar='<path>',
+            action='store',
+            help='QOM path',
+        )
+
+    def __init__(self, args: argparse.Namespace):
+        super().__init__(args)
+        self.path = args.path
+
+    def run(self) -> int:
+        rsp = self.qom_list(self.path)
+        for item in rsp:
+            if item.child:
+                print(f"{item.name}/")
+            elif item.link:
+                print(f"@{item.name}/")
+            else:
+                print(item.name)
+        return 0
+
+
+class QOMTree(QOMCommand):
+    """
+    QOM Command - Show the full tree below a given path.
+
+    usage: qom-tree [-h] [--socket SOCKET] [<path>]
+
+    Show QOM tree from a given path
+
+    positional arguments:
+      <path>                QOM path
+
+    optional arguments:
+      -h, --help            show this help message and exit
+      --socket SOCKET, -s SOCKET
+                            QMP socket path or address (addr:port). May also be
+                            set via QMP_SOCKET environment variable.
+    """
+    name = 'tree'
+    help = 'Show QOM tree from a given path'
+
+    @classmethod
+    def configure_parser(cls, parser: argparse.ArgumentParser) -> None:
+        super().configure_parser(parser)
+        parser.add_argument(
+            'path',
+            metavar='<path>',
+            action='store',
+            help='QOM path',
+            nargs='?',
+            default='/'
+        )
+
+    def __init__(self, args: argparse.Namespace):
+        super().__init__(args)
+        self.path = args.path
+
+    def _list_node(self, path: str) -> None:
+        print(path)
+        items = self.qom_list(path)
+        for item in items:
+            if item.child:
+                continue
+            try:
+                rsp = self.qmp.command('qom-get', path=path,
+                                       property=item.name)
+                print(f"  {item.name}: {rsp} ({item.type})")
+            except QMPResponseError as err:
+                print(f"  {item.name}: <EXCEPTION: {err!s}> ({item.type})")
+        print('')
+        for item in items:
+            if not item.child:
+                continue
+            if path == '/':
+                path = ''
+            self._list_node(f"{path}/{item.name}")
+
+    def run(self) -> int:
+        self._list_node(self.path)
+        return 0
+
+
+def main() -> int:
+    """QOM script main entry point."""
+    parser = argparse.ArgumentParser(
+        description='Query and manipulate QOM data'
+    )
+    subparsers = parser.add_subparsers(
+        title='QOM commands',
+        dest='command'
+    )
+
+    for command in QOMCommand.__subclasses__():
+        command.register(subparsers)
+
+    args = parser.parse_args()
+
+    if args.command is None:
+        parser.error('Command not specified.')
+        return 1
+
+    cmd_class = args.cmd_class
+    assert isinstance(cmd_class, type(QOMCommand))
+    return cmd_class.command_runner(args)
diff --git a/python/qemu/qmp/qom_common.py b/python/qemu/qmp/qom_common.py
new file mode 100644
index 0000000000..f82b16772d
--- /dev/null
+++ b/python/qemu/qmp/qom_common.py
@@ -0,0 +1,178 @@
+"""
+QOM Command abstractions.
+"""
+##
+# Copyright John Snow 2020, for Red Hat, Inc.
+# Copyright IBM, Corp. 2011
+#
+# Authors:
+#  John Snow <jsnow@redhat.com>
+#  Anthony Liguori <aliguori@amazon.com>
+#
+# This work is licensed under the terms of the GNU GPL, version 2 or later.
+# See the COPYING file in the top-level directory.
+#
+# Based on ./scripts/qmp/qom-[set|get|tree|list]
+##
+
+import argparse
+import os
+import sys
+from typing import (
+    Any,
+    Dict,
+    List,
+    Optional,
+    Type,
+    TypeVar,
+)
+
+from . import QEMUMonitorProtocol, QMPError
+
+
+# The following is needed only for a type alias.
+Subparsers = argparse._SubParsersAction  # pylint: disable=protected-access
+
+
+class ObjectPropertyInfo:
+    """
+    Represents the return type from e.g. qom-list.
+    """
+    def __init__(self, name: str, type_: str,
+                 description: Optional[str] = None,
+                 default_value: Optional[object] = None):
+        self.name = name
+        self.type = type_
+        self.description = description
+        self.default_value = default_value
+
+    @classmethod
+    def make(cls, value: Dict[str, Any]) -> 'ObjectPropertyInfo':
+        """
+        Build an ObjectPropertyInfo from a Dict with an unknown shape.
+        """
+        assert value.keys() >= {'name', 'type'}
+        assert value.keys() <= {'name', 'type', 'description', 'default-value'}
+        return cls(value['name'], value['type'],
+                   value.get('description'),
+                   value.get('default-value'))
+
+    @property
+    def child(self) -> bool:
+        """Is this property a child property?"""
+        return self.type.startswith('child<')
+
+    @property
+    def link(self) -> bool:
+        """Is this property a link property?"""
+        return self.type.startswith('link<')
+
+
+CommandT = TypeVar('CommandT', bound='QOMCommand')
+
+
+class QOMCommand:
+    """
+    Represents a QOM sub-command.
+
+    :param args: Parsed arguments, as returned from parser.parse_args.
+    """
+    name: str
+    help: str
+
+    def __init__(self, args: argparse.Namespace):
+        if args.socket is None:
+            raise QMPError("No QMP socket path or address given")
+        self.qmp = QEMUMonitorProtocol(
+            QEMUMonitorProtocol.parse_address(args.socket)
+        )
+        self.qmp.connect()
+
+    @classmethod
+    def register(cls, subparsers: Subparsers) -> None:
+        """
+        Register this command with the argument parser.
+
+        :param subparsers: argparse subparsers object, from "add_subparsers".
+        """
+        subparser = subparsers.add_parser(cls.name, help=cls.help,
+                                          description=cls.help)
+        cls.configure_parser(subparser)
+
+    @classmethod
+    def configure_parser(cls, parser: argparse.ArgumentParser) -> None:
+        """
+        Configure a parser with this command's arguments.
+
+        :param parser: argparse parser or subparser object.
+        """
+        default_path = os.environ.get('QMP_SOCKET')
+        parser.add_argument(
+            '--socket', '-s',
+            dest='socket',
+            action='store',
+            help='QMP socket path or address (addr:port).'
+            ' May also be set via QMP_SOCKET environment variable.',
+            default=default_path
+        )
+        parser.set_defaults(cmd_class=cls)
+
+    @classmethod
+    def add_path_prop_arg(cls, parser: argparse.ArgumentParser) -> None:
+        """
+        Add the <path>.<proptery> positional argument to this command.
+
+        :param parser: The parser to add the argument to.
+        """
+        parser.add_argument(
+            'path_prop',
+            metavar='<path>.<property>',
+            action='store',
+            help="QOM path and property, separated by a period '.'"
+        )
+
+    def run(self) -> int:
+        """
+        Run this command.
+
+        :return: 0 on success, 1 otherwise.
+        """
+        raise NotImplementedError
+
+    def qom_list(self, path: str) -> List[ObjectPropertyInfo]:
+        """
+        :return: a strongly typed list from the 'qom-list' command.
+        """
+        rsp = self.qmp.command('qom-list', path=path)
+        # qom-list returns List[ObjectPropertyInfo]
+        assert isinstance(rsp, list)
+        return [ObjectPropertyInfo.make(x) for x in rsp]
+
+    @classmethod
+    def command_runner(
+            cls: Type[CommandT],
+            args: argparse.Namespace
+    ) -> int:
+        """
+        Run a fully-parsed subcommand, with error-handling for the CLI.
+
+        :return: The return code from `.run()`.
+        """
+        try:
+            cmd = cls(args)
+            return cmd.run()
+        except QMPError as err:
+            print(f"{type(err).__name__}: {err!s}", file=sys.stderr)
+            return -1
+
+    @classmethod
+    def entry_point(cls) -> int:
+        """
+        Build this command's parser, parse arguments, and run the command.
+
+        :return: `run`'s return code.
+        """
+        parser = argparse.ArgumentParser(description=cls.help)
+        cls.configure_parser(parser)
+        args = parser.parse_args()
+        return cls.command_runner(args)
-- 
2.31.1



  parent reply	other threads:[~2021-06-18 23:09 UTC|newest]

Thread overview: 75+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-06-18 23:03 [PULL 00/72] Python patches John Snow
2021-06-18 23:03 ` [PULL 01/72] python/pipenv: Update Pipfile.lock John Snow
2021-06-18 23:03 ` [PULL 02/72] python/qmp: Fix type of SocketAddrT John Snow
2021-06-18 23:03 ` [PULL 03/72] python/qmp: add parse_address classmethod John Snow
2021-06-18 23:03 ` John Snow [this message]
2021-06-18 23:03 ` [PULL 05/72] python/qmp: add qom script entry points John Snow
2021-06-18 23:03 ` [PULL 06/72] scripts/qmp: redirect qom-xxx scripts to python/qemu/qmp/ John Snow
2021-06-18 23:03 ` [PULL 07/72] scripts/qom-fuse: apply isort rules John Snow
2021-06-18 23:03 ` [PULL 08/72] scripts/qom-fuse: apply flake8 rules John Snow
2021-06-18 23:03 ` [PULL 09/72] python: Add 'fh' to known-good variable names John Snow
2021-06-18 23:03 ` [PULL 10/72] scripts/qom-fuse: Apply pylint rules John Snow
2021-06-18 23:03 ` [PULL 11/72] scripts/qom-fuse: Add docstrings John Snow
2021-06-18 23:03 ` [PULL 12/72] scripts/qom-fuse: Convert to QOMCommand John Snow
2021-06-18 23:03 ` [PULL 13/72] scripts/qom-fuse: use QOMCommand.qom_list() John Snow
2021-06-18 23:03 ` [PULL 14/72] scripts/qom-fuse: ensure QOMFuse.read always returns bytes John Snow
2021-06-18 23:03 ` [PULL 15/72] scripts/qom-fuse: add static type hints John Snow
2021-06-18 23:03 ` [PULL 16/72] python: add optional FUSE dependencies John Snow
2021-06-18 23:04 ` [PULL 17/72] scripts/qom-fuse: move to python/qemu/qmp/qom_fuse.py John Snow
2021-06-18 23:04 ` [PULL 18/72] scripts/qom-fuse: add redirection shim to python/qemu/qmp/qom-fuse.py John Snow
2021-06-18 23:04 ` [PULL 19/72] python/qmp: add fuse command to 'qom' tools John Snow
2021-06-18 23:04 ` [PULL 20/72] scripts/qemu-ga-client: apply isort rules John Snow
2021-06-18 23:04 ` [PULL 21/72] scripts/qemu-ga-client: apply (most) flake8 rules John Snow
2021-06-18 23:04 ` [PULL 22/72] scripts/qemu-ga-client: Fix exception handling John Snow
2021-06-18 23:04 ` [PULL 23/72] scripts/qemu-ga-client: replace deprecated optparse with argparse John Snow
2021-06-18 23:04 ` [PULL 24/72] scripts/qemu-ga-client: add module docstring John Snow
2021-06-18 23:04 ` [PULL 25/72] scripts/qemu-ga-client: apply (most) pylint rules John Snow
2021-06-18 23:04 ` [PULL 26/72] python/qmp: Correct type of QMPReturnValue John Snow
2021-06-18 23:04 ` [PULL 27/72] scripts/qemu-ga-client: add mypy type hints John Snow
2021-06-18 23:04 ` [PULL 28/72] scripts/qemu-ga-client: move to python/qemu/qmp/qemu_ga_client.py John Snow
2021-06-18 23:04 ` [PULL 29/72] python/qemu-ga-client: add entry point John Snow
2021-06-18 23:04 ` [PULL 30/72] scripts/qemu-ga-client: Add forwarder shim John Snow
2021-06-18 23:04 ` [PULL 31/72] scripts/qmp-shell: apply isort rules John Snow
2021-06-18 23:04 ` [PULL 32/72] scripts/qmp-shell: Apply flake8 rules John Snow
2021-06-18 23:04 ` [PULL 33/72] scripts/qmp-shell: fix show_banner signature John Snow
2021-06-18 23:04 ` [PULL 34/72] scripts/qmp-shell: fix exception handling John Snow
2021-06-18 23:04 ` [PULL 35/72] scripts/qmp-shell: fix connect method signature John Snow
2021-06-18 23:04 ` [PULL 36/72] scripts/qmp-shell: remove shadowed variable from _print() John Snow
2021-06-18 23:04 ` [PULL 37/72] scripts/qmp-shell: use @classmethod where appropriate John Snow
2021-06-18 23:04 ` [PULL 38/72] scripts/qmp-shell: Use python3-style super() John Snow
2021-06-18 23:04 ` [PULL 39/72] scripts/qmp-shell: declare verbose in __init__ John Snow
2021-06-18 23:04 ` [PULL 40/72] scripts/qmp-shell: use triple-double-quote docstring style John Snow
2021-06-18 23:04 ` [PULL 41/72] scripts/qmp-shell: ignore visit_Name name John Snow
2021-06-18 23:04 ` [PULL 42/72] scripts/qmp-shell: make QMPCompleter returns explicit John Snow
2021-06-18 23:04 ` [PULL 43/72] scripts/qmp-shell: rename one and two-letter variables John Snow
2021-06-18 23:04 ` [PULL 44/72] scripts/qmp-shell: fix shell history exception handling John Snow
2021-06-18 23:04 ` [PULL 45/72] scripts/qmp-shell: remove if-raise-else patterns John Snow
2021-06-18 23:04 ` [PULL 46/72] scripts/qmp-shell: use isinstance() instead of type() John Snow
2021-06-18 23:04 ` [PULL 47/72] scripts/qmp-shell: use argparse John Snow
2021-06-18 23:04 ` [PULL 48/72] scripts/qmp-shell: Add pretty attribute to HMP shell John Snow
2021-06-18 23:04 ` [PULL 49/72] scripts/qmp-shell: Make verbose a public attribute John Snow
2021-06-18 23:04 ` [PULL 50/72] scripts/qmp-shell: move get_prompt() to prompt property John Snow
2021-06-18 23:04 ` [PULL 51/72] scripts/qmp-shell: remove prompt argument from read_exec_command John Snow
2021-06-18 23:04 ` [PULL 52/72] scripts/qmp-shell: move the REPL functionality into QMPShell John Snow
2021-06-18 23:04 ` [PULL 53/72] scripts/qmp-shell: Fix "FuzzyJSON" parser John Snow
2021-06-18 23:04 ` [PULL 54/72] scripts/qmp-shell: refactor QMPCompleter John Snow
2021-06-18 23:04 ` [PULL 55/72] scripts/qmp-shell: initialize completer early John Snow
2021-06-18 23:04 ` [PULL 56/72] python/qmp: add QMPObject type alias John Snow
2021-06-18 23:04 ` [PULL 57/72] scripts/qmp-shell: add mypy types John Snow
2021-06-18 23:04 ` [PULL 58/72] scripts/qmp-shell: Accept SocketAddrT instead of string John Snow
2021-06-18 23:04 ` [PULL 59/72] scripts/qmp-shell: unprivatize 'pretty' property John Snow
2021-06-18 23:04 ` [PULL 60/72] python/qmp: return generic type from context manager John Snow
2021-06-18 23:04 ` [PULL 61/72] scripts/qmp-shell: Use context manager instead of atexit John Snow
2021-06-18 23:04 ` [PULL 62/72] scripts/qmp-shell: use logging to show warnings John Snow
2021-06-18 23:04 ` [PULL 63/72] scripts/qmp-shell: remove TODO John Snow
2021-06-18 23:04 ` [PULL 64/72] scripts/qmp-shell: Fix empty-transaction invocation John Snow
2021-06-18 23:04 ` [PULL 65/72] scripts/qmp-shell: Remove too-broad-exception John Snow
2021-06-18 23:04 ` [PULL 66/72] scripts/qmp-shell: convert usage comment to docstring John Snow
2021-06-18 23:04 ` [PULL 67/72] scripts/qmp-shell: remove double-underscores John Snow
2021-06-18 23:04 ` [PULL 68/72] scripts/qmp-shell: make QMPShellError inherit QMPError John Snow
2021-06-18 23:04 ` [PULL 69/72] scripts/qmp-shell: add docstrings John Snow
2021-06-18 23:04 ` [PULL 70/72] scripts/qmp-shell: move to python/qemu/qmp/qmp_shell.py John Snow
2021-06-18 23:04 ` [PULL 71/72] python: add qmp-shell entry point John Snow
2021-06-18 23:04 ` [PULL 72/72] scripts/qmp-shell: add redirection shim John Snow
2021-06-21 19:42 ` [PULL 00/72] Python patches Peter Maydell
2021-06-22 13:34   ` John Snow

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=20210618230455.2891199-5-jsnow@redhat.com \
    --to=jsnow@redhat.com \
    --cc=armbru@redhat.com \
    --cc=crosa@redhat.com \
    --cc=ehabkost@redhat.com \
    --cc=peter.maydell@linaro.org \
    --cc=qemu-devel@nongnu.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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.