From 587adaca55e825412e54cbc9f9f20e86a6d68a72 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:03 -0400 Subject: python/qmp: add parse_address classmethod This takes the place of qmp-shell's __get_address function. It also allows other utilities to share the same parser and syntax for specifying QMP locations. Signed-off-by: John Snow Message-id: 20210603003719.1321369-4-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index b4d06096ab..d5ae8a9b21 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -89,8 +89,6 @@ class QMPCompleter(list): class QMPShellError(Exception): pass -class QMPShellBadPort(QMPShellError): - pass class FuzzyJSON(ast.NodeTransformer): '''This extension of ast.NodeTransformer filters literal "true/false/null" @@ -109,7 +107,7 @@ class FuzzyJSON(ast.NodeTransformer): # _execute_cmd()). Let's design a better one. class QMPShell(qmp.QEMUMonitorProtocol): def __init__(self, address, pretty=False): - super(QMPShell, self).__init__(self.__get_address(address)) + super(QMPShell, self).__init__(self.parse_address(address)) self._greeting = None self._completer = None self._pretty = pretty @@ -118,21 +116,6 @@ class QMPShell(qmp.QEMUMonitorProtocol): self._histfile = os.path.join(os.path.expanduser('~'), '.qmp-shell_history') - def __get_address(self, arg): - """ - Figure out if the argument is in the port:host form, if it's not it's - probably a file path. - """ - addr = arg.split(':') - if len(addr) == 2: - try: - port = int(addr[1]) - except ValueError: - raise QMPShellBadPort - return ( addr[0], port ) - # socket path - return arg - def _fill_completion(self): cmds = self.cmd('query-commands') if 'error' in cmds: @@ -437,7 +420,7 @@ def main(): if qemu is None: fail_cmdline() - except QMPShellBadPort: + except qmp.QMPBadPortError: die('bad port number in command-line') try: -- cgit v1.2.3-55-g7522 From 908ff4b29f6ff8a990cc7e895feb4ebf9cf6d2c9 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:06 -0400 Subject: scripts/qmp: redirect qom-xxx scripts to python/qemu/qmp/ Redirect to the new qom scripts. These forwarders can be deleted eventually when there has been more time for the dust on the Python packaging to settle and people understand how to find these commands. Note: You can run these by setting $PYTHONPATH in your shell and then running "python3 -m qemu.qmp.qom", or you can install the qemu namespace package and use the "qom" or "qom-set" scripts. I've written how to install the package elsewhere, but for the sake of git-blame, cd to ./python, and then do: - pip3 install [--user] [-e] . --user will install to your local user install (will not work inside of a venv), omitting this flag installs to your system-wide packages (outside of a venv) or to your current virtual environment (inside the venv). When installing to a venv or to your system-wide packages, "qom" should be in your $PATH already. If you do a user install, you may need to add ~/.local/bin to your $PATH if you haven't already. -e installs in editable mode: the installed package is effectively just a symlink to this folder; so changes to your git working tree are reflected in the installed package. Note: installing these packages to an environment outside a venv can be dangerous: Many QEMU scripts will begin to prefer the installed version instead of the version directly in the tree. Use with caution. editable mode is recommended when working outside of a venv. Signed-off-by: John Snow Reviewed-by: Philippe Mathieu-Daudé Message-id: 20210603003719.1321369-7-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-get | 66 +++------------------------------------------- scripts/qmp/qom-list | 63 +++----------------------------------------- scripts/qmp/qom-set | 63 +++----------------------------------------- scripts/qmp/qom-tree | 74 +++------------------------------------------------- 4 files changed, 16 insertions(+), 250 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qom-get b/scripts/qmp/qom-get index 666df71832..e4f3e0c013 100755 --- a/scripts/qmp/qom-get +++ b/scripts/qmp/qom-get @@ -1,69 +1,11 @@ #!/usr/bin/env python3 -## -# QEMU Object Model test tools -# -# Copyright IBM, Corp. 2011 -# -# Authors: -# Anthony Liguori -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. See -# the COPYING file in the top-level directory. -## -import sys import os +import sys sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu.qmp import QEMUMonitorProtocol - -cmd, args = sys.argv[0], sys.argv[1:] -socket_path = None -path = None -prop = None - -def usage(): - return '''environment variables: - QMP_SOCKET= -usage: - %s [-h] [-s ] . -''' % cmd - -def usage_error(error_msg = "unspecified error"): - sys.stderr.write('%s\nERROR: %s\n' % (usage(), error_msg)) - exit(1) - -if len(args) > 0: - if args[0] == "-h": - print(usage()) - exit(0); - elif args[0] == "-s": - try: - socket_path = args[1] - except: - usage_error("missing argument: QMP socket path or address"); - args = args[2:] - -if not socket_path: - if 'QMP_SOCKET' in os.environ: - socket_path = os.environ['QMP_SOCKET'] - else: - usage_error("no QMP socket path or address given"); - -if len(args) > 0: - try: - path, prop = args[0].rsplit('.', 1) - except: - usage_error("invalid format for path/property/value") -else: - usage_error("not enough arguments") +from qemu.qmp.qom import QOMGet -srv = QEMUMonitorProtocol(socket_path) -srv.connect() -rsp = srv.command('qom-get', path=path, property=prop) -if type(rsp) == dict: - for i in rsp.keys(): - print('%s: %s' % (i, rsp[i])) -else: - print(rsp) +if __name__ == '__main__': + sys.exit(QOMGet.entry_point()) diff --git a/scripts/qmp/qom-list b/scripts/qmp/qom-list index 5074fd939f..7a071a54e1 100755 --- a/scripts/qmp/qom-list +++ b/scripts/qmp/qom-list @@ -1,66 +1,11 @@ #!/usr/bin/env python3 -## -# QEMU Object Model test tools -# -# Copyright IBM, Corp. 2011 -# -# Authors: -# Anthony Liguori -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. See -# the COPYING file in the top-level directory. -## -import sys import os +import sys sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu.qmp import QEMUMonitorProtocol - -cmd, args = sys.argv[0], sys.argv[1:] -socket_path = None -path = None -prop = None - -def usage(): - return '''environment variables: - QMP_SOCKET= -usage: - %s [-h] [-s ] [] -''' % cmd - -def usage_error(error_msg = "unspecified error"): - sys.stderr.write('%s\nERROR: %s\n' % (usage(), error_msg)) - exit(1) - -if len(args) > 0: - if args[0] == "-h": - print(usage()) - exit(0); - elif args[0] == "-s": - try: - socket_path = args[1] - except: - usage_error("missing argument: QMP socket path or address"); - args = args[2:] - -if not socket_path: - if 'QMP_SOCKET' in os.environ: - socket_path = os.environ['QMP_SOCKET'] - else: - usage_error("no QMP socket path or address given"); - -srv = QEMUMonitorProtocol(socket_path) -srv.connect() +from qemu.qmp.qom import QOMList -if len(args) == 0: - print('/') - sys.exit(0) -for item in srv.command('qom-list', path=args[0]): - if item['type'].startswith('child<'): - print('%s/' % item['name']) - elif item['type'].startswith('link<'): - print('@%s/' % item['name']) - else: - print('%s' % item['name']) +if __name__ == '__main__': + sys.exit(QOMList.entry_point()) diff --git a/scripts/qmp/qom-set b/scripts/qmp/qom-set index 240a78187f..9ca9e2ba10 100755 --- a/scripts/qmp/qom-set +++ b/scripts/qmp/qom-set @@ -1,66 +1,11 @@ #!/usr/bin/env python3 -## -# QEMU Object Model test tools -# -# Copyright IBM, Corp. 2011 -# -# Authors: -# Anthony Liguori -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. See -# the COPYING file in the top-level directory. -## -import sys import os +import sys sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu.qmp import QEMUMonitorProtocol - -cmd, args = sys.argv[0], sys.argv[1:] -socket_path = None -path = None -prop = None -value = None - -def usage(): - return '''environment variables: - QMP_SOCKET= -usage: - %s [-h] [-s ] . -''' % cmd - -def usage_error(error_msg = "unspecified error"): - sys.stderr.write('%s\nERROR: %s\n' % (usage(), error_msg)) - exit(1) - -if len(args) > 0: - if args[0] == "-h": - print(usage()) - exit(0); - elif args[0] == "-s": - try: - socket_path = args[1] - except: - usage_error("missing argument: QMP socket path or address"); - args = args[2:] - -if not socket_path: - if 'QMP_SOCKET' in os.environ: - socket_path = os.environ['QMP_SOCKET'] - else: - usage_error("no QMP socket path or address given"); - -if len(args) > 1: - try: - path, prop = args[0].rsplit('.', 1) - except: - usage_error("invalid format for path/property/value") - value = args[1] -else: - usage_error("not enough arguments") +from qemu.qmp.qom import QOMSet -srv = QEMUMonitorProtocol(socket_path) -srv.connect() -print(srv.command('qom-set', path=path, property=prop, value=value)) +if __name__ == '__main__': + sys.exit(QOMSet.entry_point()) diff --git a/scripts/qmp/qom-tree b/scripts/qmp/qom-tree index 25b0781323..7d0ccca3a4 100755 --- a/scripts/qmp/qom-tree +++ b/scripts/qmp/qom-tree @@ -1,77 +1,11 @@ #!/usr/bin/env python3 -## -# QEMU Object Model test tools -# -# Copyright IBM, Corp. 2011 -# Copyright (c) 2013 SUSE LINUX Products GmbH -# -# Authors: -# Anthony Liguori -# Andreas Faerber -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. See -# the COPYING file in the top-level directory. -## -import sys import os +import sys sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu.qmp import QEMUMonitorProtocol - -cmd, args = sys.argv[0], sys.argv[1:] -socket_path = None -path = None -prop = None - -def usage(): - return '''environment variables: - QMP_SOCKET= -usage: - %s [-h] [-s ] [] -''' % cmd - -def usage_error(error_msg = "unspecified error"): - sys.stderr.write('%s\nERROR: %s\n' % (usage(), error_msg)) - exit(1) - -if len(args) > 0: - if args[0] == "-h": - print(usage()) - exit(0); - elif args[0] == "-s": - try: - socket_path = args[1] - except: - usage_error("missing argument: QMP socket path or address"); - args = args[2:] - -if not socket_path: - if 'QMP_SOCKET' in os.environ: - socket_path = os.environ['QMP_SOCKET'] - else: - usage_error("no QMP socket path or address given"); - -srv = QEMUMonitorProtocol(socket_path) -srv.connect() - -def list_node(path): - print('%s' % path) - items = srv.command('qom-list', path=path) - for item in items: - if not item['type'].startswith('child<'): - try: - print(' %s: %s (%s)' % (item['name'], srv.command('qom-get', path=path, property=item['name']), item['type'])) - except: - print(' %s: (%s)' % (item['name'], item['type'])) - print('') - for item in items: - if item['type'].startswith('child<'): - list_node((path if (path != '/') else '') + '/' + item['name']) +from qemu.qmp.qom import QOMTree -if len(args) == 0: - path = '/' -else: - path = args[0] -list_node(path) +if __name__ == '__main__': + sys.exit(QOMTree.entry_point()) -- cgit v1.2.3-55-g7522 From c6b7eae9b6743f017067ece3830dd8563b2350a9 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:07 -0400 Subject: scripts/qom-fuse: apply isort rules Hint: you can use symlinks to create qom_fuse.py in python/qemu/qmp/ and point to scripts/qom-fuse to apply the standard linting rules to this script. Signed-off-by: John Snow Reviewed-by: Philippe Mathieu-Daudé Message-id: 20210603003719.1321369-8-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-fuse | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse index 7c7cff8edf..62deb9adb1 100755 --- a/scripts/qmp/qom-fuse +++ b/scripts/qmp/qom-fuse @@ -13,14 +13,20 @@ # the COPYING file in the top-level directory. ## -import fuse, stat -from fuse import FUSE, FuseOSError, Operations -import os, posix, sys from errno import * +import os +import posix +import stat +import sys + +import fuse +from fuse import FUSE, FuseOSError, Operations + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) from qemu.qmp import QEMUMonitorProtocol + fuse.fuse_python_api = (0, 2) class QOMFS(Operations): -- cgit v1.2.3-55-g7522 From 26c1ccadc41bf32a720faf4d652eae41c7e54d00 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:08 -0400 Subject: scripts/qom-fuse: apply flake8 rules flake8 still has one warning because of the sys.path hack, but that will be going away by the end of this patch series. Signed-off-by: John Snow Reviewed-by: Philippe Mathieu-Daudé Message-id: 20210603003719.1321369-9-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-fuse | 81 ++++++++++++++++++++++++++++------------------------ 1 file changed, 43 insertions(+), 38 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse index 62deb9adb1..ca30e92867 100755 --- a/scripts/qmp/qom-fuse +++ b/scripts/qmp/qom-fuse @@ -9,13 +9,12 @@ # Anthony Liguori # Markus Armbruster # -# This work is licensed under the terms of the GNU GPL, version 2 or later. See -# the COPYING file in the top-level directory. +# This work is licensed under the terms of the GNU GPL, version 2 or later. +# See the COPYING file in the top-level directory. ## -from errno import * +from errno import ENOENT, EPERM import os -import posix import stat import sys @@ -29,6 +28,7 @@ from qemu.qmp import QEMUMonitorProtocol fuse.fuse_python_api = (0, 2) + class QOMFS(Operations): def __init__(self, qmp): self.qmp = qmp @@ -45,7 +45,7 @@ class QOMFS(Operations): def is_object(self, path): try: - items = self.qmp.command('qom-list', path=path) + self.qmp.command('qom-list', path=path) return True except: return False @@ -85,7 +85,7 @@ class QOMFS(Operations): path = '/' try: data = self.qmp.command('qom-get', path=path, property=prop) - data += '\n' # make values shell friendly + data += '\n' # make values shell friendly except: raise FuseOSError(EPERM) @@ -104,38 +104,44 @@ class QOMFS(Operations): def getattr(self, path, fh=None): if self.is_link(path): - value = { 'st_mode': 0o755 | stat.S_IFLNK, - 'st_ino': self.get_ino(path), - 'st_dev': 0, - 'st_nlink': 2, - 'st_uid': 1000, - 'st_gid': 1000, - 'st_size': 4096, - 'st_atime': 0, - 'st_mtime': 0, - 'st_ctime': 0 } + value = { + 'st_mode': 0o755 | stat.S_IFLNK, + 'st_ino': self.get_ino(path), + 'st_dev': 0, + 'st_nlink': 2, + 'st_uid': 1000, + 'st_gid': 1000, + 'st_size': 4096, + 'st_atime': 0, + 'st_mtime': 0, + 'st_ctime': 0 + } elif self.is_object(path): - value = { 'st_mode': 0o755 | stat.S_IFDIR, - 'st_ino': self.get_ino(path), - 'st_dev': 0, - 'st_nlink': 2, - 'st_uid': 1000, - 'st_gid': 1000, - 'st_size': 4096, - 'st_atime': 0, - 'st_mtime': 0, - 'st_ctime': 0 } + value = { + 'st_mode': 0o755 | stat.S_IFDIR, + 'st_ino': self.get_ino(path), + 'st_dev': 0, + 'st_nlink': 2, + 'st_uid': 1000, + 'st_gid': 1000, + 'st_size': 4096, + 'st_atime': 0, + 'st_mtime': 0, + 'st_ctime': 0 + } elif self.is_property(path): - value = { 'st_mode': 0o644 | stat.S_IFREG, - 'st_ino': self.get_ino(path), - 'st_dev': 0, - 'st_nlink': 1, - 'st_uid': 1000, - 'st_gid': 1000, - 'st_size': 4096, - 'st_atime': 0, - 'st_mtime': 0, - 'st_ctime': 0 } + value = { + 'st_mode': 0o644 | stat.S_IFREG, + 'st_ino': self.get_ino(path), + 'st_dev': 0, + 'st_nlink': 1, + 'st_uid': 1000, + 'st_gid': 1000, + 'st_size': 4096, + 'st_atime': 0, + 'st_mtime': 0, + 'st_ctime': 0 + } else: raise FuseOSError(ENOENT) return value @@ -146,8 +152,7 @@ class QOMFS(Operations): for item in self.qmp.command('qom-list', path=path): yield str(item['name']) -if __name__ == '__main__': - import os +if __name__ == '__main__': fuse = FUSE(QOMFS(QEMUMonitorProtocol(os.environ['QMP_SOCKET'])), sys.argv[1], foreground=True) -- cgit v1.2.3-55-g7522 From 7552823a36d3b99598ec53431ff43774ce11e6f2 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:10 -0400 Subject: scripts/qom-fuse: Apply pylint rules - Catch specific exceptions from QMP - Reraise errors with explicit context - method parameters should match parent's names Signed-off-by: John Snow Reviewed-by: Philippe Mathieu-Daudé Message-id: 20210603003719.1321369-11-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-fuse | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse index ca30e92867..805e99c8ec 100755 --- a/scripts/qmp/qom-fuse +++ b/scripts/qmp/qom-fuse @@ -23,7 +23,7 @@ from fuse import FUSE, FuseOSError, Operations sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu.qmp import QEMUMonitorProtocol +from qemu.qmp import QEMUMonitorProtocol, QMPResponseError fuse.fuse_python_api = (0, 2) @@ -47,7 +47,7 @@ class QOMFS(Operations): try: self.qmp.command('qom-list', path=path) return True - except: + except QMPResponseError: return False def is_property(self, path): @@ -59,7 +59,7 @@ class QOMFS(Operations): if item['name'] == prop: return True return False - except: + except QMPResponseError: return False def is_link(self, path): @@ -73,10 +73,10 @@ class QOMFS(Operations): return True return False return False - except: + except QMPResponseError: return False - def read(self, path, length, offset, fh): + def read(self, path, size, offset, fh): if not self.is_property(path): return -ENOENT @@ -86,13 +86,13 @@ class QOMFS(Operations): try: data = self.qmp.command('qom-get', path=path, property=prop) data += '\n' # make values shell friendly - except: - raise FuseOSError(EPERM) + except QMPResponseError as err: + raise FuseOSError(EPERM) from err if offset > len(data): return '' - return bytes(data[offset:][:length], encoding='utf-8') + return bytes(data[offset:][:size], encoding='utf-8') def readlink(self, path): if not self.is_link(path): -- cgit v1.2.3-55-g7522 From 187be27c7bc66afaf8c90c3b8c302a55c8f9f7d6 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:11 -0400 Subject: scripts/qom-fuse: Add docstrings The methods inherited from fuse don't need docstrings; that's up to fusepy to handle. Signed-off-by: John Snow Message-id: 20210603003719.1321369-12-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-fuse | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse index 805e99c8ec..1fb3008a16 100755 --- a/scripts/qmp/qom-fuse +++ b/scripts/qmp/qom-fuse @@ -1,7 +1,19 @@ #!/usr/bin/env python3 +""" +QEMU Object Model FUSE filesystem tool + +This script offers a simple FUSE filesystem within which the QOM tree +may be browsed, queried and edited using traditional shell tooling. + +This script requires the 'fusepy' python package. + +ENV: + QMP_SOCKET: Path to the QMP server socket + +Usage: + qom-fuse /mount/to/here +""" ## -# QEMU Object Model test tools -# # Copyright IBM, Corp. 2012 # Copyright (C) 2020 Red Hat, Inc. # @@ -30,6 +42,7 @@ fuse.fuse_python_api = (0, 2) class QOMFS(Operations): + """QOMFS implements fuse.Operations to provide a QOM filesystem.""" def __init__(self, qmp): self.qmp = qmp self.qmp.connect() @@ -37,6 +50,7 @@ class QOMFS(Operations): self.ino_count = 1 def get_ino(self, path): + """Get an inode number for a given QOM path.""" if path in self.ino_map: return self.ino_map[path] self.ino_map[path] = self.ino_count @@ -44,6 +58,7 @@ class QOMFS(Operations): return self.ino_map[path] def is_object(self, path): + """Is the given QOM path an object?""" try: self.qmp.command('qom-list', path=path) return True @@ -51,6 +66,7 @@ class QOMFS(Operations): return False def is_property(self, path): + """Is the given QOM path a property?""" path, prop = path.rsplit('/', 1) if path == '': path = '/' @@ -63,6 +79,7 @@ class QOMFS(Operations): return False def is_link(self, path): + """Is the given QOM path a link?""" path, prop = path.rsplit('/', 1) if path == '': path = '/' -- cgit v1.2.3-55-g7522 From 2aa101799acf45fd5e4207fbec95d2ee9507bc54 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:12 -0400 Subject: scripts/qom-fuse: Convert to QOMCommand Move qom-fuse onto the QOMCommand base established in python/qemu/qmp/qom_common.py. The interface doesn't change incompatibly, "qom-fuse mountpoint" still works as an invocation, and QMP_SOCKET is still used as the environment variable. Signed-off-by: John Snow Message-id: 20210603003719.1321369-13-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-fuse | 59 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 13 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse index 1fb3008a16..1676fb78d9 100755 --- a/scripts/qmp/qom-fuse +++ b/scripts/qmp/qom-fuse @@ -7,11 +7,19 @@ may be browsed, queried and edited using traditional shell tooling. This script requires the 'fusepy' python package. -ENV: - QMP_SOCKET: Path to the QMP server socket -Usage: - qom-fuse /mount/to/here +usage: qom-fuse [-h] [--socket SOCKET] + +Mount a QOM tree as a FUSE filesystem + +positional arguments: + Mount point + +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. """ ## # Copyright IBM, Corp. 2012 @@ -25,30 +33,56 @@ Usage: # See the COPYING file in the top-level directory. ## +import argparse from errno import ENOENT, EPERM import os import stat import sys +from typing import Dict import fuse from fuse import FUSE, FuseOSError, Operations sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu.qmp import QEMUMonitorProtocol, QMPResponseError +from qemu.qmp import QMPResponseError +from qemu.qmp.qom_common import QOMCommand fuse.fuse_python_api = (0, 2) -class QOMFS(Operations): - """QOMFS implements fuse.Operations to provide a QOM filesystem.""" - def __init__(self, qmp): - self.qmp = qmp - self.qmp.connect() - self.ino_map = {} +class QOMFuse(QOMCommand, Operations): + """ + QOMFuse implements both fuse.Operations and QOMCommand. + + Operations implements the FS, and QOMCommand implements the CLI command. + """ + name = 'fuse' + help = 'Mount a QOM tree as a FUSE filesystem' + fuse: FUSE + + @classmethod + def configure_parser(cls, parser: argparse.ArgumentParser) -> None: + super().configure_parser(parser) + parser.add_argument( + 'mount', + metavar='', + action='store', + help="Mount point", + ) + + def __init__(self, args: argparse.Namespace): + super().__init__(args) + self.mount = args.mount + self.ino_map: Dict[str, int] = {} self.ino_count = 1 + def run(self) -> int: + print(f"Mounting QOMFS to '{self.mount}'", file=sys.stderr) + self.fuse = FUSE(self, self.mount, foreground=True) + return 0 + def get_ino(self, path): """Get an inode number for a given QOM path.""" if path in self.ino_map: @@ -171,5 +205,4 @@ class QOMFS(Operations): if __name__ == '__main__': - fuse = FUSE(QOMFS(QEMUMonitorProtocol(os.environ['QMP_SOCKET'])), - sys.argv[1], foreground=True) + sys.exit(QOMFuse.entry_point()) -- cgit v1.2.3-55-g7522 From 9ec8a3869480a9826ba77b480bb7ca567aa3d26b Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:13 -0400 Subject: scripts/qom-fuse: use QOMCommand.qom_list() the qom_list method provides a type-safe object that's easier to type check, so switch to using it. Signed-off-by: John Snow Reviewed-by: Philippe Mathieu-Daudé Message-id: 20210603003719.1321369-14-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-fuse | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse index 1676fb78d9..703a97e75f 100755 --- a/scripts/qmp/qom-fuse +++ b/scripts/qmp/qom-fuse @@ -94,7 +94,7 @@ class QOMFuse(QOMCommand, Operations): def is_object(self, path): """Is the given QOM path an object?""" try: - self.qmp.command('qom-list', path=path) + self.qom_list(path) return True except QMPResponseError: return False @@ -105,8 +105,8 @@ class QOMFuse(QOMCommand, Operations): if path == '': path = '/' try: - for item in self.qmp.command('qom-list', path=path): - if item['name'] == prop: + for item in self.qom_list(path): + if item.name == prop: return True return False except QMPResponseError: @@ -118,11 +118,9 @@ class QOMFuse(QOMCommand, Operations): if path == '': path = '/' try: - for item in self.qmp.command('qom-list', path=path): - if item['name'] == prop: - if item['type'].startswith('link<'): - return True - return False + for item in self.qom_list(path): + if item.name == prop and item.link: + return True return False except QMPResponseError: return False @@ -200,8 +198,8 @@ class QOMFuse(QOMCommand, Operations): def readdir(self, path, fh): yield '.' yield '..' - for item in self.qmp.command('qom-list', path=path): - yield str(item['name']) + for item in self.qom_list(path): + yield item.name if __name__ == '__main__': -- cgit v1.2.3-55-g7522 From 2cea7134620749b106af167322d921716ef61144 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:14 -0400 Subject: scripts/qom-fuse: ensure QOMFuse.read always returns bytes - Use FuseOSError to signal ENOENT instead of returning it - Wrap qom-get in str(), as we don't always know its type - The empty return should be b'', not ''. Signed-off-by: John Snow Reviewed-by: Philippe Mathieu-Daudé Message-id: 20210603003719.1321369-15-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-fuse | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse index 703a97e75f..0d11f73152 100755 --- a/scripts/qmp/qom-fuse +++ b/scripts/qmp/qom-fuse @@ -127,19 +127,19 @@ class QOMFuse(QOMCommand, Operations): def read(self, path, size, offset, fh): if not self.is_property(path): - return -ENOENT + raise FuseOSError(ENOENT) path, prop = path.rsplit('/', 1) if path == '': path = '/' try: - data = self.qmp.command('qom-get', path=path, property=prop) + data = str(self.qmp.command('qom-get', path=path, property=prop)) data += '\n' # make values shell friendly except QMPResponseError as err: raise FuseOSError(EPERM) from err if offset > len(data): - return '' + return b'' return bytes(data[offset:][:size], encoding='utf-8') -- cgit v1.2.3-55-g7522 From 30ec845c599acc0616a57811316bc506a08344f2 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:15 -0400 Subject: scripts/qom-fuse: add static type hints Because fusepy does not have type hints, add some targeted warning suppressions. Namely, we need to allow subclassing something of an unknown type (in qom_fuse.py), and we need to allow missing imports (recorded against fuse itself) because mypy will be unable to import fusepy (even when installed) as it has no types nor type stubs available. Note: Until now, it was possible to run invocations like 'mypy qemu/' from ./python and have that work. However, these targeted suppressions require that you run 'mypy -p qemu/' instead. The correct, canonical invocation is recorded in ./python/tests/mypy.sh and all of the various CI invocations always use this correct form. Signed-off-by: John Snow Message-id: 20210603003719.1321369-16-jsnow@redhat.com Signed-off-by: John Snow --- python/setup.cfg | 8 ++++++++ scripts/qmp/qom-fuse | 26 +++++++++++++++++--------- 2 files changed, 25 insertions(+), 9 deletions(-) (limited to 'scripts') diff --git a/python/setup.cfg b/python/setup.cfg index c9b9445af9..ba8d29fd62 100644 --- a/python/setup.cfg +++ b/python/setup.cfg @@ -57,6 +57,14 @@ python_version = 3.6 warn_unused_configs = True namespace_packages = True +[mypy-qemu.qmp.qom_fuse] +# fusepy has no type stubs: +allow_subclassing_any = True + +[mypy-fuse] +# fusepy has no type stubs: +ignore_missing_imports = True + [pylint.messages control] # Disable the message, report, category or checker with the given id(s). You # can either give multiple identifiers separated by comma (,) or put this diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse index 0d11f73152..a5a7a304a3 100755 --- a/scripts/qmp/qom-fuse +++ b/scripts/qmp/qom-fuse @@ -38,7 +38,14 @@ from errno import ENOENT, EPERM import os import stat import sys -from typing import Dict +from typing import ( + IO, + Dict, + Iterator, + Mapping, + Optional, + Union, +) import fuse from fuse import FUSE, FuseOSError, Operations @@ -83,7 +90,7 @@ class QOMFuse(QOMCommand, Operations): self.fuse = FUSE(self, self.mount, foreground=True) return 0 - def get_ino(self, path): + def get_ino(self, path: str) -> int: """Get an inode number for a given QOM path.""" if path in self.ino_map: return self.ino_map[path] @@ -91,7 +98,7 @@ class QOMFuse(QOMCommand, Operations): self.ino_count += 1 return self.ino_map[path] - def is_object(self, path): + def is_object(self, path: str) -> bool: """Is the given QOM path an object?""" try: self.qom_list(path) @@ -99,7 +106,7 @@ class QOMFuse(QOMCommand, Operations): except QMPResponseError: return False - def is_property(self, path): + def is_property(self, path: str) -> bool: """Is the given QOM path a property?""" path, prop = path.rsplit('/', 1) if path == '': @@ -112,7 +119,7 @@ class QOMFuse(QOMCommand, Operations): except QMPResponseError: return False - def is_link(self, path): + def is_link(self, path: str) -> bool: """Is the given QOM path a link?""" path, prop = path.rsplit('/', 1) if path == '': @@ -125,7 +132,7 @@ class QOMFuse(QOMCommand, Operations): except QMPResponseError: return False - def read(self, path, size, offset, fh): + def read(self, path: str, size: int, offset: int, fh: IO[bytes]) -> bytes: if not self.is_property(path): raise FuseOSError(ENOENT) @@ -143,7 +150,7 @@ class QOMFuse(QOMCommand, Operations): return bytes(data[offset:][:size], encoding='utf-8') - def readlink(self, path): + def readlink(self, path: str) -> Union[bool, str]: if not self.is_link(path): return False path, prop = path.rsplit('/', 1) @@ -151,7 +158,8 @@ class QOMFuse(QOMCommand, Operations): return prefix + str(self.qmp.command('qom-get', path=path, property=prop)) - def getattr(self, path, fh=None): + def getattr(self, path: str, + fh: Optional[IO[bytes]] = None) -> Mapping[str, object]: if self.is_link(path): value = { 'st_mode': 0o755 | stat.S_IFLNK, @@ -195,7 +203,7 @@ class QOMFuse(QOMCommand, Operations): raise FuseOSError(ENOENT) return value - def readdir(self, path, fh): + def readdir(self, path: str, fh: IO[bytes]) -> Iterator[str]: yield '.' yield '..' for item in self.qom_list(path): -- cgit v1.2.3-55-g7522 From 173d185de98c4ee358e5615cedcd8773719586c0 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:17 -0400 Subject: scripts/qom-fuse: move to python/qemu/qmp/qom_fuse.py Move qom-fuse over to the python package now that it passes the linter. Update the import paradigms so that it continues to pass in the context of the Python package. Signed-off-by: John Snow Message-id: 20210603003719.1321369-18-jsnow@redhat.com Signed-off-by: John Snow --- python/qemu/qmp/qom_fuse.py | 206 ++++++++++++++++++++++++++++++++++++++++++ scripts/qmp/qom-fuse | 214 -------------------------------------------- 2 files changed, 206 insertions(+), 214 deletions(-) create mode 100644 python/qemu/qmp/qom_fuse.py delete mode 100755 scripts/qmp/qom-fuse (limited to 'scripts') diff --git a/python/qemu/qmp/qom_fuse.py b/python/qemu/qmp/qom_fuse.py new file mode 100644 index 0000000000..43f4671fdb --- /dev/null +++ b/python/qemu/qmp/qom_fuse.py @@ -0,0 +1,206 @@ +""" +QEMU Object Model FUSE filesystem tool + +This script offers a simple FUSE filesystem within which the QOM tree +may be browsed, queried and edited using traditional shell tooling. + +This script requires the 'fusepy' python package. + + +usage: qom-fuse [-h] [--socket SOCKET] + +Mount a QOM tree as a FUSE filesystem + +positional arguments: + Mount point + +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. +""" +## +# Copyright IBM, Corp. 2012 +# Copyright (C) 2020 Red Hat, Inc. +# +# Authors: +# Anthony Liguori +# Markus Armbruster +# +# This work is licensed under the terms of the GNU GPL, version 2 or later. +# See the COPYING file in the top-level directory. +## + +import argparse +from errno import ENOENT, EPERM +import stat +import sys +from typing import ( + IO, + Dict, + Iterator, + Mapping, + Optional, + Union, +) + +import fuse +from fuse import FUSE, FuseOSError, Operations + +from . import QMPResponseError +from .qom_common import QOMCommand + + +fuse.fuse_python_api = (0, 2) + + +class QOMFuse(QOMCommand, Operations): + """ + QOMFuse implements both fuse.Operations and QOMCommand. + + Operations implements the FS, and QOMCommand implements the CLI command. + """ + name = 'fuse' + help = 'Mount a QOM tree as a FUSE filesystem' + fuse: FUSE + + @classmethod + def configure_parser(cls, parser: argparse.ArgumentParser) -> None: + super().configure_parser(parser) + parser.add_argument( + 'mount', + metavar='', + action='store', + help="Mount point", + ) + + def __init__(self, args: argparse.Namespace): + super().__init__(args) + self.mount = args.mount + self.ino_map: Dict[str, int] = {} + self.ino_count = 1 + + def run(self) -> int: + print(f"Mounting QOMFS to '{self.mount}'", file=sys.stderr) + self.fuse = FUSE(self, self.mount, foreground=True) + return 0 + + def get_ino(self, path: str) -> int: + """Get an inode number for a given QOM path.""" + if path in self.ino_map: + return self.ino_map[path] + self.ino_map[path] = self.ino_count + self.ino_count += 1 + return self.ino_map[path] + + def is_object(self, path: str) -> bool: + """Is the given QOM path an object?""" + try: + self.qom_list(path) + return True + except QMPResponseError: + return False + + def is_property(self, path: str) -> bool: + """Is the given QOM path a property?""" + path, prop = path.rsplit('/', 1) + if path == '': + path = '/' + try: + for item in self.qom_list(path): + if item.name == prop: + return True + return False + except QMPResponseError: + return False + + def is_link(self, path: str) -> bool: + """Is the given QOM path a link?""" + path, prop = path.rsplit('/', 1) + if path == '': + path = '/' + try: + for item in self.qom_list(path): + if item.name == prop and item.link: + return True + return False + except QMPResponseError: + return False + + def read(self, path: str, size: int, offset: int, fh: IO[bytes]) -> bytes: + if not self.is_property(path): + raise FuseOSError(ENOENT) + + path, prop = path.rsplit('/', 1) + if path == '': + path = '/' + try: + data = str(self.qmp.command('qom-get', path=path, property=prop)) + data += '\n' # make values shell friendly + except QMPResponseError as err: + raise FuseOSError(EPERM) from err + + if offset > len(data): + return b'' + + return bytes(data[offset:][:size], encoding='utf-8') + + def readlink(self, path: str) -> Union[bool, str]: + if not self.is_link(path): + return False + path, prop = path.rsplit('/', 1) + prefix = '/'.join(['..'] * (len(path.split('/')) - 1)) + return prefix + str(self.qmp.command('qom-get', path=path, + property=prop)) + + def getattr(self, path: str, + fh: Optional[IO[bytes]] = None) -> Mapping[str, object]: + if self.is_link(path): + value = { + 'st_mode': 0o755 | stat.S_IFLNK, + 'st_ino': self.get_ino(path), + 'st_dev': 0, + 'st_nlink': 2, + 'st_uid': 1000, + 'st_gid': 1000, + 'st_size': 4096, + 'st_atime': 0, + 'st_mtime': 0, + 'st_ctime': 0 + } + elif self.is_object(path): + value = { + 'st_mode': 0o755 | stat.S_IFDIR, + 'st_ino': self.get_ino(path), + 'st_dev': 0, + 'st_nlink': 2, + 'st_uid': 1000, + 'st_gid': 1000, + 'st_size': 4096, + 'st_atime': 0, + 'st_mtime': 0, + 'st_ctime': 0 + } + elif self.is_property(path): + value = { + 'st_mode': 0o644 | stat.S_IFREG, + 'st_ino': self.get_ino(path), + 'st_dev': 0, + 'st_nlink': 1, + 'st_uid': 1000, + 'st_gid': 1000, + 'st_size': 4096, + 'st_atime': 0, + 'st_mtime': 0, + 'st_ctime': 0 + } + else: + raise FuseOSError(ENOENT) + return value + + def readdir(self, path: str, fh: IO[bytes]) -> Iterator[str]: + yield '.' + yield '..' + for item in self.qom_list(path): + yield item.name diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse deleted file mode 100755 index a5a7a304a3..0000000000 --- a/scripts/qmp/qom-fuse +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 -""" -QEMU Object Model FUSE filesystem tool - -This script offers a simple FUSE filesystem within which the QOM tree -may be browsed, queried and edited using traditional shell tooling. - -This script requires the 'fusepy' python package. - - -usage: qom-fuse [-h] [--socket SOCKET] - -Mount a QOM tree as a FUSE filesystem - -positional arguments: - Mount point - -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. -""" -## -# Copyright IBM, Corp. 2012 -# Copyright (C) 2020 Red Hat, Inc. -# -# Authors: -# Anthony Liguori -# Markus Armbruster -# -# This work is licensed under the terms of the GNU GPL, version 2 or later. -# See the COPYING file in the top-level directory. -## - -import argparse -from errno import ENOENT, EPERM -import os -import stat -import sys -from typing import ( - IO, - Dict, - Iterator, - Mapping, - Optional, - Union, -) - -import fuse -from fuse import FUSE, FuseOSError, Operations - - -sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu.qmp import QMPResponseError -from qemu.qmp.qom_common import QOMCommand - - -fuse.fuse_python_api = (0, 2) - - -class QOMFuse(QOMCommand, Operations): - """ - QOMFuse implements both fuse.Operations and QOMCommand. - - Operations implements the FS, and QOMCommand implements the CLI command. - """ - name = 'fuse' - help = 'Mount a QOM tree as a FUSE filesystem' - fuse: FUSE - - @classmethod - def configure_parser(cls, parser: argparse.ArgumentParser) -> None: - super().configure_parser(parser) - parser.add_argument( - 'mount', - metavar='', - action='store', - help="Mount point", - ) - - def __init__(self, args: argparse.Namespace): - super().__init__(args) - self.mount = args.mount - self.ino_map: Dict[str, int] = {} - self.ino_count = 1 - - def run(self) -> int: - print(f"Mounting QOMFS to '{self.mount}'", file=sys.stderr) - self.fuse = FUSE(self, self.mount, foreground=True) - return 0 - - def get_ino(self, path: str) -> int: - """Get an inode number for a given QOM path.""" - if path in self.ino_map: - return self.ino_map[path] - self.ino_map[path] = self.ino_count - self.ino_count += 1 - return self.ino_map[path] - - def is_object(self, path: str) -> bool: - """Is the given QOM path an object?""" - try: - self.qom_list(path) - return True - except QMPResponseError: - return False - - def is_property(self, path: str) -> bool: - """Is the given QOM path a property?""" - path, prop = path.rsplit('/', 1) - if path == '': - path = '/' - try: - for item in self.qom_list(path): - if item.name == prop: - return True - return False - except QMPResponseError: - return False - - def is_link(self, path: str) -> bool: - """Is the given QOM path a link?""" - path, prop = path.rsplit('/', 1) - if path == '': - path = '/' - try: - for item in self.qom_list(path): - if item.name == prop and item.link: - return True - return False - except QMPResponseError: - return False - - def read(self, path: str, size: int, offset: int, fh: IO[bytes]) -> bytes: - if not self.is_property(path): - raise FuseOSError(ENOENT) - - path, prop = path.rsplit('/', 1) - if path == '': - path = '/' - try: - data = str(self.qmp.command('qom-get', path=path, property=prop)) - data += '\n' # make values shell friendly - except QMPResponseError as err: - raise FuseOSError(EPERM) from err - - if offset > len(data): - return b'' - - return bytes(data[offset:][:size], encoding='utf-8') - - def readlink(self, path: str) -> Union[bool, str]: - if not self.is_link(path): - return False - path, prop = path.rsplit('/', 1) - prefix = '/'.join(['..'] * (len(path.split('/')) - 1)) - return prefix + str(self.qmp.command('qom-get', path=path, - property=prop)) - - def getattr(self, path: str, - fh: Optional[IO[bytes]] = None) -> Mapping[str, object]: - if self.is_link(path): - value = { - 'st_mode': 0o755 | stat.S_IFLNK, - 'st_ino': self.get_ino(path), - 'st_dev': 0, - 'st_nlink': 2, - 'st_uid': 1000, - 'st_gid': 1000, - 'st_size': 4096, - 'st_atime': 0, - 'st_mtime': 0, - 'st_ctime': 0 - } - elif self.is_object(path): - value = { - 'st_mode': 0o755 | stat.S_IFDIR, - 'st_ino': self.get_ino(path), - 'st_dev': 0, - 'st_nlink': 2, - 'st_uid': 1000, - 'st_gid': 1000, - 'st_size': 4096, - 'st_atime': 0, - 'st_mtime': 0, - 'st_ctime': 0 - } - elif self.is_property(path): - value = { - 'st_mode': 0o644 | stat.S_IFREG, - 'st_ino': self.get_ino(path), - 'st_dev': 0, - 'st_nlink': 1, - 'st_uid': 1000, - 'st_gid': 1000, - 'st_size': 4096, - 'st_atime': 0, - 'st_mtime': 0, - 'st_ctime': 0 - } - else: - raise FuseOSError(ENOENT) - return value - - def readdir(self, path: str, fh: IO[bytes]) -> Iterator[str]: - yield '.' - yield '..' - for item in self.qom_list(path): - yield item.name - - -if __name__ == '__main__': - sys.exit(QOMFuse.entry_point()) -- cgit v1.2.3-55-g7522 From c89b38cd0a9ec852ed5504b3083ca7592d76b730 Mon Sep 17 00:00:00 2001 From: John Snow Date: Wed, 2 Jun 2021 20:37:18 -0400 Subject: scripts/qom-fuse: add redirection shim to python/qemu/qmp/qom-fuse.py By leaving the script absent for a commit, git-blame travels to the new file instead of staying on the shim. Signed-off-by: John Snow Reviewed-by: Philippe Mathieu-Daudé Message-id: 20210603003719.1321369-19-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qom-fuse | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 scripts/qmp/qom-fuse (limited to 'scripts') diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse new file mode 100755 index 0000000000..a58c8ef979 --- /dev/null +++ b/scripts/qmp/qom-fuse @@ -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.qmp.qom_fuse import QOMFuse + + +if __name__ == '__main__': + sys.exit(QOMFuse.entry_point()) -- cgit v1.2.3-55-g7522 From 9510e4fb6967c39871b149676e09bb7ee875bc18 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:22 -0400 Subject: scripts/qemu-ga-client: apply isort rules Hint: > ln -s scripts/qmp/qemu-ga-client python/qemu/qmp/qemu_ga_client.py > cd python > isort qemu Signed-off-by: John Snow Message-id: 20210604155532.1499282-2-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qemu-ga-client | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client index 348d85864c..97f4047a62 100755 --- a/scripts/qmp/qemu-ga-client +++ b/scripts/qmp/qemu-ga-client @@ -36,10 +36,11 @@ # See also: https://wiki.qemu.org/Features/QAPI/GuestAgent # -import os -import sys import base64 +import os import random +import sys + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) from qemu import qmp @@ -279,9 +280,9 @@ def main(address, cmd, args): if __name__ == '__main__': - import sys - import os import optparse + import os + import sys address = os.environ['QGA_CLIENT_ADDRESS'] if 'QGA_CLIENT_ADDRESS' in os.environ else None -- cgit v1.2.3-55-g7522 From e75f516ac131dbc3c82ac52ef527680c4745add3 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:23 -0400 Subject: scripts/qemu-ga-client: apply (most) flake8 rules - Line length should be < 80 - You shouldn't perform unscoped imports except at the top of the module Notably, the sys.path hack creates problems with the import rule. This will be fixed later. Signed-off-by: John Snow Message-id: 20210604155532.1499282-3-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qemu-ga-client | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client index 97f4047a62..566bddc89d 100755 --- a/scripts/qmp/qemu-ga-client +++ b/scripts/qmp/qemu-ga-client @@ -12,7 +12,8 @@ # Start QEMU with: # # # qemu [...] -chardev socket,path=/tmp/qga.sock,server=on,wait=off,id=qga0 \ -# -device virtio-serial -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 +# -device virtio-serial \ +# -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 # # Run the script: # @@ -37,6 +38,7 @@ # import base64 +import optparse import os import random import sys @@ -94,9 +96,11 @@ class QemuGuestAgentClient: msgs = [] msgs.append('version: ' + info['version']) msgs.append('supported_commands:') - enabled = [c['name'] for c in info['supported_commands'] if c['enabled']] + enabled = [c['name'] for c in info['supported_commands'] + if c['enabled']] msgs.append('\tenabled: ' + ', '.join(enabled)) - disabled = [c['name'] for c in info['supported_commands'] if not c['enabled']] + disabled = [c['name'] for c in info['supported_commands'] + if not c['enabled']] msgs.append('\tdisabled: ' + ', '.join(disabled)) return '\n'.join(msgs) @@ -119,11 +123,11 @@ class QemuGuestAgentClient: if ipaddr['ip-address-type'] == 'ipv4': addr = ipaddr['ip-address'] mask = self.__gen_ipv4_netmask(int(ipaddr['prefix'])) - msgs.append("\tinet %s netmask %s" % (addr, mask)) + msgs.append(f"\tinet {addr} netmask {mask}") elif ipaddr['ip-address-type'] == 'ipv6': addr = ipaddr['ip-address'] prefix = ipaddr['prefix'] - msgs.append("\tinet6 %s prefixlen %s" % (addr, prefix)) + msgs.append(f"\tinet6 {addr} prefixlen {prefix}") if nif['hardware-address'] != '00:00:00:00:00:00': msgs.append("\tether " + nif['hardware-address']) @@ -237,6 +241,8 @@ def _cmd_suspend(client, args): def _cmd_shutdown(client, args): client.shutdown() + + _cmd_powerdown = _cmd_shutdown @@ -280,17 +286,15 @@ def main(address, cmd, args): if __name__ == '__main__': - import optparse - import os - import sys - - address = os.environ['QGA_CLIENT_ADDRESS'] if 'QGA_CLIENT_ADDRESS' in os.environ else None + address = os.environ.get('QGA_CLIENT_ADDRESS') - usage = "%prog [--address=|] [args...]\n" + usage = ("%prog [--address=|]" + " [args...]\n") usage += ': ' + ', '.join(commands) parser = optparse.OptionParser(usage=usage) parser.add_option('--address', action='store', type='string', - default=address, help='Specify a ip:port pair or a unix socket path') + default=address, + help='Specify a ip:port pair or a unix socket path') options, args = parser.parse_args() address = options.address -- cgit v1.2.3-55-g7522 From e6de9ce90a67960a6477da5a5c69c19ce390ed1d Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:24 -0400 Subject: scripts/qemu-ga-client: Fix exception handling Fixes: 50d189c These error classes aren't available anymore. Fix the bitrot. Signed-off-by: John Snow Message-id: 20210604155532.1499282-4-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qemu-ga-client | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client index 566bddc89d..7aba09f0fe 100755 --- a/scripts/qmp/qemu-ga-client +++ b/scripts/qmp/qemu-ga-client @@ -56,8 +56,6 @@ class QemuGuestAgent(qmp.QEMUMonitorProtocol): class QemuGuestAgentClient: - error = QemuGuestAgent.error - def __init__(self, address): self.qga = QemuGuestAgent(address) self.qga.connect(negotiate=False) @@ -137,7 +135,7 @@ class QemuGuestAgentClient: self.qga.settimeout(timeout) try: self.qga.ping() - except self.qga.timeout: + except TimeoutError: return False return True @@ -269,11 +267,11 @@ def main(address, cmd, args): try: client = QemuGuestAgentClient(address) - except QemuGuestAgent.error as e: + except OSError as err: import errno - print(e) - if e.errno == errno.ECONNREFUSED: + print(err) + if err.errno == errno.ECONNREFUSED: print('Hint: qemu is not running?') sys.exit(1) -- cgit v1.2.3-55-g7522 From 0cf1a52d289d69461df0d984aa50ec61f8a21c30 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:25 -0400 Subject: scripts/qemu-ga-client: replace deprecated optparse with argparse optparse isn't supported anymore, it's from the python2 days. Replace it with the mostly similar argparse. Signed-off-by: John Snow Message-id: 20210604155532.1499282-5-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qemu-ga-client | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client index 7aba09f0fe..8eb4015e61 100755 --- a/scripts/qmp/qemu-ga-client +++ b/scripts/qmp/qemu-ga-client @@ -37,8 +37,8 @@ # See also: https://wiki.qemu.org/Features/QAPI/GuestAgent # +import argparse import base64 -import optparse import os import random import sys @@ -255,7 +255,7 @@ def _cmd_reboot(client, args): commands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m] -def main(address, cmd, args): +def send_command(address, cmd, args): if not os.path.exists(address): print('%s not found' % address) sys.exit(1) @@ -283,25 +283,23 @@ def main(address, cmd, args): globals()['_cmd_' + cmd](client, args) -if __name__ == '__main__': +def main(): address = os.environ.get('QGA_CLIENT_ADDRESS') - usage = ("%prog [--address=|]" - " [args...]\n") - usage += ': ' + ', '.join(commands) - parser = optparse.OptionParser(usage=usage) - parser.add_option('--address', action='store', type='string', - default=address, - help='Specify a ip:port pair or a unix socket path') - options, args = parser.parse_args() - - address = options.address - if address is None: + parser = argparse.ArgumentParser() + parser.add_argument('--address', action='store', + default=address, + help='Specify a ip:port pair or a unix socket path') + parser.add_argument('command', choices=commands) + parser.add_argument('args', nargs='*') + + args = parser.parse_args() + if args.address is None: parser.error('address is not specified') sys.exit(1) - if len(args) == 0: - parser.error('Less argument') - sys.exit(1) + send_command(args.address, args.command, args.args) + - main(address, args[0], args[1:]) +if __name__ == '__main__': + main() -- cgit v1.2.3-55-g7522 From aff103b531dc6e44650e5d3d983f313700cb7534 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:26 -0400 Subject: scripts/qemu-ga-client: add module docstring Turn that nice usage comment into a docstring. Signed-off-by: John Snow Message-id: 20210604155532.1499282-6-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qemu-ga-client | 65 +++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 32 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client index 8eb4015e61..e81937e0ea 100755 --- a/scripts/qmp/qemu-ga-client +++ b/scripts/qmp/qemu-ga-client @@ -1,41 +1,42 @@ #!/usr/bin/env python3 -# QEMU Guest Agent Client -# +""" +QEMU Guest Agent Client + +Usage: + +Start QEMU with: + +# qemu [...] -chardev socket,path=/tmp/qga.sock,server,wait=off,id=qga0 \ + -device virtio-serial \ + -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 + +Run the script: + +$ qemu-ga-client --address=/tmp/qga.sock [args...] + +or + +$ export QGA_CLIENT_ADDRESS=/tmp/qga.sock +$ qemu-ga-client [args...] + +For example: + +$ qemu-ga-client cat /etc/resolv.conf +# Generated by NetworkManager +nameserver 10.0.2.3 +$ qemu-ga-client fsfreeze status +thawed +$ qemu-ga-client fsfreeze freeze +2 filesystems frozen + +See also: https://wiki.qemu.org/Features/QAPI/GuestAgent +""" + # Copyright (C) 2012 Ryota Ozaki # # This work is licensed under the terms of the GNU GPL, version 2. See # the COPYING file in the top-level directory. -# -# Usage: -# -# Start QEMU with: -# -# # qemu [...] -chardev socket,path=/tmp/qga.sock,server=on,wait=off,id=qga0 \ -# -device virtio-serial \ -# -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 -# -# Run the script: -# -# $ qemu-ga-client --address=/tmp/qga.sock [args...] -# -# or -# -# $ export QGA_CLIENT_ADDRESS=/tmp/qga.sock -# $ qemu-ga-client [args...] -# -# For example: -# -# $ qemu-ga-client cat /etc/resolv.conf -# # Generated by NetworkManager -# nameserver 10.0.2.3 -# $ qemu-ga-client fsfreeze status -# thawed -# $ qemu-ga-client fsfreeze freeze -# 2 filesystems frozen -# -# See also: https://wiki.qemu.org/Features/QAPI/GuestAgent -# import argparse import base64 -- cgit v1.2.3-55-g7522 From f85d3252ef889b102eb42756450f45c973d3cb43 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:27 -0400 Subject: scripts/qemu-ga-client: apply (most) pylint rules I'm only doing a very quick best-effort to preserve this script, to help keep it from breaking further. I think there are pending ideas swirling on the right way to implement better SDKs and better clients, and this script might be a handy reference for those discussions. It presents some interesting design problems, like static type safety when using a dynamic RPC mechanism. I believe it's worth preserving the effort and care that went into making this script by updating it to work with our current infrastructure. However, I am disabling the requirement for docstrings in this file. If you would like to help improve this script, please add docstrings alongside any refactors or rejuvenations you might apply at that time. Signed-off-by: John Snow Message-id: 20210604155532.1499282-7-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qemu-ga-client | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client index e81937e0ea..ece9f74fa8 100755 --- a/scripts/qmp/qemu-ga-client +++ b/scripts/qmp/qemu-ga-client @@ -40,6 +40,7 @@ See also: https://wiki.qemu.org/Features/QAPI/GuestAgent import argparse import base64 +import errno import os import random import sys @@ -49,6 +50,13 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) from qemu import qmp +# This script has not seen many patches or careful attention in quite +# some time. If you would like to improve it, please review the design +# carefully and add docstrings at that point in time. Until then: + +# pylint: disable=missing-docstring + + class QemuGuestAgent(qmp.QEMUMonitorProtocol): def __getattr__(self, name): def wrapper(**kwds): @@ -104,7 +112,8 @@ class QemuGuestAgentClient: return '\n'.join(msgs) - def __gen_ipv4_netmask(self, prefixlen): + @classmethod + def __gen_ipv4_netmask(cls, prefixlen): mask = int('1' * prefixlen + '0' * (32 - prefixlen), 2) return '.'.join([str(mask >> 24), str((mask >> 16) & 0xff), @@ -207,10 +216,12 @@ def _cmd_fstrim(client, args): def _cmd_ifconfig(client, args): + assert not args print(client.ifconfig()) def _cmd_info(client, args): + assert not args print(client.info()) @@ -239,6 +250,7 @@ def _cmd_suspend(client, args): def _cmd_shutdown(client, args): + assert not args client.shutdown() @@ -246,10 +258,12 @@ _cmd_powerdown = _cmd_shutdown def _cmd_halt(client, args): + assert not args client.shutdown('halt') def _cmd_reboot(client, args): + assert not args client.shutdown('reboot') @@ -269,8 +283,6 @@ def send_command(address, cmd, args): try: client = QemuGuestAgentClient(address) except OSError as err: - import errno - print(err) if err.errno == errno.ECONNREFUSED: print('Hint: qemu is not running?') -- cgit v1.2.3-55-g7522 From ca683d4a2fece0e6bf58f065baa1e23226c1ac06 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:29 -0400 Subject: scripts/qemu-ga-client: add mypy type hints This script is in slightly rough shape, but it still works. A lot of care went into its initial development. In good faith, I'm updating it to the latest Python coding standards. If there is in interest in this script, though, I'll be asking for a contributor to take care of it further. Signed-off-by: John Snow Message-id: 20210604155532.1499282-9-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qemu-ga-client | 89 +++++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 40 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client index ece9f74fa8..a7d0ef8347 100755 --- a/scripts/qmp/qemu-ga-client +++ b/scripts/qmp/qemu-ga-client @@ -44,10 +44,18 @@ import errno import os import random import sys +from typing import ( + Any, + Callable, + Dict, + Optional, + Sequence, +) sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) from qemu import qmp +from qemu.qmp import SocketAddrT # This script has not seen many patches or careful attention in quite @@ -58,18 +66,18 @@ from qemu import qmp class QemuGuestAgent(qmp.QEMUMonitorProtocol): - def __getattr__(self, name): - def wrapper(**kwds): + def __getattr__(self, name: str) -> Callable[..., Any]: + def wrapper(**kwds: object) -> object: return self.command('guest-' + name.replace('_', '-'), **kwds) return wrapper class QemuGuestAgentClient: - def __init__(self, address): + def __init__(self, address: SocketAddrT): self.qga = QemuGuestAgent(address) self.qga.connect(negotiate=False) - def sync(self, timeout=3): + def sync(self, timeout: Optional[float] = 3) -> None: # Avoid being blocked forever if not self.ping(timeout): raise EnvironmentError('Agent seems not alive') @@ -79,9 +87,9 @@ class QemuGuestAgentClient: if isinstance(ret, int) and int(ret) == uid: break - def __file_read_all(self, handle): + def __file_read_all(self, handle: int) -> bytes: eof = False - data = '' + data = b'' while not eof: ret = self.qga.file_read(handle=handle, count=1024) _data = base64.b64decode(ret['buf-b64']) @@ -89,7 +97,7 @@ class QemuGuestAgentClient: eof = ret['eof'] return data - def read(self, path): + def read(self, path: str) -> bytes: handle = self.qga.file_open(path=path) try: data = self.__file_read_all(handle) @@ -97,7 +105,7 @@ class QemuGuestAgentClient: self.qga.file_close(handle=handle) return data - def info(self): + def info(self) -> str: info = self.qga.info() msgs = [] @@ -113,14 +121,14 @@ class QemuGuestAgentClient: return '\n'.join(msgs) @classmethod - def __gen_ipv4_netmask(cls, prefixlen): + def __gen_ipv4_netmask(cls, prefixlen: int) -> str: mask = int('1' * prefixlen + '0' * (32 - prefixlen), 2) return '.'.join([str(mask >> 24), str((mask >> 16) & 0xff), str((mask >> 8) & 0xff), str(mask & 0xff)]) - def ifconfig(self): + def ifconfig(self) -> str: nifs = self.qga.network_get_interfaces() msgs = [] @@ -141,7 +149,7 @@ class QemuGuestAgentClient: return '\n'.join(msgs) - def ping(self, timeout): + def ping(self, timeout: Optional[float]) -> bool: self.qga.settimeout(timeout) try: self.qga.ping() @@ -149,37 +157,40 @@ class QemuGuestAgentClient: return False return True - def fsfreeze(self, cmd): + def fsfreeze(self, cmd: str) -> object: if cmd not in ['status', 'freeze', 'thaw']: raise Exception('Invalid command: ' + cmd) - + # Can be int (freeze, thaw) or GuestFsfreezeStatus (status) return getattr(self.qga, 'fsfreeze' + '_' + cmd)() - def fstrim(self, minimum=0): - return getattr(self.qga, 'fstrim')(minimum=minimum) + def fstrim(self, minimum: int) -> Dict[str, object]: + # returns GuestFilesystemTrimResponse + ret = getattr(self.qga, 'fstrim')(minimum=minimum) + assert isinstance(ret, dict) + return ret - def suspend(self, mode): + def suspend(self, mode: str) -> None: if mode not in ['disk', 'ram', 'hybrid']: raise Exception('Invalid mode: ' + mode) try: getattr(self.qga, 'suspend' + '_' + mode)() # On error exception will raise - except self.qga.timeout: + except TimeoutError: # On success command will timed out return - def shutdown(self, mode='powerdown'): + def shutdown(self, mode: str = 'powerdown') -> None: if mode not in ['powerdown', 'halt', 'reboot']: raise Exception('Invalid mode: ' + mode) try: self.qga.shutdown(mode=mode) - except self.qga.timeout: - return + except TimeoutError: + pass -def _cmd_cat(client, args): +def _cmd_cat(client: QemuGuestAgentClient, args: Sequence[str]) -> None: if len(args) != 1: print('Invalid argument') print('Usage: cat ') @@ -187,7 +198,7 @@ def _cmd_cat(client, args): print(client.read(args[0])) -def _cmd_fsfreeze(client, args): +def _cmd_fsfreeze(client: QemuGuestAgentClient, args: Sequence[str]) -> None: usage = 'Usage: fsfreeze status|freeze|thaw' if len(args) != 1: print('Invalid argument') @@ -201,13 +212,14 @@ def _cmd_fsfreeze(client, args): ret = client.fsfreeze(cmd) if cmd == 'status': print(ret) - elif cmd == 'freeze': - print("%d filesystems frozen" % ret) - else: - print("%d filesystems thawed" % ret) + return + + assert isinstance(ret, int) + verb = 'frozen' if cmd == 'freeze' else 'thawed' + print(f"{ret:d} filesystems {verb}") -def _cmd_fstrim(client, args): +def _cmd_fstrim(client: QemuGuestAgentClient, args: Sequence[str]) -> None: if len(args) == 0: minimum = 0 else: @@ -215,28 +227,25 @@ def _cmd_fstrim(client, args): print(client.fstrim(minimum)) -def _cmd_ifconfig(client, args): +def _cmd_ifconfig(client: QemuGuestAgentClient, args: Sequence[str]) -> None: assert not args print(client.ifconfig()) -def _cmd_info(client, args): +def _cmd_info(client: QemuGuestAgentClient, args: Sequence[str]) -> None: assert not args print(client.info()) -def _cmd_ping(client, args): - if len(args) == 0: - timeout = 3 - else: - timeout = float(args[0]) +def _cmd_ping(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + timeout = 3.0 if len(args) == 0 else float(args[0]) alive = client.ping(timeout) if not alive: print("Not responded in %s sec" % args[0]) sys.exit(1) -def _cmd_suspend(client, args): +def _cmd_suspend(client: QemuGuestAgentClient, args: Sequence[str]) -> None: usage = 'Usage: suspend disk|ram|hybrid' if len(args) != 1: print('Less argument') @@ -249,7 +258,7 @@ def _cmd_suspend(client, args): client.suspend(args[0]) -def _cmd_shutdown(client, args): +def _cmd_shutdown(client: QemuGuestAgentClient, args: Sequence[str]) -> None: assert not args client.shutdown() @@ -257,12 +266,12 @@ def _cmd_shutdown(client, args): _cmd_powerdown = _cmd_shutdown -def _cmd_halt(client, args): +def _cmd_halt(client: QemuGuestAgentClient, args: Sequence[str]) -> None: assert not args client.shutdown('halt') -def _cmd_reboot(client, args): +def _cmd_reboot(client: QemuGuestAgentClient, args: Sequence[str]) -> None: assert not args client.shutdown('reboot') @@ -270,7 +279,7 @@ def _cmd_reboot(client, args): commands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m] -def send_command(address, cmd, args): +def send_command(address: str, cmd: str, args: Sequence[str]) -> None: if not os.path.exists(address): print('%s not found' % address) sys.exit(1) @@ -296,7 +305,7 @@ def send_command(address, cmd, args): globals()['_cmd_' + cmd](client, args) -def main(): +def main() -> None: address = os.environ.get('QGA_CLIENT_ADDRESS') parser = argparse.ArgumentParser() -- cgit v1.2.3-55-g7522 From 1e129afc311a4a3897e83a308cce79e2c1cc3d89 Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:30 -0400 Subject: scripts/qemu-ga-client: move to python/qemu/qmp/qemu_ga_client.py The script itself will be unavailable for a few commits before being restored, with no way to run it right after this commit. This helps move git history into the new file. To prevent linter regressions, though, we do need to immediately touch up the filename to remove dashes (to make the module importable), and remove the executable bit. Signed-off-by: John Snow Message-id: 20210604155532.1499282-10-jsnow@redhat.com Signed-off-by: John Snow --- python/qemu/qmp/qemu_ga_client.py | 325 +++++++++++++++++++++++++++++++++++++ scripts/qmp/qemu-ga-client | 327 -------------------------------------- 2 files changed, 325 insertions(+), 327 deletions(-) create mode 100755 python/qemu/qmp/qemu_ga_client.py delete mode 100755 scripts/qmp/qemu-ga-client (limited to 'scripts') diff --git a/python/qemu/qmp/qemu_ga_client.py b/python/qemu/qmp/qemu_ga_client.py new file mode 100755 index 0000000000..d2938ad47c --- /dev/null +++ b/python/qemu/qmp/qemu_ga_client.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 + +""" +QEMU Guest Agent Client + +Usage: + +Start QEMU with: + +# qemu [...] -chardev socket,path=/tmp/qga.sock,server,wait=off,id=qga0 \ + -device virtio-serial \ + -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 + +Run the script: + +$ qemu-ga-client --address=/tmp/qga.sock [args...] + +or + +$ export QGA_CLIENT_ADDRESS=/tmp/qga.sock +$ qemu-ga-client [args...] + +For example: + +$ qemu-ga-client cat /etc/resolv.conf +# Generated by NetworkManager +nameserver 10.0.2.3 +$ qemu-ga-client fsfreeze status +thawed +$ qemu-ga-client fsfreeze freeze +2 filesystems frozen + +See also: https://wiki.qemu.org/Features/QAPI/GuestAgent +""" + +# Copyright (C) 2012 Ryota Ozaki +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. + +import argparse +import base64 +import errno +import os +import random +import sys +from typing import ( + Any, + Callable, + Dict, + Optional, + Sequence, +) + +from qemu import qmp +from qemu.qmp import SocketAddrT + + +# This script has not seen many patches or careful attention in quite +# some time. If you would like to improve it, please review the design +# carefully and add docstrings at that point in time. Until then: + +# pylint: disable=missing-docstring + + +class QemuGuestAgent(qmp.QEMUMonitorProtocol): + def __getattr__(self, name: str) -> Callable[..., Any]: + def wrapper(**kwds: object) -> object: + return self.command('guest-' + name.replace('_', '-'), **kwds) + return wrapper + + +class QemuGuestAgentClient: + def __init__(self, address: SocketAddrT): + self.qga = QemuGuestAgent(address) + self.qga.connect(negotiate=False) + + def sync(self, timeout: Optional[float] = 3) -> None: + # Avoid being blocked forever + if not self.ping(timeout): + raise EnvironmentError('Agent seems not alive') + uid = random.randint(0, (1 << 32) - 1) + while True: + ret = self.qga.sync(id=uid) + if isinstance(ret, int) and int(ret) == uid: + break + + def __file_read_all(self, handle: int) -> bytes: + eof = False + data = b'' + while not eof: + ret = self.qga.file_read(handle=handle, count=1024) + _data = base64.b64decode(ret['buf-b64']) + data += _data + eof = ret['eof'] + return data + + def read(self, path: str) -> bytes: + handle = self.qga.file_open(path=path) + try: + data = self.__file_read_all(handle) + finally: + self.qga.file_close(handle=handle) + return data + + def info(self) -> str: + info = self.qga.info() + + msgs = [] + msgs.append('version: ' + info['version']) + msgs.append('supported_commands:') + enabled = [c['name'] for c in info['supported_commands'] + if c['enabled']] + msgs.append('\tenabled: ' + ', '.join(enabled)) + disabled = [c['name'] for c in info['supported_commands'] + if not c['enabled']] + msgs.append('\tdisabled: ' + ', '.join(disabled)) + + return '\n'.join(msgs) + + @classmethod + def __gen_ipv4_netmask(cls, prefixlen: int) -> str: + mask = int('1' * prefixlen + '0' * (32 - prefixlen), 2) + return '.'.join([str(mask >> 24), + str((mask >> 16) & 0xff), + str((mask >> 8) & 0xff), + str(mask & 0xff)]) + + def ifconfig(self) -> str: + nifs = self.qga.network_get_interfaces() + + msgs = [] + for nif in nifs: + msgs.append(nif['name'] + ':') + if 'ip-addresses' in nif: + for ipaddr in nif['ip-addresses']: + if ipaddr['ip-address-type'] == 'ipv4': + addr = ipaddr['ip-address'] + mask = self.__gen_ipv4_netmask(int(ipaddr['prefix'])) + msgs.append(f"\tinet {addr} netmask {mask}") + elif ipaddr['ip-address-type'] == 'ipv6': + addr = ipaddr['ip-address'] + prefix = ipaddr['prefix'] + msgs.append(f"\tinet6 {addr} prefixlen {prefix}") + if nif['hardware-address'] != '00:00:00:00:00:00': + msgs.append("\tether " + nif['hardware-address']) + + return '\n'.join(msgs) + + def ping(self, timeout: Optional[float]) -> bool: + self.qga.settimeout(timeout) + try: + self.qga.ping() + except TimeoutError: + return False + return True + + def fsfreeze(self, cmd: str) -> object: + if cmd not in ['status', 'freeze', 'thaw']: + raise Exception('Invalid command: ' + cmd) + # Can be int (freeze, thaw) or GuestFsfreezeStatus (status) + return getattr(self.qga, 'fsfreeze' + '_' + cmd)() + + def fstrim(self, minimum: int) -> Dict[str, object]: + # returns GuestFilesystemTrimResponse + ret = getattr(self.qga, 'fstrim')(minimum=minimum) + assert isinstance(ret, dict) + return ret + + def suspend(self, mode: str) -> None: + if mode not in ['disk', 'ram', 'hybrid']: + raise Exception('Invalid mode: ' + mode) + + try: + getattr(self.qga, 'suspend' + '_' + mode)() + # On error exception will raise + except TimeoutError: + # On success command will timed out + return + + def shutdown(self, mode: str = 'powerdown') -> None: + if mode not in ['powerdown', 'halt', 'reboot']: + raise Exception('Invalid mode: ' + mode) + + try: + self.qga.shutdown(mode=mode) + except TimeoutError: + pass + + +def _cmd_cat(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + if len(args) != 1: + print('Invalid argument') + print('Usage: cat ') + sys.exit(1) + print(client.read(args[0])) + + +def _cmd_fsfreeze(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + usage = 'Usage: fsfreeze status|freeze|thaw' + if len(args) != 1: + print('Invalid argument') + print(usage) + sys.exit(1) + if args[0] not in ['status', 'freeze', 'thaw']: + print('Invalid command: ' + args[0]) + print(usage) + sys.exit(1) + cmd = args[0] + ret = client.fsfreeze(cmd) + if cmd == 'status': + print(ret) + return + + assert isinstance(ret, int) + verb = 'frozen' if cmd == 'freeze' else 'thawed' + print(f"{ret:d} filesystems {verb}") + + +def _cmd_fstrim(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + if len(args) == 0: + minimum = 0 + else: + minimum = int(args[0]) + print(client.fstrim(minimum)) + + +def _cmd_ifconfig(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + assert not args + print(client.ifconfig()) + + +def _cmd_info(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + assert not args + print(client.info()) + + +def _cmd_ping(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + timeout = 3.0 if len(args) == 0 else float(args[0]) + alive = client.ping(timeout) + if not alive: + print("Not responded in %s sec" % args[0]) + sys.exit(1) + + +def _cmd_suspend(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + usage = 'Usage: suspend disk|ram|hybrid' + if len(args) != 1: + print('Less argument') + print(usage) + sys.exit(1) + if args[0] not in ['disk', 'ram', 'hybrid']: + print('Invalid command: ' + args[0]) + print(usage) + sys.exit(1) + client.suspend(args[0]) + + +def _cmd_shutdown(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + assert not args + client.shutdown() + + +_cmd_powerdown = _cmd_shutdown + + +def _cmd_halt(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + assert not args + client.shutdown('halt') + + +def _cmd_reboot(client: QemuGuestAgentClient, args: Sequence[str]) -> None: + assert not args + client.shutdown('reboot') + + +commands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m] + + +def send_command(address: str, cmd: str, args: Sequence[str]) -> None: + if not os.path.exists(address): + print('%s not found' % address) + sys.exit(1) + + if cmd not in commands: + print('Invalid command: ' + cmd) + print('Available commands: ' + ', '.join(commands)) + sys.exit(1) + + try: + client = QemuGuestAgentClient(address) + except OSError as err: + print(err) + if err.errno == errno.ECONNREFUSED: + print('Hint: qemu is not running?') + sys.exit(1) + + if cmd == 'fsfreeze' and args[0] == 'freeze': + client.sync(60) + elif cmd != 'ping': + client.sync() + + globals()['_cmd_' + cmd](client, args) + + +def main() -> None: + address = os.environ.get('QGA_CLIENT_ADDRESS') + + parser = argparse.ArgumentParser() + parser.add_argument('--address', action='store', + default=address, + help='Specify a ip:port pair or a unix socket path') + parser.add_argument('command', choices=commands) + parser.add_argument('args', nargs='*') + + args = parser.parse_args() + if args.address is None: + parser.error('address is not specified') + sys.exit(1) + + send_command(args.address, args.command, args.args) + + +if __name__ == '__main__': + main() diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client deleted file mode 100755 index a7d0ef8347..0000000000 --- a/scripts/qmp/qemu-ga-client +++ /dev/null @@ -1,327 +0,0 @@ -#!/usr/bin/env python3 - -""" -QEMU Guest Agent Client - -Usage: - -Start QEMU with: - -# qemu [...] -chardev socket,path=/tmp/qga.sock,server,wait=off,id=qga0 \ - -device virtio-serial \ - -device virtserialport,chardev=qga0,name=org.qemu.guest_agent.0 - -Run the script: - -$ qemu-ga-client --address=/tmp/qga.sock [args...] - -or - -$ export QGA_CLIENT_ADDRESS=/tmp/qga.sock -$ qemu-ga-client [args...] - -For example: - -$ qemu-ga-client cat /etc/resolv.conf -# Generated by NetworkManager -nameserver 10.0.2.3 -$ qemu-ga-client fsfreeze status -thawed -$ qemu-ga-client fsfreeze freeze -2 filesystems frozen - -See also: https://wiki.qemu.org/Features/QAPI/GuestAgent -""" - -# Copyright (C) 2012 Ryota Ozaki -# -# This work is licensed under the terms of the GNU GPL, version 2. See -# the COPYING file in the top-level directory. - -import argparse -import base64 -import errno -import os -import random -import sys -from typing import ( - Any, - Callable, - Dict, - Optional, - Sequence, -) - - -sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu import qmp -from qemu.qmp import SocketAddrT - - -# This script has not seen many patches or careful attention in quite -# some time. If you would like to improve it, please review the design -# carefully and add docstrings at that point in time. Until then: - -# pylint: disable=missing-docstring - - -class QemuGuestAgent(qmp.QEMUMonitorProtocol): - def __getattr__(self, name: str) -> Callable[..., Any]: - def wrapper(**kwds: object) -> object: - return self.command('guest-' + name.replace('_', '-'), **kwds) - return wrapper - - -class QemuGuestAgentClient: - def __init__(self, address: SocketAddrT): - self.qga = QemuGuestAgent(address) - self.qga.connect(negotiate=False) - - def sync(self, timeout: Optional[float] = 3) -> None: - # Avoid being blocked forever - if not self.ping(timeout): - raise EnvironmentError('Agent seems not alive') - uid = random.randint(0, (1 << 32) - 1) - while True: - ret = self.qga.sync(id=uid) - if isinstance(ret, int) and int(ret) == uid: - break - - def __file_read_all(self, handle: int) -> bytes: - eof = False - data = b'' - while not eof: - ret = self.qga.file_read(handle=handle, count=1024) - _data = base64.b64decode(ret['buf-b64']) - data += _data - eof = ret['eof'] - return data - - def read(self, path: str) -> bytes: - handle = self.qga.file_open(path=path) - try: - data = self.__file_read_all(handle) - finally: - self.qga.file_close(handle=handle) - return data - - def info(self) -> str: - info = self.qga.info() - - msgs = [] - msgs.append('version: ' + info['version']) - msgs.append('supported_commands:') - enabled = [c['name'] for c in info['supported_commands'] - if c['enabled']] - msgs.append('\tenabled: ' + ', '.join(enabled)) - disabled = [c['name'] for c in info['supported_commands'] - if not c['enabled']] - msgs.append('\tdisabled: ' + ', '.join(disabled)) - - return '\n'.join(msgs) - - @classmethod - def __gen_ipv4_netmask(cls, prefixlen: int) -> str: - mask = int('1' * prefixlen + '0' * (32 - prefixlen), 2) - return '.'.join([str(mask >> 24), - str((mask >> 16) & 0xff), - str((mask >> 8) & 0xff), - str(mask & 0xff)]) - - def ifconfig(self) -> str: - nifs = self.qga.network_get_interfaces() - - msgs = [] - for nif in nifs: - msgs.append(nif['name'] + ':') - if 'ip-addresses' in nif: - for ipaddr in nif['ip-addresses']: - if ipaddr['ip-address-type'] == 'ipv4': - addr = ipaddr['ip-address'] - mask = self.__gen_ipv4_netmask(int(ipaddr['prefix'])) - msgs.append(f"\tinet {addr} netmask {mask}") - elif ipaddr['ip-address-type'] == 'ipv6': - addr = ipaddr['ip-address'] - prefix = ipaddr['prefix'] - msgs.append(f"\tinet6 {addr} prefixlen {prefix}") - if nif['hardware-address'] != '00:00:00:00:00:00': - msgs.append("\tether " + nif['hardware-address']) - - return '\n'.join(msgs) - - def ping(self, timeout: Optional[float]) -> bool: - self.qga.settimeout(timeout) - try: - self.qga.ping() - except TimeoutError: - return False - return True - - def fsfreeze(self, cmd: str) -> object: - if cmd not in ['status', 'freeze', 'thaw']: - raise Exception('Invalid command: ' + cmd) - # Can be int (freeze, thaw) or GuestFsfreezeStatus (status) - return getattr(self.qga, 'fsfreeze' + '_' + cmd)() - - def fstrim(self, minimum: int) -> Dict[str, object]: - # returns GuestFilesystemTrimResponse - ret = getattr(self.qga, 'fstrim')(minimum=minimum) - assert isinstance(ret, dict) - return ret - - def suspend(self, mode: str) -> None: - if mode not in ['disk', 'ram', 'hybrid']: - raise Exception('Invalid mode: ' + mode) - - try: - getattr(self.qga, 'suspend' + '_' + mode)() - # On error exception will raise - except TimeoutError: - # On success command will timed out - return - - def shutdown(self, mode: str = 'powerdown') -> None: - if mode not in ['powerdown', 'halt', 'reboot']: - raise Exception('Invalid mode: ' + mode) - - try: - self.qga.shutdown(mode=mode) - except TimeoutError: - pass - - -def _cmd_cat(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - if len(args) != 1: - print('Invalid argument') - print('Usage: cat ') - sys.exit(1) - print(client.read(args[0])) - - -def _cmd_fsfreeze(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - usage = 'Usage: fsfreeze status|freeze|thaw' - if len(args) != 1: - print('Invalid argument') - print(usage) - sys.exit(1) - if args[0] not in ['status', 'freeze', 'thaw']: - print('Invalid command: ' + args[0]) - print(usage) - sys.exit(1) - cmd = args[0] - ret = client.fsfreeze(cmd) - if cmd == 'status': - print(ret) - return - - assert isinstance(ret, int) - verb = 'frozen' if cmd == 'freeze' else 'thawed' - print(f"{ret:d} filesystems {verb}") - - -def _cmd_fstrim(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - if len(args) == 0: - minimum = 0 - else: - minimum = int(args[0]) - print(client.fstrim(minimum)) - - -def _cmd_ifconfig(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - assert not args - print(client.ifconfig()) - - -def _cmd_info(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - assert not args - print(client.info()) - - -def _cmd_ping(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - timeout = 3.0 if len(args) == 0 else float(args[0]) - alive = client.ping(timeout) - if not alive: - print("Not responded in %s sec" % args[0]) - sys.exit(1) - - -def _cmd_suspend(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - usage = 'Usage: suspend disk|ram|hybrid' - if len(args) != 1: - print('Less argument') - print(usage) - sys.exit(1) - if args[0] not in ['disk', 'ram', 'hybrid']: - print('Invalid command: ' + args[0]) - print(usage) - sys.exit(1) - client.suspend(args[0]) - - -def _cmd_shutdown(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - assert not args - client.shutdown() - - -_cmd_powerdown = _cmd_shutdown - - -def _cmd_halt(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - assert not args - client.shutdown('halt') - - -def _cmd_reboot(client: QemuGuestAgentClient, args: Sequence[str]) -> None: - assert not args - client.shutdown('reboot') - - -commands = [m.replace('_cmd_', '') for m in dir() if '_cmd_' in m] - - -def send_command(address: str, cmd: str, args: Sequence[str]) -> None: - if not os.path.exists(address): - print('%s not found' % address) - sys.exit(1) - - if cmd not in commands: - print('Invalid command: ' + cmd) - print('Available commands: ' + ', '.join(commands)) - sys.exit(1) - - try: - client = QemuGuestAgentClient(address) - except OSError as err: - print(err) - if err.errno == errno.ECONNREFUSED: - print('Hint: qemu is not running?') - sys.exit(1) - - if cmd == 'fsfreeze' and args[0] == 'freeze': - client.sync(60) - elif cmd != 'ping': - client.sync() - - globals()['_cmd_' + cmd](client, args) - - -def main() -> None: - address = os.environ.get('QGA_CLIENT_ADDRESS') - - parser = argparse.ArgumentParser() - parser.add_argument('--address', action='store', - default=address, - help='Specify a ip:port pair or a unix socket path') - parser.add_argument('command', choices=commands) - parser.add_argument('args', nargs='*') - - args = parser.parse_args() - if args.address is None: - parser.error('address is not specified') - sys.exit(1) - - send_command(args.address, args.command, args.args) - - -if __name__ == '__main__': - main() -- cgit v1.2.3-55-g7522 From 88fb483fc559e297ee681e161d70c4615b2a28ea Mon Sep 17 00:00:00 2001 From: John Snow Date: Fri, 4 Jun 2021 11:55:32 -0400 Subject: scripts/qemu-ga-client: Add forwarder shim Add a little forwarder shim until we are sure that everyone is comfortable with how to use the tools in their new packaged location. Signed-off-by: John Snow Message-id: 20210604155532.1499282-12-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qemu-ga-client | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 scripts/qmp/qemu-ga-client (limited to 'scripts') diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client new file mode 100755 index 0000000000..102fd2cad9 --- /dev/null +++ b/scripts/qmp/qemu-ga-client @@ -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.qmp import qemu_ga_client + + +if __name__ == '__main__': + sys.exit(qemu_ga_client.main()) -- cgit v1.2.3-55-g7522 From badf462985eb55a8f589d983ee65542972d16d3e Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:08 -0400 Subject: scripts/qmp-shell: apply isort rules Signed-off-by: John Snow Message-id: 20210607200649.1840382-2-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index d5ae8a9b21..a00efe6fea 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -65,18 +65,20 @@ # which will echo back the properly formatted JSON-compliant QMP that is being # sent to QEMU, which is useful for debugging and documentation generation. -import json import ast -import readline -import sys -import os -import errno import atexit +import errno +import json +import os import re +import readline +import sys + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) from qemu import qmp + class QMPCompleter(list): def complete(self, text, state): for cmd in self: -- cgit v1.2.3-55-g7522 From 169b43b367b874076c544984fc3e63e3c5c49763 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:09 -0400 Subject: scripts/qmp-shell: Apply flake8 rules A lot of fiddling around to get us below 80 columns. Signed-off-by: John Snow Message-id: 20210607200649.1840382-3-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 64 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 21 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index a00efe6fea..62a6377e06 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -88,6 +88,7 @@ class QMPCompleter(list): else: state -= 1 + class QMPShellError(Exception): pass @@ -105,6 +106,7 @@ class FuzzyJSON(ast.NodeTransformer): node.id = 'None' return node + # TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and # _execute_cmd()). Let's design a better one. class QMPShell(qmp.QEMUMonitorProtocol): @@ -131,8 +133,8 @@ class QMPShell(qmp.QEMUMonitorProtocol): readline.set_history_length(1024) readline.set_completer(self._completer.complete) readline.parse_and_bind("tab: complete") - # XXX: default delimiters conflict with some command names (eg. query-), - # clearing everything as it doesn't seem to matter + # NB: default delimiters conflict with some command names + # (eg. query-), clearing everything as it doesn't seem to matter readline.set_completer_delims('') try: readline.read_history_file(self._histfile) @@ -180,7 +182,9 @@ class QMPShell(qmp.QEMUMonitorProtocol): for arg in tokens: (key, sep, val) = arg.partition('=') if sep != '=': - raise QMPShellError("Expected a key=value pair, got '%s'" % arg) + raise QMPShellError( + f"Expected a key=value pair, got '{arg!s}'" + ) value = self.__parse_value(val) optpath = key.split('.') @@ -189,14 +193,16 @@ class QMPShell(qmp.QEMUMonitorProtocol): curpath.append(p) d = parent.get(p, {}) if type(d) is not dict: - raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath)) + msg = 'Cannot use "{:s}" as both leaf and non-leaf key' + raise QMPShellError(msg.format('.'.join(curpath))) parent[p] = d parent = d if optpath[-1] in parent: if type(parent[optpath[-1]]) is dict: - raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath)) + msg = 'Cannot use "{:s}" as both leaf and non-leaf key' + raise QMPShellError(msg.format('.'.join(curpath))) else: - raise QMPShellError('Cannot set "%s" multiple times' % key) + raise QMPShellError(f'Cannot set "{key}" multiple times') parent[optpath[-1]] = value def __build_cmd(self, cmdline): @@ -206,7 +212,8 @@ class QMPShell(qmp.QEMUMonitorProtocol): < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] """ - cmdargs = re.findall(r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''', cmdline) + argument_regex = r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''' + cmdargs = re.findall(argument_regex, cmdline) # Transactional CLI entry/exit: if cmdargs[0] == 'transaction(': @@ -215,9 +222,12 @@ class QMPShell(qmp.QEMUMonitorProtocol): elif cmdargs[0] == ')' and self._transmode: self._transmode = False if len(cmdargs) > 1: - raise QMPShellError("Unexpected input after close of Transaction sub-shell") - qmpcmd = { 'execute': 'transaction', - 'arguments': { 'actions': self._actions } } + msg = 'Unexpected input after close of Transaction sub-shell' + raise QMPShellError(msg) + qmpcmd = { + 'execute': 'transaction', + 'arguments': {'actions': self._actions} + } self._actions = list() return qmpcmd @@ -228,7 +238,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): # Parse and then cache this Transactional Action if self._transmode: finalize = False - action = { 'type': cmdargs[0], 'data': {} } + action = {'type': cmdargs[0], 'data': {}} if cmdargs[-1] == ')': cmdargs.pop(-1) finalize = True @@ -237,7 +247,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): return self.__build_cmd(')') if finalize else None # Standard command: parse and return it to be executed. - qmpcmd = { 'execute': cmdargs[0], 'arguments': {} } + qmpcmd = {'execute': cmdargs[0], 'arguments': {}} self.__cli_expr(cmdargs[1:], qmpcmd['arguments']) return qmpcmd @@ -278,7 +288,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): print('Connected') return version = self._greeting['QMP']['version']['qemu'] - print('Connected to QEMU %d.%d.%d\n' % (version['major'],version['minor'],version['micro'])) + print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version)) def get_prompt(self): if self._transmode: @@ -307,6 +317,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): def set_verbosity(self, verbose): self._verbose = verbose + class HMPShell(QMPShell): def __init__(self, address): QMPShell.__init__(self, address) @@ -315,7 +326,7 @@ class HMPShell(QMPShell): def __cmd_completion(self): for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'): if cmd and cmd[0] != '[' and cmd[0] != '\t': - name = cmd.split()[0] # drop help text + name = cmd.split()[0] # drop help text if name == 'info': continue if name.find('|') != -1: @@ -327,7 +338,7 @@ class HMPShell(QMPShell): else: name = opt[0] self._completer.append(name) - self._completer.append('help ' + name) # help completion + self._completer.append('help ' + name) # help completion def __info_completion(self): for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'): @@ -343,17 +354,21 @@ class HMPShell(QMPShell): self.__info_completion() self.__other_completion() - def __cmd_passthrough(self, cmdline, cpu_index = 0): - return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments': - { 'command-line': cmdline, - 'cpu-index': cpu_index } }) + def __cmd_passthrough(self, cmdline, cpu_index=0): + return self.cmd_obj({ + 'execute': 'human-monitor-command', + 'arguments': { + 'command-line': cmdline, + 'cpu-index': cpu_index + } + }) def _execute_cmd(self, cmdline): if cmdline.split()[0] == "cpu": # trap the cpu command, it requires special setting try: idx = int(cmdline.split()[1]) - if not 'return' in self.__cmd_passthrough('info version', idx): + if 'return' not in self.__cmd_passthrough('info version', idx): print('bad CPU index') return True self.__cpu_index = idx @@ -377,20 +392,26 @@ class HMPShell(QMPShell): def show_banner(self): QMPShell.show_banner(self, msg='Welcome to the HMP shell!') + def die(msg): sys.stderr.write('ERROR: %s\n' % msg) sys.exit(1) + def fail_cmdline(option=None): if option: sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option) - sys.stderr.write('qmp-shell [ -v ] [ -p ] [ -H ] [ -N ] < UNIX socket path> | < TCP address:port >\n') + sys.stderr.write( + 'qmp-shell [ -v ] [ -p ] [ -H ] [ -N ] ' + '< UNIX socket path> | < TCP address:port >\n' + ) sys.stderr.write(' -v Verbose (echo command sent and received)\n') sys.stderr.write(' -p Pretty-print JSON\n') sys.stderr.write(' -H Use HMP interface\n') sys.stderr.write(' -N Skip negotiate (for qemu-ga)\n') sys.exit(1) + def main(): addr = '' qemu = None @@ -440,5 +461,6 @@ def main(): pass qemu.close() + if __name__ == '__main__': main() -- cgit v1.2.3-55-g7522 From 70e56740181a980a5bb60c3b0223e34e2616caf4 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:10 -0400 Subject: scripts/qmp-shell: fix show_banner signature The signatures need to match. Signed-off-by: John Snow Message-id: 20210607200649.1840382-4-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 62a6377e06..18bf49bb26 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -389,8 +389,8 @@ class HMPShell(QMPShell): print('%s: %s' % (resp['error']['class'], resp['error']['desc'])) return True - def show_banner(self): - QMPShell.show_banner(self, msg='Welcome to the HMP shell!') + def show_banner(self, msg='Welcome to the HMP shell!'): + QMPShell.show_banner(self, msg) def die(msg): -- cgit v1.2.3-55-g7522 From de14ba24f3f31329debdfac9fa73c8d78e30399b Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:11 -0400 Subject: scripts/qmp-shell: fix exception handling Fixes: 50d189c Signed-off-by: John Snow Message-id: 20210607200649.1840382-5-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 18bf49bb26..413dd4d2de 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -452,7 +452,7 @@ def main(): die('Didn\'t get QMP greeting message') except qmp.QMPCapabilitiesError: die('Could not negotiate capabilities') - except qemu.error: + except OSError: die('Could not connect to %s' % addr) qemu.show_banner() -- cgit v1.2.3-55-g7522 From f2daa2d489d7b4f94288d6fafb8072fb1ea384a9 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:12 -0400 Subject: scripts/qmp-shell: fix connect method signature It needs to match the parent's signature -- the negotiate parameter must be optional. Signed-off-by: John Snow Message-id: 20210607200649.1840382-6-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 413dd4d2de..04ca6a25ae 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -278,7 +278,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): self._print(resp) return True - def connect(self, negotiate): + def connect(self, negotiate: bool = True): self._greeting = super(QMPShell, self).connect(negotiate) self.__completer_setup() -- cgit v1.2.3-55-g7522 From 9669c8289c8f16eb97ac887ab09598925f4712ce Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:13 -0400 Subject: scripts/qmp-shell: remove shadowed variable from _print() Don't use 'qmp' here, which shadows the qmp module. Signed-off-by: John Snow Message-id: 20210607200649.1840382-7-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 04ca6a25ae..ae3f04534a 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -251,11 +251,11 @@ class QMPShell(qmp.QEMUMonitorProtocol): self.__cli_expr(cmdargs[1:], qmpcmd['arguments']) return qmpcmd - def _print(self, qmp): + def _print(self, qmp_message): indent = None if self._pretty: indent = 4 - jsobj = json.dumps(qmp, indent=indent, sort_keys=self._pretty) + jsobj = json.dumps(qmp_message, indent=indent, sort_keys=self._pretty) print(str(jsobj)) def _execute_cmd(self, cmdline): -- cgit v1.2.3-55-g7522 From c4a1447fc13fd71df3813bfd451b98137f12b434 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:14 -0400 Subject: scripts/qmp-shell: use @classmethod where appropriate Methods with no self-use should belong to the class. Signed-off-by: John Snow Message-id: 20210607200649.1840382-8-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index ae3f04534a..f354549bf2 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -97,7 +97,8 @@ class FuzzyJSON(ast.NodeTransformer): '''This extension of ast.NodeTransformer filters literal "true/false/null" values in an AST and replaces them by proper "True/False/None" values that Python can properly evaluate.''' - def visit_Name(self, node): + @classmethod + def visit_Name(cls, node): if node.id == 'true': node.id = 'True' if node.id == 'false': @@ -152,7 +153,8 @@ class QMPShell(qmp.QEMUMonitorProtocol): except Exception as e: print("Failed to save history file '%s'; %s" % (self._histfile, e)) - def __parse_value(self, val): + @classmethod + def __parse_value(cls, val): try: return int(val) except ValueError: -- cgit v1.2.3-55-g7522 From 5cb0233861750908f5942ffb36061442e84c86da Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:15 -0400 Subject: scripts/qmp-shell: Use python3-style super() Signed-off-by: John Snow Message-id: 20210607200649.1840382-9-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index f354549bf2..3066e37ae5 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -112,7 +112,7 @@ class FuzzyJSON(ast.NodeTransformer): # _execute_cmd()). Let's design a better one. class QMPShell(qmp.QEMUMonitorProtocol): def __init__(self, address, pretty=False): - super(QMPShell, self).__init__(self.parse_address(address)) + super().__init__(self.parse_address(address)) self._greeting = None self._completer = None self._pretty = pretty @@ -281,7 +281,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): return True def connect(self, negotiate: bool = True): - self._greeting = super(QMPShell, self).connect(negotiate) + self._greeting = super().connect(negotiate) self.__completer_setup() def show_banner(self, msg='Welcome to the QMP low-level shell!'): @@ -322,7 +322,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): class HMPShell(QMPShell): def __init__(self, address): - QMPShell.__init__(self, address) + super().__init__(address) self.__cpu_index = 0 def __cmd_completion(self): -- cgit v1.2.3-55-g7522 From 31226369ab9fa654cdb37d977c5f396367065019 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:16 -0400 Subject: scripts/qmp-shell: declare verbose in __init__ Linters get angry when we don't define state at init time. Signed-off-by: John Snow Message-id: 20210607200649.1840382-10-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 1 + 1 file changed, 1 insertion(+) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 3066e37ae5..4027454324 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -120,6 +120,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): self._actions = list() self._histfile = os.path.join(os.path.expanduser('~'), '.qmp-shell_history') + self._verbose = False def _fill_completion(self): cmds = self.cmd('query-commands') -- cgit v1.2.3-55-g7522 From c6be2bf86eb93a2e6afc4a6818310303471b3212 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:17 -0400 Subject: scripts/qmp-shell: use triple-double-quote docstring style (2014 me had never written python before.) Signed-off-by: John Snow Message-id: 20210607200649.1840382-11-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 4027454324..c46f4f516b 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -94,9 +94,12 @@ class QMPShellError(Exception): class FuzzyJSON(ast.NodeTransformer): - '''This extension of ast.NodeTransformer filters literal "true/false/null" + """ + This extension of ast.NodeTransformer filters literal "true/false/null" values in an AST and replaces them by proper "True/False/None" values that - Python can properly evaluate.''' + Python can properly evaluate. + """ + @classmethod def visit_Name(cls, node): if node.id == 'true': -- cgit v1.2.3-55-g7522 From 50f6f1c3081322a1d9e2d0bbe5c9f434d712cf50 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:18 -0400 Subject: scripts/qmp-shell: ignore visit_Name name Not something I control, sorry, pylint. Signed-off-by: John Snow Message-id: 20210607200649.1840382-12-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index c46f4f516b..ea6a87e0b3 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -101,7 +101,7 @@ class FuzzyJSON(ast.NodeTransformer): """ @classmethod - def visit_Name(cls, node): + def visit_Name(cls, node): # pylint: disable=invalid-name if node.id == 'true': node.id = 'True' if node.id == 'false': -- cgit v1.2.3-55-g7522 From 2813dee0536c1b5d114c0fa0bdeb25317a38f486 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:19 -0400 Subject: scripts/qmp-shell: make QMPCompleter returns explicit This function returns None when it doesn't find a match; do that explicitly. Signed-off-by: John Snow Message-id: 20210607200649.1840382-13-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index ea6a87e0b3..8d84467b53 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -83,10 +83,10 @@ class QMPCompleter(list): def complete(self, text, state): for cmd in self: if cmd.startswith(text): - if not state: + if state == 0: return cmd - else: - state -= 1 + state -= 1 + return None class QMPShellError(Exception): -- cgit v1.2.3-55-g7522 From 628b92dd67a262ebeac12dba65905c8143ce710f Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:20 -0400 Subject: scripts/qmp-shell: rename one and two-letter variables A bit of churn and housekeeping for pylint, flake8 et al. Signed-off-by: John Snow Message-id: 20210607200649.1840382-14-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 8d84467b53..afb4b0c544 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -176,8 +176,8 @@ class QMPShell(qmp.QEMUMonitorProtocol): pass # Try once again as FuzzyJSON: try: - st = ast.parse(val, mode='eval') - return ast.literal_eval(FuzzyJSON().visit(st)) + tree = ast.parse(val, mode='eval') + return ast.literal_eval(FuzzyJSON().visit(tree)) except SyntaxError: pass except ValueError: @@ -195,14 +195,14 @@ class QMPShell(qmp.QEMUMonitorProtocol): value = self.__parse_value(val) optpath = key.split('.') curpath = [] - for p in optpath[:-1]: - curpath.append(p) - d = parent.get(p, {}) - if type(d) is not dict: + for path in optpath[:-1]: + curpath.append(path) + obj = parent.get(path, {}) + if type(obj) is not dict: msg = 'Cannot use "{:s}" as both leaf and non-leaf key' raise QMPShellError(msg.format('.'.join(curpath))) - parent[p] = d - parent = d + parent[path] = obj + parent = obj if optpath[-1] in parent: if type(parent[optpath[-1]]) is dict: msg = 'Cannot use "{:s}" as both leaf and non-leaf key' @@ -267,8 +267,8 @@ class QMPShell(qmp.QEMUMonitorProtocol): def _execute_cmd(self, cmdline): try: qmpcmd = self.__build_cmd(cmdline) - except Exception as e: - print('Error while parsing command line: %s' % e) + except Exception as err: + print('Error while parsing command line: %s' % err) print('command format: ', end=' ') print('[arg-name1=arg1] ... [arg-nameN=argN]') return True @@ -313,8 +313,8 @@ class QMPShell(qmp.QEMUMonitorProtocol): print() return False if cmdline == '': - for ev in self.get_events(): - print(ev) + for event in self.get_events(): + print(event) self.clear_events() return True else: -- cgit v1.2.3-55-g7522 From d962ec85ed188b04f35a28771c69845f09a3867e Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:21 -0400 Subject: scripts/qmp-shell: fix shell history exception handling We want to remove exceptions that are too broad here; we only want to catch IOErrors that get raised as a direct result of the open call. Signed-off-by: John Snow Message-id: 20210607200649.1840382-15-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index afb4b0c544..80cd432607 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -67,7 +67,6 @@ import ast import atexit -import errno import json import os import re @@ -143,19 +142,17 @@ class QMPShell(qmp.QEMUMonitorProtocol): readline.set_completer_delims('') try: readline.read_history_file(self._histfile) - except Exception as e: - if isinstance(e, IOError) and e.errno == errno.ENOENT: - # File not found. No problem. - pass - else: - print("Failed to read history '%s'; %s" % (self._histfile, e)) + except FileNotFoundError: + pass + except IOError as err: + print(f"Failed to read history '{self._histfile}': {err!s}") atexit.register(self.__save_history) def __save_history(self): try: readline.write_history_file(self._histfile) - except Exception as e: - print("Failed to save history file '%s'; %s" % (self._histfile, e)) + except IOError as err: + print(f"Failed to save history file '{self._histfile}': {err!s}") @classmethod def __parse_value(cls, val): -- cgit v1.2.3-55-g7522 From 73f699c903d429dfcd3b43232155cd515a854e80 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:22 -0400 Subject: scripts/qmp-shell: remove if-raise-else patterns Shushes pylint. I don't always mind these patterns personally, but I'm not as sure that I want to remove the warning from pylint's repertoire entirely. Oh well. Signed-off-by: John Snow Message-id: 20210607200649.1840382-16-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 80cd432607..bf7a49dfc1 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -204,8 +204,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): if type(parent[optpath[-1]]) is dict: msg = 'Cannot use "{:s}" as both leaf and non-leaf key' raise QMPShellError(msg.format('.'.join(curpath))) - else: - raise QMPShellError(f'Cannot set "{key}" multiple times') + raise QMPShellError(f'Cannot set "{key}" multiple times') parent[optpath[-1]] = value def __build_cmd(self, cmdline): @@ -309,13 +308,14 @@ class QMPShell(qmp.QEMUMonitorProtocol): except EOFError: print() return False + if cmdline == '': for event in self.get_events(): print(event) self.clear_events() return True - else: - return self._execute_cmd(cmdline) + + return self._execute_cmd(cmdline) def set_verbosity(self, verbose): self._verbose = verbose -- cgit v1.2.3-55-g7522 From 90bd8eb8dcdc94da964786ddedd90c30eb54ada7 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:23 -0400 Subject: scripts/qmp-shell: use isinstance() instead of type() A bit more idiomatic, and quiets some linter warnings. Signed-off-by: John Snow Message-id: 20210607200649.1840382-17-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index bf7a49dfc1..970f43dd00 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -195,13 +195,13 @@ class QMPShell(qmp.QEMUMonitorProtocol): for path in optpath[:-1]: curpath.append(path) obj = parent.get(path, {}) - if type(obj) is not dict: + if not isinstance(obj, dict): msg = 'Cannot use "{:s}" as both leaf and non-leaf key' raise QMPShellError(msg.format('.'.join(curpath))) parent[path] = obj parent = obj if optpath[-1] in parent: - if type(parent[optpath[-1]]) is dict: + if isinstance(parent[optpath[-1]], dict): msg = 'Cannot use "{:s}" as both leaf and non-leaf key' raise QMPShellError(msg.format('.'.join(curpath))) raise QMPShellError(f'Cannot set "{key}" multiple times') -- cgit v1.2.3-55-g7522 From 17329be2fd1557c3e1e30ce3d0f714fc70c4e5be Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:24 -0400 Subject: scripts/qmp-shell: use argparse Use argparse instead of an open-coded CLI parser, for consistency with everything else. Signed-off-by: John Snow Message-id: 20210607200649.1840382-18-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 84 ++++++++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 51 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 970f43dd00..5317dcd516 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -64,7 +64,7 @@ # Use the -v and -p options to activate the verbose and pretty-print options, # which will echo back the properly formatted JSON-compliant QMP that is being # sent to QEMU, which is useful for debugging and documentation generation. - +import argparse import ast import atexit import json @@ -401,65 +401,47 @@ def die(msg): sys.exit(1) -def fail_cmdline(option=None): - if option: - sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option) - sys.stderr.write( - 'qmp-shell [ -v ] [ -p ] [ -H ] [ -N ] ' - '< UNIX socket path> | < TCP address:port >\n' - ) - sys.stderr.write(' -v Verbose (echo command sent and received)\n') - sys.stderr.write(' -p Pretty-print JSON\n') - sys.stderr.write(' -H Use HMP interface\n') - sys.stderr.write(' -N Skip negotiate (for qemu-ga)\n') - sys.exit(1) - - def main(): - addr = '' - qemu = None - hmp = False - pretty = False - verbose = False - negotiate = True - + parser = argparse.ArgumentParser() + parser.add_argument('-H', '--hmp', action='store_true', + help='Use HMP interface') + parser.add_argument('-N', '--skip-negotiation', action='store_true', + help='Skip negotiate (for qemu-ga)') + 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') + + default_server = os.environ.get('QMP_SOCKET') + parser.add_argument('qmp_server', action='store', + default=default_server, + help='< UNIX socket path | TCP address:port >') + + args = parser.parse_args() + if args.qmp_server is None: + parser.error("QMP socket or TCP address must be specified") + + qemu: QMPShell try: - for arg in sys.argv[1:]: - if arg == "-H": - if qemu is not None: - fail_cmdline(arg) - hmp = True - elif arg == "-p": - pretty = True - elif arg == "-N": - negotiate = False - elif arg == "-v": - verbose = True - else: - if qemu is not None: - fail_cmdline(arg) - if hmp: - qemu = HMPShell(arg) - else: - qemu = QMPShell(arg, pretty) - addr = arg - - if qemu is None: - fail_cmdline() + if args.hmp: + qemu = HMPShell(args.qmp_server) + else: + qemu = QMPShell(args.qmp_server, args.pretty) except qmp.QMPBadPortError: - die('bad port number in command-line') + parser.error(f"Bad port number: {args.qmp_server}") + return # pycharm doesn't know error() is noreturn try: - qemu.connect(negotiate) + qemu.connect(negotiate=not args.skip_negotiation) except qmp.QMPConnectError: - die('Didn\'t get QMP greeting message') + die("Didn't get QMP greeting message") except qmp.QMPCapabilitiesError: - die('Could not negotiate capabilities') - except OSError: - die('Could not connect to %s' % addr) + die("Couldn't negotiate capabilities") + except OSError as err: + die(f"Couldn't connect to {args.qmp_server}: {err!s}") qemu.show_banner() - qemu.set_verbosity(verbose) + qemu.set_verbosity(args.verbose) while qemu.read_exec_command(qemu.get_prompt()): pass qemu.close() -- cgit v1.2.3-55-g7522 From ad459132c088dc9b33e86b8ad87f96742e4de4bf Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:25 -0400 Subject: scripts/qmp-shell: Add pretty attribute to HMP shell It's less useful, but it makes the initialization methods LSP consistent, which quiets a mypy complaint. Signed-off-by: John Snow Message-id: 20210607200649.1840382-19-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 5317dcd516..de5fa189f0 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -322,8 +322,8 @@ class QMPShell(qmp.QEMUMonitorProtocol): class HMPShell(QMPShell): - def __init__(self, address): - super().__init__(address) + def __init__(self, address, pretty=False): + super().__init__(address, pretty) self.__cpu_index = 0 def __cmd_completion(self): @@ -421,12 +421,9 @@ def main(): if args.qmp_server is None: parser.error("QMP socket or TCP address must be specified") - qemu: QMPShell + shell_class = HMPShell if args.hmp else QMPShell try: - if args.hmp: - qemu = HMPShell(args.qmp_server) - else: - qemu = QMPShell(args.qmp_server, args.pretty) + qemu = shell_class(args.qmp_server, args.pretty) except qmp.QMPBadPortError: parser.error(f"Bad port number: {args.qmp_server}") return # pycharm doesn't know error() is noreturn -- cgit v1.2.3-55-g7522 From 2ac3f3786e09c2ad40da16fa4ff4b0f99200f72e Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:26 -0400 Subject: scripts/qmp-shell: Make verbose a public attribute No real reason to hide this behind an underscore; make it part of the initializer and make it a regular RW attribute. Signed-off-by: John Snow Message-id: 20210607200649.1840382-20-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index de5fa189f0..cfcefb95f9 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -113,7 +113,7 @@ class FuzzyJSON(ast.NodeTransformer): # TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and # _execute_cmd()). Let's design a better one. class QMPShell(qmp.QEMUMonitorProtocol): - def __init__(self, address, pretty=False): + def __init__(self, address, pretty=False, verbose=False): super().__init__(self.parse_address(address)) self._greeting = None self._completer = None @@ -122,7 +122,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): self._actions = list() self._histfile = os.path.join(os.path.expanduser('~'), '.qmp-shell_history') - self._verbose = False + self.verbose = verbose def _fill_completion(self): cmds = self.cmd('query-commands') @@ -271,7 +271,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): # For transaction mode, we may have just cached the action: if qmpcmd is None: return True - if self._verbose: + if self.verbose: self._print(qmpcmd) resp = self.cmd_obj(qmpcmd) if resp is None: @@ -317,13 +317,10 @@ class QMPShell(qmp.QEMUMonitorProtocol): return self._execute_cmd(cmdline) - def set_verbosity(self, verbose): - self._verbose = verbose - class HMPShell(QMPShell): - def __init__(self, address, pretty=False): - super().__init__(address, pretty) + def __init__(self, address, pretty=False, verbose=False): + super().__init__(address, pretty, verbose) self.__cpu_index = 0 def __cmd_completion(self): @@ -423,7 +420,7 @@ def main(): shell_class = HMPShell if args.hmp else QMPShell try: - qemu = shell_class(args.qmp_server, args.pretty) + qemu = shell_class(args.qmp_server, args.pretty, args.verbose) except qmp.QMPBadPortError: parser.error(f"Bad port number: {args.qmp_server}") return # pycharm doesn't know error() is noreturn @@ -438,7 +435,6 @@ def main(): die(f"Couldn't connect to {args.qmp_server}: {err!s}") qemu.show_banner() - qemu.set_verbosity(args.verbose) while qemu.read_exec_command(qemu.get_prompt()): pass qemu.close() -- cgit v1.2.3-55-g7522 From 1caa505766dbf7bd233b088e82bd7a745d2ba325 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:27 -0400 Subject: scripts/qmp-shell: move get_prompt() to prompt property Small tidying; treat "prompt" like an immutable property instead of function/method/routine. Signed-off-by: John Snow Message-id: 20210607200649.1840382-21-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index cfcefb95f9..3b86ef7d88 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -292,10 +292,11 @@ class QMPShell(qmp.QEMUMonitorProtocol): version = self._greeting['QMP']['version']['qemu'] print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version)) - def get_prompt(self): + @property + def prompt(self): if self._transmode: - return "TRANS> " - return "(QEMU) " + return 'TRANS> ' + return '(QEMU) ' def read_exec_command(self, prompt): """ @@ -435,7 +436,7 @@ def main(): die(f"Couldn't connect to {args.qmp_server}: {err!s}") qemu.show_banner() - while qemu.read_exec_command(qemu.get_prompt()): + while qemu.read_exec_command(qemu.prompt): pass qemu.close() -- cgit v1.2.3-55-g7522 From 1215a1fbef205ba9930a976619163271d8195cdb Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:28 -0400 Subject: scripts/qmp-shell: remove prompt argument from read_exec_command It's only ever used by one caller, we can just absorb that logic. Signed-off-by: John Snow Message-id: 20210607200649.1840382-22-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 3b86ef7d88..31269859c4 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -298,14 +298,14 @@ class QMPShell(qmp.QEMUMonitorProtocol): return 'TRANS> ' return '(QEMU) ' - def read_exec_command(self, prompt): + def read_exec_command(self): """ Read and execute a command. @return True if execution was ok, return False if disconnected. """ try: - cmdline = input(prompt) + cmdline = input(self.prompt) except EOFError: print() return False @@ -436,7 +436,7 @@ def main(): die(f"Couldn't connect to {args.qmp_server}: {err!s}") qemu.show_banner() - while qemu.read_exec_command(qemu.prompt): + while qemu.read_exec_command(): pass qemu.close() -- cgit v1.2.3-55-g7522 From ad4eebee00a52a6f0e9761ffd0fd0002c259bc21 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:29 -0400 Subject: scripts/qmp-shell: move the REPL functionality into QMPShell Instead of doing this in main, move it into the class itself. (This makes it easier to put into the qemu.qmp package later by removing as much as we can from the main() function.) Signed-off-by: John Snow Message-id: 20210607200649.1840382-23-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 31269859c4..aa148517a8 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -318,6 +318,12 @@ class QMPShell(qmp.QEMUMonitorProtocol): return self._execute_cmd(cmdline) + def repl(self): + self.show_banner() + while self.read_exec_command(): + yield + self.close() + class HMPShell(QMPShell): def __init__(self, address, pretty=False, verbose=False): @@ -435,10 +441,8 @@ def main(): except OSError as err: die(f"Couldn't connect to {args.qmp_server}: {err!s}") - qemu.show_banner() - while qemu.read_exec_command(): + for _ in qemu.repl(): pass - qemu.close() if __name__ == '__main__': -- cgit v1.2.3-55-g7522 From 6faf2384ec78d5a1e0b5dfe430e80cf2278e45c4 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:30 -0400 Subject: scripts/qmp-shell: Fix "FuzzyJSON" parser I'm not sure when this regressed (Or maybe if it was ever working right to begin with?), but the Python AST requires you to change "Names" to "Constants" in order to truly convert `false` to `False`. Signed-off-by: John Snow Message-id: 20210607200649.1840382-24-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index aa148517a8..847d34890f 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -95,18 +95,19 @@ class QMPShellError(Exception): class FuzzyJSON(ast.NodeTransformer): """ This extension of ast.NodeTransformer filters literal "true/false/null" - values in an AST and replaces them by proper "True/False/None" values that - Python can properly evaluate. + values in a Python AST and replaces them by proper "True/False/None" values + that Python can properly evaluate. """ @classmethod - def visit_Name(cls, node): # pylint: disable=invalid-name + def visit_Name(cls, # pylint: disable=invalid-name + node: ast.Name) -> ast.AST: if node.id == 'true': - node.id = 'True' + return ast.Constant(value=True) if node.id == 'false': - node.id = 'False' + return ast.Constant(value=False) if node.id == 'null': - node.id = 'None' + return ast.Constant(value=None) return node @@ -174,10 +175,9 @@ class QMPShell(qmp.QEMUMonitorProtocol): # Try once again as FuzzyJSON: try: tree = ast.parse(val, mode='eval') - return ast.literal_eval(FuzzyJSON().visit(tree)) - except SyntaxError: - pass - except ValueError: + transformed = FuzzyJSON().visit(tree) + return ast.literal_eval(transformed) + except (SyntaxError, ValueError): pass return val -- cgit v1.2.3-55-g7522 From db12abc20859e93e802f668a2f744222c96ada63 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:31 -0400 Subject: scripts/qmp-shell: refactor QMPCompleter list is a generic type, but we expect to use strings directly. We could subclass list[str], but pylint does not presently understand that invocation. Change this class to envelop a list instead of *being* a list, for simpler mypy typing. Signed-off-by: John Snow Message-id: 20210607200649.1840382-25-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 847d34890f..73694035b2 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -78,9 +78,17 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) from qemu import qmp -class QMPCompleter(list): - def complete(self, text, state): - for cmd in self: +class QMPCompleter: + # NB: Python 3.9+ will probably allow us to subclass list[str] directly, + # but pylint as of today does not know that List[str] is simply 'list'. + def __init__(self) -> None: + self._matches: List[str] = [] + + def append(self, value: str) -> None: + return self._matches.append(value) + + def complete(self, text: str, state: int) -> Optional[str]: + for cmd in self._matches: if cmd.startswith(text): if state == 0: return cmd -- cgit v1.2.3-55-g7522 From 41574295829b9a34d43e7cb95847340942d1ddf5 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:32 -0400 Subject: scripts/qmp-shell: initialize completer early Add an empty completer as a more type-safe placeholder instead of 'None'. Signed-off-by: John Snow Message-id: 20210607200649.1840382-26-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 73694035b2..670361322c 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -125,7 +125,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): def __init__(self, address, pretty=False, verbose=False): super().__init__(self.parse_address(address)) self._greeting = None - self._completer = None + self._completer = QMPCompleter() self._pretty = pretty self._transmode = False self._actions = list() -- cgit v1.2.3-55-g7522 From 1eab88723c4c8c18cd67b4cad88dbbccf5916061 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:34 -0400 Subject: scripts/qmp-shell: add mypy types As per my usual, this patch is annotations only. Any changes with side effects are done elsewhere. Note: pylint does not understand the subscripts for Collection in Python 3.6, so use the stronger Sequence type as a workaround. Signed-off-by: John Snow Message-id: 20210607200649.1840382-28-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 67 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 26 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 670361322c..2d0e85b5f7 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -72,10 +72,18 @@ import os import re import readline import sys +from typing import ( + Iterator, + List, + NoReturn, + Optional, + Sequence, +) sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) from qemu import qmp +from qemu.qmp import QMPMessage class QMPCompleter: @@ -122,25 +130,26 @@ class FuzzyJSON(ast.NodeTransformer): # TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and # _execute_cmd()). Let's design a better one. class QMPShell(qmp.QEMUMonitorProtocol): - def __init__(self, address, pretty=False, verbose=False): + def __init__(self, address: str, pretty: bool = False, + verbose: bool = False): super().__init__(self.parse_address(address)) - self._greeting = None + self._greeting: Optional[QMPMessage] = None self._completer = QMPCompleter() self._pretty = pretty self._transmode = False - self._actions = list() + self._actions: List[QMPMessage] = [] self._histfile = os.path.join(os.path.expanduser('~'), '.qmp-shell_history') self.verbose = verbose - def _fill_completion(self): + def _fill_completion(self) -> None: cmds = self.cmd('query-commands') if 'error' in cmds: return for cmd in cmds['return']: self._completer.append(cmd['name']) - def __completer_setup(self): + def __completer_setup(self) -> None: self._completer = QMPCompleter() self._fill_completion() readline.set_history_length(1024) @@ -157,14 +166,14 @@ class QMPShell(qmp.QEMUMonitorProtocol): print(f"Failed to read history '{self._histfile}': {err!s}") atexit.register(self.__save_history) - def __save_history(self): + def __save_history(self) -> None: try: readline.write_history_file(self._histfile) except IOError as err: print(f"Failed to save history file '{self._histfile}': {err!s}") @classmethod - def __parse_value(cls, val): + def __parse_value(cls, val: str) -> object: try: return int(val) except ValueError: @@ -189,7 +198,9 @@ class QMPShell(qmp.QEMUMonitorProtocol): pass return val - def __cli_expr(self, tokens, parent): + def __cli_expr(self, + tokens: Sequence[str], + parent: qmp.QMPObject) -> None: for arg in tokens: (key, sep, val) = arg.partition('=') if sep != '=': @@ -215,7 +226,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): raise QMPShellError(f'Cannot set "{key}" multiple times') parent[optpath[-1]] = value - def __build_cmd(self, cmdline): + def __build_cmd(self, cmdline: str) -> Optional[QMPMessage]: """ Build a QMP input object from a user provided command-line in the following format: @@ -224,6 +235,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): """ argument_regex = r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''' cmdargs = re.findall(argument_regex, cmdline) + qmpcmd: QMPMessage # Transactional CLI entry/exit: if cmdargs[0] == 'transaction(': @@ -261,14 +273,14 @@ class QMPShell(qmp.QEMUMonitorProtocol): self.__cli_expr(cmdargs[1:], qmpcmd['arguments']) return qmpcmd - def _print(self, qmp_message): + def _print(self, qmp_message: object) -> None: indent = None if self._pretty: indent = 4 jsobj = json.dumps(qmp_message, indent=indent, sort_keys=self._pretty) print(str(jsobj)) - def _execute_cmd(self, cmdline): + def _execute_cmd(self, cmdline: str) -> bool: try: qmpcmd = self.__build_cmd(cmdline) except Exception as err: @@ -288,11 +300,12 @@ class QMPShell(qmp.QEMUMonitorProtocol): self._print(resp) return True - def connect(self, negotiate: bool = True): + def connect(self, negotiate: bool = True) -> None: self._greeting = super().connect(negotiate) self.__completer_setup() - def show_banner(self, msg='Welcome to the QMP low-level shell!'): + def show_banner(self, + msg: str = 'Welcome to the QMP low-level shell!') -> None: print(msg) if not self._greeting: print('Connected') @@ -301,12 +314,12 @@ class QMPShell(qmp.QEMUMonitorProtocol): print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version)) @property - def prompt(self): + def prompt(self) -> str: if self._transmode: return 'TRANS> ' return '(QEMU) ' - def read_exec_command(self): + def read_exec_command(self) -> bool: """ Read and execute a command. @@ -326,7 +339,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): return self._execute_cmd(cmdline) - def repl(self): + def repl(self) -> Iterator[None]: self.show_banner() while self.read_exec_command(): yield @@ -334,11 +347,12 @@ class QMPShell(qmp.QEMUMonitorProtocol): class HMPShell(QMPShell): - def __init__(self, address, pretty=False, verbose=False): + def __init__(self, address: str, + pretty: bool = False, verbose: bool = False): super().__init__(address, pretty, verbose) self.__cpu_index = 0 - def __cmd_completion(self): + def __cmd_completion(self) -> None: for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'): if cmd and cmd[0] != '[' and cmd[0] != '\t': name = cmd.split()[0] # drop help text @@ -355,21 +369,22 @@ class HMPShell(QMPShell): self._completer.append(name) self._completer.append('help ' + name) # help completion - def __info_completion(self): + def __info_completion(self) -> None: for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'): if cmd: self._completer.append('info ' + cmd.split()[1]) - def __other_completion(self): + def __other_completion(self) -> None: # special cases self._completer.append('help info') - def _fill_completion(self): + def _fill_completion(self) -> None: self.__cmd_completion() self.__info_completion() self.__other_completion() - def __cmd_passthrough(self, cmdline, cpu_index=0): + def __cmd_passthrough(self, cmdline: str, + cpu_index: int = 0) -> QMPMessage: return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments': { @@ -378,7 +393,7 @@ class HMPShell(QMPShell): } }) - def _execute_cmd(self, cmdline): + def _execute_cmd(self, cmdline: str) -> bool: if cmdline.split()[0] == "cpu": # trap the cpu command, it requires special setting try: @@ -404,16 +419,16 @@ class HMPShell(QMPShell): print('%s: %s' % (resp['error']['class'], resp['error']['desc'])) return True - def show_banner(self, msg='Welcome to the HMP shell!'): + def show_banner(self, msg: str = 'Welcome to the HMP shell!') -> None: QMPShell.show_banner(self, msg) -def die(msg): +def die(msg: str) -> NoReturn: sys.stderr.write('ERROR: %s\n' % msg) sys.exit(1) -def main(): +def main() -> None: parser = argparse.ArgumentParser() parser.add_argument('-H', '--hmp', action='store_true', help='Use HMP interface') -- cgit v1.2.3-55-g7522 From b0b8ca17e518bc57ae231ade4be4a7ed0e5f07f1 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:35 -0400 Subject: scripts/qmp-shell: Accept SocketAddrT instead of string Don't "extend" QEMUMonitorProtocol by changing the argument types. Move the string parsing just outside of the class instead. Signed-off-by: John Snow Message-id: 20210607200649.1840382-29-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 2d0e85b5f7..b465c7f9e2 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -130,9 +130,9 @@ class FuzzyJSON(ast.NodeTransformer): # TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and # _execute_cmd()). Let's design a better one. class QMPShell(qmp.QEMUMonitorProtocol): - def __init__(self, address: str, pretty: bool = False, - verbose: bool = False): - super().__init__(self.parse_address(address)) + def __init__(self, address: qmp.SocketAddrT, + pretty: bool = False, verbose: bool = False): + super().__init__(address) self._greeting: Optional[QMPMessage] = None self._completer = QMPCompleter() self._pretty = pretty @@ -347,7 +347,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): class HMPShell(QMPShell): - def __init__(self, address: str, + def __init__(self, address: qmp.SocketAddrT, pretty: bool = False, verbose: bool = False): super().__init__(address, pretty, verbose) self.__cpu_index = 0 @@ -450,11 +450,13 @@ def main() -> None: shell_class = HMPShell if args.hmp else QMPShell try: - qemu = shell_class(args.qmp_server, args.pretty, args.verbose) + address = shell_class.parse_address(args.qmp_server) except qmp.QMPBadPortError: parser.error(f"Bad port number: {args.qmp_server}") return # pycharm doesn't know error() is noreturn + qemu = shell_class(address, args.pretty, args.verbose) + try: qemu.connect(negotiate=not args.skip_negotiation) except qmp.QMPConnectError: -- cgit v1.2.3-55-g7522 From 6e24a7edb8b64854636acd1f74abd8e5f1735b50 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:36 -0400 Subject: scripts/qmp-shell: unprivatize 'pretty' property Similar to verbose, there's no reason this needs to be hidden. Signed-off-by: John Snow Message-id: 20210607200649.1840382-30-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index b465c7f9e2..f14fe211cc 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -135,11 +135,11 @@ class QMPShell(qmp.QEMUMonitorProtocol): super().__init__(address) self._greeting: Optional[QMPMessage] = None self._completer = QMPCompleter() - self._pretty = pretty self._transmode = False self._actions: List[QMPMessage] = [] self._histfile = os.path.join(os.path.expanduser('~'), '.qmp-shell_history') + self.pretty = pretty self.verbose = verbose def _fill_completion(self) -> None: @@ -274,10 +274,9 @@ class QMPShell(qmp.QEMUMonitorProtocol): return qmpcmd def _print(self, qmp_message: object) -> None: - indent = None - if self._pretty: - indent = 4 - jsobj = json.dumps(qmp_message, indent=indent, sort_keys=self._pretty) + jsobj = json.dumps(qmp_message, + indent=4 if self.pretty else None, + sort_keys=self.pretty) print(str(jsobj)) def _execute_cmd(self, cmdline: str) -> bool: -- cgit v1.2.3-55-g7522 From d1d14e59895b2ebd2953e9442225ffba56f80e9b Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:38 -0400 Subject: scripts/qmp-shell: Use context manager instead of atexit We can invoke the shell history writing when we leave the QMPShell scope instead of relying on atexit. Doing so may be preferable to avoid global state being registered from within a class instead of from the application logic directly. Use QMP's context manager to hook this history saving at close time, which gets invoked when we leave the context block. Signed-off-by: John Snow Message-id: 20210607200649.1840382-32-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index f14fe211cc..ec028d662e 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -66,7 +66,6 @@ # sent to QEMU, which is useful for debugging and documentation generation. import argparse import ast -import atexit import json import os import re @@ -142,6 +141,11 @@ class QMPShell(qmp.QEMUMonitorProtocol): self.pretty = pretty self.verbose = verbose + def close(self) -> None: + # Hook into context manager of parent to save shell history. + self._save_history() + super().close() + def _fill_completion(self) -> None: cmds = self.cmd('query-commands') if 'error' in cmds: @@ -164,9 +168,8 @@ class QMPShell(qmp.QEMUMonitorProtocol): pass except IOError as err: print(f"Failed to read history '{self._histfile}': {err!s}") - atexit.register(self.__save_history) - def __save_history(self) -> None: + def _save_history(self) -> None: try: readline.write_history_file(self._histfile) except IOError as err: @@ -448,25 +451,25 @@ def main() -> None: parser.error("QMP socket or TCP address must be specified") shell_class = HMPShell if args.hmp else QMPShell + try: address = shell_class.parse_address(args.qmp_server) except qmp.QMPBadPortError: parser.error(f"Bad port number: {args.qmp_server}") return # pycharm doesn't know error() is noreturn - qemu = shell_class(address, args.pretty, args.verbose) - - try: - qemu.connect(negotiate=not args.skip_negotiation) - except qmp.QMPConnectError: - die("Didn't get QMP greeting message") - except qmp.QMPCapabilitiesError: - die("Couldn't negotiate capabilities") - except OSError as err: - die(f"Couldn't connect to {args.qmp_server}: {err!s}") - - for _ in qemu.repl(): - pass + with shell_class(address, args.pretty, args.verbose) as qemu: + try: + qemu.connect(negotiate=not args.skip_negotiation) + except qmp.QMPConnectError: + die("Didn't get QMP greeting message") + except qmp.QMPCapabilitiesError: + die("Couldn't negotiate capabilities") + except OSError as err: + die(f"Couldn't connect to {args.qmp_server}: {err!s}") + + for _ in qemu.repl(): + pass if __name__ == '__main__': -- cgit v1.2.3-55-g7522 From be19c6a7126e5a463b3ef1ea23b985f6dc627a7a Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:39 -0400 Subject: scripts/qmp-shell: use logging to show warnings A perfect candidate is non-fatal shell history messages. Signed-off-by: John Snow Message-id: 20210607200649.1840382-33-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index ec028d662e..0199a13a34 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -67,6 +67,7 @@ import argparse import ast import json +import logging import os import re import readline @@ -85,6 +86,9 @@ from qemu import qmp from qemu.qmp import QMPMessage +LOG = logging.getLogger(__name__) + + class QMPCompleter: # NB: Python 3.9+ will probably allow us to subclass list[str] directly, # but pylint as of today does not know that List[str] is simply 'list'. @@ -167,13 +171,15 @@ class QMPShell(qmp.QEMUMonitorProtocol): except FileNotFoundError: pass except IOError as err: - print(f"Failed to read history '{self._histfile}': {err!s}") + msg = f"Failed to read history '{self._histfile}': {err!s}" + LOG.warning(msg) def _save_history(self) -> None: try: readline.write_history_file(self._histfile) except IOError as err: - print(f"Failed to save history file '{self._histfile}': {err!s}") + msg = f"Failed to save history file '{self._histfile}': {err!s}" + LOG.warning(msg) @classmethod def __parse_value(cls, val: str) -> object: -- cgit v1.2.3-55-g7522 From 74688377feb551101f8a3e3de483df56d30299e4 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:40 -0400 Subject: scripts/qmp-shell: remove TODO We still want to revamp qmp-shell again, but there's much more to the idea than the comment now intuits. Remove it. Signed-off-by: John Snow Message-id: 20210607200649.1840382-34-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 2 -- 1 file changed, 2 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 0199a13a34..3c32b576a3 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -130,8 +130,6 @@ class FuzzyJSON(ast.NodeTransformer): return node -# TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and -# _execute_cmd()). Let's design a better one. class QMPShell(qmp.QEMUMonitorProtocol): def __init__(self, address: qmp.SocketAddrT, pretty: bool = False, verbose: bool = False): -- cgit v1.2.3-55-g7522 From c83055ef1d47cd03667d7608f3f5ff232484146e Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:41 -0400 Subject: scripts/qmp-shell: Fix empty-transaction invocation calling "transaction( )" is pointless, but valid. Rework the parser to allow this kind of invocation. This helps clean up exception handling later by removing accidental breakages of the parser that aren't explicitly forbidden. Signed-off-by: John Snow Message-id: 20210607200649.1840382-35-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 3c32b576a3..78e4eae007 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -244,11 +244,14 @@ class QMPShell(qmp.QEMUMonitorProtocol): cmdargs = re.findall(argument_regex, cmdline) qmpcmd: QMPMessage - # Transactional CLI entry/exit: - if cmdargs[0] == 'transaction(': + # Transactional CLI entry: + if cmdargs and cmdargs[0] == 'transaction(': self._transmode = True + self._actions = [] cmdargs.pop(0) - elif cmdargs[0] == ')' and self._transmode: + + # Transactional CLI exit: + if cmdargs and cmdargs[0] == ')' and self._transmode: self._transmode = False if len(cmdargs) > 1: msg = 'Unexpected input after close of Transaction sub-shell' @@ -257,15 +260,14 @@ class QMPShell(qmp.QEMUMonitorProtocol): 'execute': 'transaction', 'arguments': {'actions': self._actions} } - self._actions = list() return qmpcmd - # Nothing to process? + # No args, or no args remaining if not cmdargs: return None - # Parse and then cache this Transactional Action if self._transmode: + # Parse and cache this Transactional Action finalize = False action = {'type': cmdargs[0], 'data': {}} if cmdargs[-1] == ')': -- cgit v1.2.3-55-g7522 From 26d3ce9e5e42920a6f9c1f481d900e63a636b07d Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:42 -0400 Subject: scripts/qmp-shell: Remove too-broad-exception We are only anticipating QMPShellErrors here, for syntax we weren't able to understand. Other errors, if any, should be allowed to percolate upwards. Signed-off-by: John Snow Message-id: 20210607200649.1840382-36-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 78e4eae007..8d5845ab48 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -291,10 +291,13 @@ class QMPShell(qmp.QEMUMonitorProtocol): def _execute_cmd(self, cmdline: str) -> bool: try: qmpcmd = self.__build_cmd(cmdline) - except Exception as err: - print('Error while parsing command line: %s' % err) - print('command format: ', end=' ') - print('[arg-name1=arg1] ... [arg-nameN=argN]') + except QMPShellError as err: + print( + f"Error while parsing command line: {err!s}\n" + "command format: " + "[arg-name1=arg1] ... [arg-nameN=argN", + file=sys.stderr + ) return True # For transaction mode, we may have just cached the action: if qmpcmd is None: -- cgit v1.2.3-55-g7522 From 7fc29896d237b6cb2db49e65f00882f554fc48c0 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:43 -0400 Subject: scripts/qmp-shell: convert usage comment to docstring The nice usage comment should be a docstring instead of a comment, so that it's visible from other python tooling. Signed-off-by: John Snow Message-id: 20210607200649.1840382-37-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 128 ++++++++++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 56 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 8d5845ab48..82fe16cff8 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -1,7 +1,5 @@ #!/usr/bin/env python3 # -# Low-level QEMU shell on top of QMP. -# # Copyright (C) 2009, 2010 Red Hat Inc. # # Authors: @@ -10,60 +8,78 @@ # This work is licensed under the terms of the GNU GPL, version 2. See # the COPYING file in the top-level directory. # -# Usage: -# -# Start QEMU with: -# -# # qemu [...] -qmp unix:./qmp-sock,server -# -# Run the shell: -# -# $ qmp-shell ./qmp-sock -# -# Commands have the following format: -# -# < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] -# -# For example: -# -# (QEMU) device_add driver=e1000 id=net1 -# {u'return': {}} -# (QEMU) -# -# key=value pairs also support Python or JSON object literal subset notations, -# without spaces. Dictionaries/objects {} are supported as are arrays []. -# -# example-command arg-name1={'key':'value','obj'={'prop':"value"}} -# -# Both JSON and Python formatting should work, including both styles of -# string literal quotes. Both paradigms of literal values should work, -# including null/true/false for JSON and None/True/False for Python. -# -# -# Transactions have the following multi-line format: -# -# transaction( -# action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ] -# ... -# action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ] -# ) -# -# One line transactions are also supported: -# -# transaction( action-name1 ... ) -# -# For example: -# -# (QEMU) transaction( -# TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1 -# TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0 -# TRANS> ) -# {"return": {}} -# (QEMU) -# -# Use the -v and -p options to activate the verbose and pretty-print options, -# which will echo back the properly formatted JSON-compliant QMP that is being -# sent to QEMU, which is useful for debugging and documentation generation. + +""" +Low-level QEMU shell on top of QMP. + +usage: qmp-shell [-h] [-H] [-N] [-v] [-p] qmp_server + +positional arguments: + qmp_server < UNIX socket path | TCP address:port > + +optional arguments: + -h, --help show this help message and exit + -H, --hmp Use HMP interface + -N, --skip-negotiation + Skip negotiate (for qemu-ga) + -v, --verbose Verbose (echo commands sent and received) + -p, --pretty Pretty-print JSON + + +Start QEMU with: + +# qemu [...] -qmp unix:./qmp-sock,server + +Run the shell: + +$ qmp-shell ./qmp-sock + +Commands have the following format: + + < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] + +For example: + +(QEMU) device_add driver=e1000 id=net1 +{'return': {}} +(QEMU) + +key=value pairs also support Python or JSON object literal subset notations, +without spaces. Dictionaries/objects {} are supported as are arrays []. + + example-command arg-name1={'key':'value','obj'={'prop':"value"}} + +Both JSON and Python formatting should work, including both styles of +string literal quotes. Both paradigms of literal values should work, +including null/true/false for JSON and None/True/False for Python. + + +Transactions have the following multi-line format: + + transaction( + action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ] + ... + action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ] + ) + +One line transactions are also supported: + + transaction( action-name1 ... ) + +For example: + + (QEMU) transaction( + TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1 + TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0 + TRANS> ) + {"return": {}} + (QEMU) + +Use the -v and -p options to activate the verbose and pretty-print options, +which will echo back the properly formatted JSON-compliant QMP that is being +sent to QEMU, which is useful for debugging and documentation generation. +""" + import argparse import ast import json -- cgit v1.2.3-55-g7522 From a64fe44d5abfd4aa933b17a8cf21d3c0b9df6143 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:44 -0400 Subject: scripts/qmp-shell: remove double-underscores They're not needed; single underscore is enough to express intent that these methods are "internal". double underscore is used as a weak name mangling, but that isn't beneficial for us here. Signed-off-by: John Snow Message-id: 20210607200649.1840382-38-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 52 +++++++++++++++++++++++++-------------------------- 1 file changed, 26 insertions(+), 26 deletions(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 82fe16cff8..40ff9e0a82 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -171,7 +171,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): for cmd in cmds['return']: self._completer.append(cmd['name']) - def __completer_setup(self) -> None: + def _completer_setup(self) -> None: self._completer = QMPCompleter() self._fill_completion() readline.set_history_length(1024) @@ -196,7 +196,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): LOG.warning(msg) @classmethod - def __parse_value(cls, val: str) -> object: + def _parse_value(cls, val: str) -> object: try: return int(val) except ValueError: @@ -221,9 +221,9 @@ class QMPShell(qmp.QEMUMonitorProtocol): pass return val - def __cli_expr(self, - tokens: Sequence[str], - parent: qmp.QMPObject) -> None: + def _cli_expr(self, + tokens: Sequence[str], + parent: qmp.QMPObject) -> None: for arg in tokens: (key, sep, val) = arg.partition('=') if sep != '=': @@ -231,7 +231,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): f"Expected a key=value pair, got '{arg!s}'" ) - value = self.__parse_value(val) + value = self._parse_value(val) optpath = key.split('.') curpath = [] for path in optpath[:-1]: @@ -249,7 +249,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): raise QMPShellError(f'Cannot set "{key}" multiple times') parent[optpath[-1]] = value - def __build_cmd(self, cmdline: str) -> Optional[QMPMessage]: + def _build_cmd(self, cmdline: str) -> Optional[QMPMessage]: """ Build a QMP input object from a user provided command-line in the following format: @@ -289,13 +289,13 @@ class QMPShell(qmp.QEMUMonitorProtocol): if cmdargs[-1] == ')': cmdargs.pop(-1) finalize = True - self.__cli_expr(cmdargs[1:], action['data']) + self._cli_expr(cmdargs[1:], action['data']) self._actions.append(action) - return self.__build_cmd(')') if finalize else None + return self._build_cmd(')') if finalize else None # Standard command: parse and return it to be executed. qmpcmd = {'execute': cmdargs[0], 'arguments': {}} - self.__cli_expr(cmdargs[1:], qmpcmd['arguments']) + self._cli_expr(cmdargs[1:], qmpcmd['arguments']) return qmpcmd def _print(self, qmp_message: object) -> None: @@ -306,7 +306,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): def _execute_cmd(self, cmdline: str) -> bool: try: - qmpcmd = self.__build_cmd(cmdline) + qmpcmd = self._build_cmd(cmdline) except QMPShellError as err: print( f"Error while parsing command line: {err!s}\n" @@ -329,7 +329,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): def connect(self, negotiate: bool = True) -> None: self._greeting = super().connect(negotiate) - self.__completer_setup() + self._completer_setup() def show_banner(self, msg: str = 'Welcome to the QMP low-level shell!') -> None: @@ -377,10 +377,10 @@ class HMPShell(QMPShell): def __init__(self, address: qmp.SocketAddrT, pretty: bool = False, verbose: bool = False): super().__init__(address, pretty, verbose) - self.__cpu_index = 0 + self._cpu_index = 0 - def __cmd_completion(self) -> None: - for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'): + def _cmd_completion(self) -> None: + for cmd in self._cmd_passthrough('help')['return'].split('\r\n'): if cmd and cmd[0] != '[' and cmd[0] != '\t': name = cmd.split()[0] # drop help text if name == 'info': @@ -396,22 +396,22 @@ class HMPShell(QMPShell): self._completer.append(name) self._completer.append('help ' + name) # help completion - def __info_completion(self) -> None: - for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'): + def _info_completion(self) -> None: + for cmd in self._cmd_passthrough('info')['return'].split('\r\n'): if cmd: self._completer.append('info ' + cmd.split()[1]) - def __other_completion(self) -> None: + def _other_completion(self) -> None: # special cases self._completer.append('help info') def _fill_completion(self) -> None: - self.__cmd_completion() - self.__info_completion() - self.__other_completion() + self._cmd_completion() + self._info_completion() + self._other_completion() - def __cmd_passthrough(self, cmdline: str, - cpu_index: int = 0) -> QMPMessage: + def _cmd_passthrough(self, cmdline: str, + cpu_index: int = 0) -> QMPMessage: return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments': { @@ -425,14 +425,14 @@ class HMPShell(QMPShell): # trap the cpu command, it requires special setting try: idx = int(cmdline.split()[1]) - if 'return' not in self.__cmd_passthrough('info version', idx): + if 'return' not in self._cmd_passthrough('info version', idx): print('bad CPU index') return True - self.__cpu_index = idx + self._cpu_index = idx except ValueError: print('cpu command takes an integer argument') return True - resp = self.__cmd_passthrough(cmdline, self.__cpu_index) + resp = self._cmd_passthrough(cmdline, self._cpu_index) if resp is None: print('Disconnected') return False -- cgit v1.2.3-55-g7522 From 6a1105adba41c3cd6371437b46c045ae9ea85314 Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:45 -0400 Subject: scripts/qmp-shell: make QMPShellError inherit QMPError In preparation for moving qmp-shell into the qemu.qmp package, make QMPShellError inherit from QMPError so that all custom errors in this package all derive from QMPError. Signed-off-by: John Snow Message-id: 20210607200649.1840382-39-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 40ff9e0a82..1a8a4ba18a 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -123,7 +123,7 @@ class QMPCompleter: return None -class QMPShellError(Exception): +class QMPShellError(qmp.QMPError): pass -- cgit v1.2.3-55-g7522 From e359c5a8b8e6184c15806d1408de085aab9c268b Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:46 -0400 Subject: scripts/qmp-shell: add docstrings Signed-off-by: John Snow Message-id: 20210607200649.1840382-40-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell index 1a8a4ba18a..15aedb80c2 100755 --- a/scripts/qmp/qmp-shell +++ b/scripts/qmp/qmp-shell @@ -106,15 +106,20 @@ LOG = logging.getLogger(__name__) class QMPCompleter: + """ + QMPCompleter provides a readline library tab-complete behavior. + """ # NB: Python 3.9+ will probably allow us to subclass list[str] directly, # but pylint as of today does not know that List[str] is simply 'list'. def __init__(self) -> None: self._matches: List[str] = [] def append(self, value: str) -> None: + """Append a new valid completion to the list of possibilities.""" return self._matches.append(value) def complete(self, text: str, state: int) -> Optional[str]: + """readline.set_completer() callback implementation.""" for cmd in self._matches: if cmd.startswith(text): if state == 0: @@ -124,7 +129,9 @@ class QMPCompleter: class QMPShellError(qmp.QMPError): - pass + """ + QMP Shell Base error class. + """ class FuzzyJSON(ast.NodeTransformer): @@ -137,6 +144,9 @@ class FuzzyJSON(ast.NodeTransformer): @classmethod def visit_Name(cls, # pylint: disable=invalid-name node: ast.Name) -> ast.AST: + """ + Transform Name nodes with certain values into Constant (keyword) nodes. + """ if node.id == 'true': return ast.Constant(value=True) if node.id == 'false': @@ -147,6 +157,13 @@ class FuzzyJSON(ast.NodeTransformer): class QMPShell(qmp.QEMUMonitorProtocol): + """ + QMPShell provides a basic readline-based QMP shell. + + :param address: Address of the QMP server. + :param pretty: Pretty-print QMP messages. + :param verbose: Echo outgoing QMP messages to console. + """ def __init__(self, address: qmp.SocketAddrT, pretty: bool = False, verbose: bool = False): super().__init__(address) @@ -333,6 +350,9 @@ class QMPShell(qmp.QEMUMonitorProtocol): def show_banner(self, msg: str = 'Welcome to the QMP low-level shell!') -> None: + """ + Print to stdio a greeting, and the QEMU version if available. + """ print(msg) if not self._greeting: print('Connected') @@ -342,6 +362,9 @@ class QMPShell(qmp.QEMUMonitorProtocol): @property def prompt(self) -> str: + """ + Return the current shell prompt, including a trailing space. + """ if self._transmode: return 'TRANS> ' return '(QEMU) ' @@ -367,6 +390,9 @@ class QMPShell(qmp.QEMUMonitorProtocol): return self._execute_cmd(cmdline) def repl(self) -> Iterator[None]: + """ + Return an iterator that implements the REPL. + """ self.show_banner() while self.read_exec_command(): yield @@ -374,6 +400,13 @@ class QMPShell(qmp.QEMUMonitorProtocol): class HMPShell(QMPShell): + """ + HMPShell provides a basic readline-based HMP shell, tunnelled via QMP. + + :param address: Address of the QMP server. + :param pretty: Pretty-print QMP messages. + :param verbose: Echo outgoing QMP messages to console. + """ def __init__(self, address: qmp.SocketAddrT, pretty: bool = False, verbose: bool = False): super().__init__(address, pretty, verbose) @@ -451,11 +484,15 @@ class HMPShell(QMPShell): def die(msg: str) -> NoReturn: + """Write an error to stderr, then exit with a return code of 1.""" sys.stderr.write('ERROR: %s\n' % msg) sys.exit(1) def main() -> None: + """ + qmp-shell entry point: parse command line arguments and start the REPL. + """ parser = argparse.ArgumentParser() parser.add_argument('-H', '--hmp', action='store_true', help='Use HMP interface') -- cgit v1.2.3-55-g7522 From 6be7206efc394b0232912e7055c7298ec2b0352d Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:47 -0400 Subject: scripts/qmp-shell: move to python/qemu/qmp/qmp_shell.py The script will be unavailable for a commit or two, which will help preserve development history attached to the new file. A forwarder will be added shortly afterwards. With qmp_shell in the python qemu.qmp package, now it is fully type checked, linted, etc. via the Python CI. It will be quite a bit harder to accidentally break it again in the future. Signed-off-by: John Snow Message-id: 20210607200649.1840382-41-jsnow@redhat.com Signed-off-by: John Snow --- python/qemu/qmp/qmp_shell.py | 535 ++++++++++++++++++++++++++++++++++++++++++ scripts/qmp/qmp-shell | 538 ------------------------------------------- 2 files changed, 535 insertions(+), 538 deletions(-) create mode 100644 python/qemu/qmp/qmp_shell.py delete mode 100755 scripts/qmp/qmp-shell (limited to 'scripts') diff --git a/python/qemu/qmp/qmp_shell.py b/python/qemu/qmp/qmp_shell.py new file mode 100644 index 0000000000..337acfce2d --- /dev/null +++ b/python/qemu/qmp/qmp_shell.py @@ -0,0 +1,535 @@ +# +# Copyright (C) 2009, 2010 Red Hat Inc. +# +# Authors: +# Luiz Capitulino +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +# + +""" +Low-level QEMU shell on top of QMP. + +usage: qmp-shell [-h] [-H] [-N] [-v] [-p] qmp_server + +positional arguments: + qmp_server < UNIX socket path | TCP address:port > + +optional arguments: + -h, --help show this help message and exit + -H, --hmp Use HMP interface + -N, --skip-negotiation + Skip negotiate (for qemu-ga) + -v, --verbose Verbose (echo commands sent and received) + -p, --pretty Pretty-print JSON + + +Start QEMU with: + +# qemu [...] -qmp unix:./qmp-sock,server + +Run the shell: + +$ qmp-shell ./qmp-sock + +Commands have the following format: + + < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] + +For example: + +(QEMU) device_add driver=e1000 id=net1 +{'return': {}} +(QEMU) + +key=value pairs also support Python or JSON object literal subset notations, +without spaces. Dictionaries/objects {} are supported as are arrays []. + + example-command arg-name1={'key':'value','obj'={'prop':"value"}} + +Both JSON and Python formatting should work, including both styles of +string literal quotes. Both paradigms of literal values should work, +including null/true/false for JSON and None/True/False for Python. + + +Transactions have the following multi-line format: + + transaction( + action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ] + ... + action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ] + ) + +One line transactions are also supported: + + transaction( action-name1 ... ) + +For example: + + (QEMU) transaction( + TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1 + TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0 + TRANS> ) + {"return": {}} + (QEMU) + +Use the -v and -p options to activate the verbose and pretty-print options, +which will echo back the properly formatted JSON-compliant QMP that is being +sent to QEMU, which is useful for debugging and documentation generation. +""" + +import argparse +import ast +import json +import logging +import os +import re +import readline +import sys +from typing import ( + Iterator, + List, + NoReturn, + Optional, + Sequence, +) + +from qemu import qmp +from qemu.qmp import QMPMessage + + +LOG = logging.getLogger(__name__) + + +class QMPCompleter: + """ + QMPCompleter provides a readline library tab-complete behavior. + """ + # NB: Python 3.9+ will probably allow us to subclass list[str] directly, + # but pylint as of today does not know that List[str] is simply 'list'. + def __init__(self) -> None: + self._matches: List[str] = [] + + def append(self, value: str) -> None: + """Append a new valid completion to the list of possibilities.""" + return self._matches.append(value) + + def complete(self, text: str, state: int) -> Optional[str]: + """readline.set_completer() callback implementation.""" + for cmd in self._matches: + if cmd.startswith(text): + if state == 0: + return cmd + state -= 1 + return None + + +class QMPShellError(qmp.QMPError): + """ + QMP Shell Base error class. + """ + + +class FuzzyJSON(ast.NodeTransformer): + """ + This extension of ast.NodeTransformer filters literal "true/false/null" + values in a Python AST and replaces them by proper "True/False/None" values + that Python can properly evaluate. + """ + + @classmethod + def visit_Name(cls, # pylint: disable=invalid-name + node: ast.Name) -> ast.AST: + """ + Transform Name nodes with certain values into Constant (keyword) nodes. + """ + if node.id == 'true': + return ast.Constant(value=True) + if node.id == 'false': + return ast.Constant(value=False) + if node.id == 'null': + return ast.Constant(value=None) + return node + + +class QMPShell(qmp.QEMUMonitorProtocol): + """ + QMPShell provides a basic readline-based QMP shell. + + :param address: Address of the QMP server. + :param pretty: Pretty-print QMP messages. + :param verbose: Echo outgoing QMP messages to console. + """ + def __init__(self, address: qmp.SocketAddrT, + pretty: bool = False, verbose: bool = False): + super().__init__(address) + self._greeting: Optional[QMPMessage] = None + self._completer = QMPCompleter() + self._transmode = False + self._actions: List[QMPMessage] = [] + self._histfile = os.path.join(os.path.expanduser('~'), + '.qmp-shell_history') + self.pretty = pretty + self.verbose = verbose + + def close(self) -> None: + # Hook into context manager of parent to save shell history. + self._save_history() + super().close() + + def _fill_completion(self) -> None: + cmds = self.cmd('query-commands') + if 'error' in cmds: + return + for cmd in cmds['return']: + self._completer.append(cmd['name']) + + def _completer_setup(self) -> None: + self._completer = QMPCompleter() + self._fill_completion() + readline.set_history_length(1024) + readline.set_completer(self._completer.complete) + readline.parse_and_bind("tab: complete") + # NB: default delimiters conflict with some command names + # (eg. query-), clearing everything as it doesn't seem to matter + readline.set_completer_delims('') + try: + readline.read_history_file(self._histfile) + except FileNotFoundError: + pass + except IOError as err: + msg = f"Failed to read history '{self._histfile}': {err!s}" + LOG.warning(msg) + + def _save_history(self) -> None: + try: + readline.write_history_file(self._histfile) + except IOError as err: + msg = f"Failed to save history file '{self._histfile}': {err!s}" + LOG.warning(msg) + + @classmethod + def _parse_value(cls, val: str) -> object: + try: + return int(val) + except ValueError: + pass + + if val.lower() == 'true': + return True + if val.lower() == 'false': + return False + if val.startswith(('{', '[')): + # Try first as pure JSON: + try: + return json.loads(val) + except ValueError: + pass + # Try once again as FuzzyJSON: + try: + tree = ast.parse(val, mode='eval') + transformed = FuzzyJSON().visit(tree) + return ast.literal_eval(transformed) + except (SyntaxError, ValueError): + pass + return val + + def _cli_expr(self, + tokens: Sequence[str], + parent: qmp.QMPObject) -> None: + for arg in tokens: + (key, sep, val) = arg.partition('=') + if sep != '=': + raise QMPShellError( + f"Expected a key=value pair, got '{arg!s}'" + ) + + value = self._parse_value(val) + optpath = key.split('.') + curpath = [] + for path in optpath[:-1]: + curpath.append(path) + obj = parent.get(path, {}) + if not isinstance(obj, dict): + msg = 'Cannot use "{:s}" as both leaf and non-leaf key' + raise QMPShellError(msg.format('.'.join(curpath))) + parent[path] = obj + parent = obj + if optpath[-1] in parent: + if isinstance(parent[optpath[-1]], dict): + msg = 'Cannot use "{:s}" as both leaf and non-leaf key' + raise QMPShellError(msg.format('.'.join(curpath))) + raise QMPShellError(f'Cannot set "{key}" multiple times') + parent[optpath[-1]] = value + + def _build_cmd(self, cmdline: str) -> Optional[QMPMessage]: + """ + Build a QMP input object from a user provided command-line in the + following format: + + < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] + """ + argument_regex = r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''' + cmdargs = re.findall(argument_regex, cmdline) + qmpcmd: QMPMessage + + # Transactional CLI entry: + if cmdargs and cmdargs[0] == 'transaction(': + self._transmode = True + self._actions = [] + cmdargs.pop(0) + + # Transactional CLI exit: + if cmdargs and cmdargs[0] == ')' and self._transmode: + self._transmode = False + if len(cmdargs) > 1: + msg = 'Unexpected input after close of Transaction sub-shell' + raise QMPShellError(msg) + qmpcmd = { + 'execute': 'transaction', + 'arguments': {'actions': self._actions} + } + return qmpcmd + + # No args, or no args remaining + if not cmdargs: + return None + + if self._transmode: + # Parse and cache this Transactional Action + finalize = False + action = {'type': cmdargs[0], 'data': {}} + if cmdargs[-1] == ')': + cmdargs.pop(-1) + finalize = True + self._cli_expr(cmdargs[1:], action['data']) + self._actions.append(action) + return self._build_cmd(')') if finalize else None + + # Standard command: parse and return it to be executed. + qmpcmd = {'execute': cmdargs[0], 'arguments': {}} + self._cli_expr(cmdargs[1:], qmpcmd['arguments']) + return qmpcmd + + 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, cmdline: str) -> bool: + try: + qmpcmd = self._build_cmd(cmdline) + except QMPShellError as err: + print( + f"Error while parsing command line: {err!s}\n" + "command format: " + "[arg-name1=arg1] ... [arg-nameN=argN", + file=sys.stderr + ) + return True + # For transaction mode, we may have just cached the action: + if qmpcmd is None: + return True + if self.verbose: + self._print(qmpcmd) + resp = self.cmd_obj(qmpcmd) + if resp is None: + print('Disconnected') + return False + self._print(resp) + return True + + def connect(self, negotiate: bool = True) -> None: + self._greeting = super().connect(negotiate) + self._completer_setup() + + def show_banner(self, + msg: str = 'Welcome to the QMP low-level shell!') -> None: + """ + Print to stdio a greeting, and the QEMU version if available. + """ + print(msg) + if not self._greeting: + print('Connected') + return + version = self._greeting['QMP']['version']['qemu'] + print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version)) + + @property + def prompt(self) -> str: + """ + Return the current shell prompt, including a trailing space. + """ + if self._transmode: + return 'TRANS> ' + return '(QEMU) ' + + def read_exec_command(self) -> bool: + """ + Read and execute a command. + + @return True if execution was ok, return False if disconnected. + """ + try: + cmdline = input(self.prompt) + except EOFError: + print() + return False + + if cmdline == '': + for event in self.get_events(): + print(event) + self.clear_events() + return True + + return self._execute_cmd(cmdline) + + def repl(self) -> Iterator[None]: + """ + Return an iterator that implements the REPL. + """ + self.show_banner() + while self.read_exec_command(): + yield + self.close() + + +class HMPShell(QMPShell): + """ + HMPShell provides a basic readline-based HMP shell, tunnelled via QMP. + + :param address: Address of the QMP server. + :param pretty: Pretty-print QMP messages. + :param verbose: Echo outgoing QMP messages to console. + """ + def __init__(self, address: qmp.SocketAddrT, + pretty: bool = False, verbose: bool = False): + super().__init__(address, pretty, verbose) + self._cpu_index = 0 + + def _cmd_completion(self) -> None: + for cmd in self._cmd_passthrough('help')['return'].split('\r\n'): + if cmd and cmd[0] != '[' and cmd[0] != '\t': + name = cmd.split()[0] # drop help text + if name == 'info': + continue + if name.find('|') != -1: + # Command in the form 'foobar|f' or 'f|foobar', take the + # full name + opt = name.split('|') + if len(opt[0]) == 1: + name = opt[1] + else: + name = opt[0] + self._completer.append(name) + self._completer.append('help ' + name) # help completion + + def _info_completion(self) -> None: + for cmd in self._cmd_passthrough('info')['return'].split('\r\n'): + if cmd: + self._completer.append('info ' + cmd.split()[1]) + + def _other_completion(self) -> None: + # special cases + self._completer.append('help info') + + def _fill_completion(self) -> None: + self._cmd_completion() + self._info_completion() + self._other_completion() + + def _cmd_passthrough(self, cmdline: str, + cpu_index: int = 0) -> QMPMessage: + return self.cmd_obj({ + 'execute': 'human-monitor-command', + 'arguments': { + 'command-line': cmdline, + 'cpu-index': cpu_index + } + }) + + def _execute_cmd(self, cmdline: str) -> bool: + if cmdline.split()[0] == "cpu": + # trap the cpu command, it requires special setting + try: + idx = int(cmdline.split()[1]) + if 'return' not in self._cmd_passthrough('info version', idx): + print('bad CPU index') + return True + self._cpu_index = idx + except ValueError: + print('cpu command takes an integer argument') + return True + resp = self._cmd_passthrough(cmdline, self._cpu_index) + if resp is None: + print('Disconnected') + return False + assert 'return' in resp or 'error' in resp + if 'return' in resp: + # Success + if len(resp['return']) > 0: + print(resp['return'], end=' ') + else: + # Error + print('%s: %s' % (resp['error']['class'], resp['error']['desc'])) + return True + + def show_banner(self, msg: str = 'Welcome to the HMP shell!') -> None: + QMPShell.show_banner(self, msg) + + +def die(msg: str) -> NoReturn: + """Write an error to stderr, then exit with a return code of 1.""" + sys.stderr.write('ERROR: %s\n' % msg) + sys.exit(1) + + +def main() -> None: + """ + qmp-shell entry point: parse command line arguments and start the REPL. + """ + parser = argparse.ArgumentParser() + parser.add_argument('-H', '--hmp', action='store_true', + help='Use HMP interface') + parser.add_argument('-N', '--skip-negotiation', action='store_true', + help='Skip negotiate (for qemu-ga)') + 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') + + default_server = os.environ.get('QMP_SOCKET') + parser.add_argument('qmp_server', action='store', + default=default_server, + help='< UNIX socket path | TCP address:port >') + + args = parser.parse_args() + if args.qmp_server is None: + parser.error("QMP socket or TCP address must be specified") + + shell_class = HMPShell if args.hmp else QMPShell + + try: + address = shell_class.parse_address(args.qmp_server) + except qmp.QMPBadPortError: + parser.error(f"Bad port number: {args.qmp_server}") + return # pycharm doesn't know error() is noreturn + + with shell_class(address, args.pretty, args.verbose) as qemu: + try: + qemu.connect(negotiate=not args.skip_negotiation) + except qmp.QMPConnectError: + die("Didn't get QMP greeting message") + except qmp.QMPCapabilitiesError: + die("Couldn't negotiate capabilities") + except OSError as err: + die(f"Couldn't connect to {args.qmp_server}: {err!s}") + + for _ in qemu.repl(): + pass + + +if __name__ == '__main__': + main() diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell deleted file mode 100755 index 15aedb80c2..0000000000 --- a/scripts/qmp/qmp-shell +++ /dev/null @@ -1,538 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2009, 2010 Red Hat Inc. -# -# Authors: -# Luiz Capitulino -# -# This work is licensed under the terms of the GNU GPL, version 2. See -# the COPYING file in the top-level directory. -# - -""" -Low-level QEMU shell on top of QMP. - -usage: qmp-shell [-h] [-H] [-N] [-v] [-p] qmp_server - -positional arguments: - qmp_server < UNIX socket path | TCP address:port > - -optional arguments: - -h, --help show this help message and exit - -H, --hmp Use HMP interface - -N, --skip-negotiation - Skip negotiate (for qemu-ga) - -v, --verbose Verbose (echo commands sent and received) - -p, --pretty Pretty-print JSON - - -Start QEMU with: - -# qemu [...] -qmp unix:./qmp-sock,server - -Run the shell: - -$ qmp-shell ./qmp-sock - -Commands have the following format: - - < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] - -For example: - -(QEMU) device_add driver=e1000 id=net1 -{'return': {}} -(QEMU) - -key=value pairs also support Python or JSON object literal subset notations, -without spaces. Dictionaries/objects {} are supported as are arrays []. - - example-command arg-name1={'key':'value','obj'={'prop':"value"}} - -Both JSON and Python formatting should work, including both styles of -string literal quotes. Both paradigms of literal values should work, -including null/true/false for JSON and None/True/False for Python. - - -Transactions have the following multi-line format: - - transaction( - action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ] - ... - action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ] - ) - -One line transactions are also supported: - - transaction( action-name1 ... ) - -For example: - - (QEMU) transaction( - TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1 - TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0 - TRANS> ) - {"return": {}} - (QEMU) - -Use the -v and -p options to activate the verbose and pretty-print options, -which will echo back the properly formatted JSON-compliant QMP that is being -sent to QEMU, which is useful for debugging and documentation generation. -""" - -import argparse -import ast -import json -import logging -import os -import re -import readline -import sys -from typing import ( - Iterator, - List, - NoReturn, - Optional, - Sequence, -) - - -sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) -from qemu import qmp -from qemu.qmp import QMPMessage - - -LOG = logging.getLogger(__name__) - - -class QMPCompleter: - """ - QMPCompleter provides a readline library tab-complete behavior. - """ - # NB: Python 3.9+ will probably allow us to subclass list[str] directly, - # but pylint as of today does not know that List[str] is simply 'list'. - def __init__(self) -> None: - self._matches: List[str] = [] - - def append(self, value: str) -> None: - """Append a new valid completion to the list of possibilities.""" - return self._matches.append(value) - - def complete(self, text: str, state: int) -> Optional[str]: - """readline.set_completer() callback implementation.""" - for cmd in self._matches: - if cmd.startswith(text): - if state == 0: - return cmd - state -= 1 - return None - - -class QMPShellError(qmp.QMPError): - """ - QMP Shell Base error class. - """ - - -class FuzzyJSON(ast.NodeTransformer): - """ - This extension of ast.NodeTransformer filters literal "true/false/null" - values in a Python AST and replaces them by proper "True/False/None" values - that Python can properly evaluate. - """ - - @classmethod - def visit_Name(cls, # pylint: disable=invalid-name - node: ast.Name) -> ast.AST: - """ - Transform Name nodes with certain values into Constant (keyword) nodes. - """ - if node.id == 'true': - return ast.Constant(value=True) - if node.id == 'false': - return ast.Constant(value=False) - if node.id == 'null': - return ast.Constant(value=None) - return node - - -class QMPShell(qmp.QEMUMonitorProtocol): - """ - QMPShell provides a basic readline-based QMP shell. - - :param address: Address of the QMP server. - :param pretty: Pretty-print QMP messages. - :param verbose: Echo outgoing QMP messages to console. - """ - def __init__(self, address: qmp.SocketAddrT, - pretty: bool = False, verbose: bool = False): - super().__init__(address) - self._greeting: Optional[QMPMessage] = None - self._completer = QMPCompleter() - self._transmode = False - self._actions: List[QMPMessage] = [] - self._histfile = os.path.join(os.path.expanduser('~'), - '.qmp-shell_history') - self.pretty = pretty - self.verbose = verbose - - def close(self) -> None: - # Hook into context manager of parent to save shell history. - self._save_history() - super().close() - - def _fill_completion(self) -> None: - cmds = self.cmd('query-commands') - if 'error' in cmds: - return - for cmd in cmds['return']: - self._completer.append(cmd['name']) - - def _completer_setup(self) -> None: - self._completer = QMPCompleter() - self._fill_completion() - readline.set_history_length(1024) - readline.set_completer(self._completer.complete) - readline.parse_and_bind("tab: complete") - # NB: default delimiters conflict with some command names - # (eg. query-), clearing everything as it doesn't seem to matter - readline.set_completer_delims('') - try: - readline.read_history_file(self._histfile) - except FileNotFoundError: - pass - except IOError as err: - msg = f"Failed to read history '{self._histfile}': {err!s}" - LOG.warning(msg) - - def _save_history(self) -> None: - try: - readline.write_history_file(self._histfile) - except IOError as err: - msg = f"Failed to save history file '{self._histfile}': {err!s}" - LOG.warning(msg) - - @classmethod - def _parse_value(cls, val: str) -> object: - try: - return int(val) - except ValueError: - pass - - if val.lower() == 'true': - return True - if val.lower() == 'false': - return False - if val.startswith(('{', '[')): - # Try first as pure JSON: - try: - return json.loads(val) - except ValueError: - pass - # Try once again as FuzzyJSON: - try: - tree = ast.parse(val, mode='eval') - transformed = FuzzyJSON().visit(tree) - return ast.literal_eval(transformed) - except (SyntaxError, ValueError): - pass - return val - - def _cli_expr(self, - tokens: Sequence[str], - parent: qmp.QMPObject) -> None: - for arg in tokens: - (key, sep, val) = arg.partition('=') - if sep != '=': - raise QMPShellError( - f"Expected a key=value pair, got '{arg!s}'" - ) - - value = self._parse_value(val) - optpath = key.split('.') - curpath = [] - for path in optpath[:-1]: - curpath.append(path) - obj = parent.get(path, {}) - if not isinstance(obj, dict): - msg = 'Cannot use "{:s}" as both leaf and non-leaf key' - raise QMPShellError(msg.format('.'.join(curpath))) - parent[path] = obj - parent = obj - if optpath[-1] in parent: - if isinstance(parent[optpath[-1]], dict): - msg = 'Cannot use "{:s}" as both leaf and non-leaf key' - raise QMPShellError(msg.format('.'.join(curpath))) - raise QMPShellError(f'Cannot set "{key}" multiple times') - parent[optpath[-1]] = value - - def _build_cmd(self, cmdline: str) -> Optional[QMPMessage]: - """ - Build a QMP input object from a user provided command-line in the - following format: - - < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] - """ - argument_regex = r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''' - cmdargs = re.findall(argument_regex, cmdline) - qmpcmd: QMPMessage - - # Transactional CLI entry: - if cmdargs and cmdargs[0] == 'transaction(': - self._transmode = True - self._actions = [] - cmdargs.pop(0) - - # Transactional CLI exit: - if cmdargs and cmdargs[0] == ')' and self._transmode: - self._transmode = False - if len(cmdargs) > 1: - msg = 'Unexpected input after close of Transaction sub-shell' - raise QMPShellError(msg) - qmpcmd = { - 'execute': 'transaction', - 'arguments': {'actions': self._actions} - } - return qmpcmd - - # No args, or no args remaining - if not cmdargs: - return None - - if self._transmode: - # Parse and cache this Transactional Action - finalize = False - action = {'type': cmdargs[0], 'data': {}} - if cmdargs[-1] == ')': - cmdargs.pop(-1) - finalize = True - self._cli_expr(cmdargs[1:], action['data']) - self._actions.append(action) - return self._build_cmd(')') if finalize else None - - # Standard command: parse and return it to be executed. - qmpcmd = {'execute': cmdargs[0], 'arguments': {}} - self._cli_expr(cmdargs[1:], qmpcmd['arguments']) - return qmpcmd - - 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, cmdline: str) -> bool: - try: - qmpcmd = self._build_cmd(cmdline) - except QMPShellError as err: - print( - f"Error while parsing command line: {err!s}\n" - "command format: " - "[arg-name1=arg1] ... [arg-nameN=argN", - file=sys.stderr - ) - return True - # For transaction mode, we may have just cached the action: - if qmpcmd is None: - return True - if self.verbose: - self._print(qmpcmd) - resp = self.cmd_obj(qmpcmd) - if resp is None: - print('Disconnected') - return False - self._print(resp) - return True - - def connect(self, negotiate: bool = True) -> None: - self._greeting = super().connect(negotiate) - self._completer_setup() - - def show_banner(self, - msg: str = 'Welcome to the QMP low-level shell!') -> None: - """ - Print to stdio a greeting, and the QEMU version if available. - """ - print(msg) - if not self._greeting: - print('Connected') - return - version = self._greeting['QMP']['version']['qemu'] - print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version)) - - @property - def prompt(self) -> str: - """ - Return the current shell prompt, including a trailing space. - """ - if self._transmode: - return 'TRANS> ' - return '(QEMU) ' - - def read_exec_command(self) -> bool: - """ - Read and execute a command. - - @return True if execution was ok, return False if disconnected. - """ - try: - cmdline = input(self.prompt) - except EOFError: - print() - return False - - if cmdline == '': - for event in self.get_events(): - print(event) - self.clear_events() - return True - - return self._execute_cmd(cmdline) - - def repl(self) -> Iterator[None]: - """ - Return an iterator that implements the REPL. - """ - self.show_banner() - while self.read_exec_command(): - yield - self.close() - - -class HMPShell(QMPShell): - """ - HMPShell provides a basic readline-based HMP shell, tunnelled via QMP. - - :param address: Address of the QMP server. - :param pretty: Pretty-print QMP messages. - :param verbose: Echo outgoing QMP messages to console. - """ - def __init__(self, address: qmp.SocketAddrT, - pretty: bool = False, verbose: bool = False): - super().__init__(address, pretty, verbose) - self._cpu_index = 0 - - def _cmd_completion(self) -> None: - for cmd in self._cmd_passthrough('help')['return'].split('\r\n'): - if cmd and cmd[0] != '[' and cmd[0] != '\t': - name = cmd.split()[0] # drop help text - if name == 'info': - continue - if name.find('|') != -1: - # Command in the form 'foobar|f' or 'f|foobar', take the - # full name - opt = name.split('|') - if len(opt[0]) == 1: - name = opt[1] - else: - name = opt[0] - self._completer.append(name) - self._completer.append('help ' + name) # help completion - - def _info_completion(self) -> None: - for cmd in self._cmd_passthrough('info')['return'].split('\r\n'): - if cmd: - self._completer.append('info ' + cmd.split()[1]) - - def _other_completion(self) -> None: - # special cases - self._completer.append('help info') - - def _fill_completion(self) -> None: - self._cmd_completion() - self._info_completion() - self._other_completion() - - def _cmd_passthrough(self, cmdline: str, - cpu_index: int = 0) -> QMPMessage: - return self.cmd_obj({ - 'execute': 'human-monitor-command', - 'arguments': { - 'command-line': cmdline, - 'cpu-index': cpu_index - } - }) - - def _execute_cmd(self, cmdline: str) -> bool: - if cmdline.split()[0] == "cpu": - # trap the cpu command, it requires special setting - try: - idx = int(cmdline.split()[1]) - if 'return' not in self._cmd_passthrough('info version', idx): - print('bad CPU index') - return True - self._cpu_index = idx - except ValueError: - print('cpu command takes an integer argument') - return True - resp = self._cmd_passthrough(cmdline, self._cpu_index) - if resp is None: - print('Disconnected') - return False - assert 'return' in resp or 'error' in resp - if 'return' in resp: - # Success - if len(resp['return']) > 0: - print(resp['return'], end=' ') - else: - # Error - print('%s: %s' % (resp['error']['class'], resp['error']['desc'])) - return True - - def show_banner(self, msg: str = 'Welcome to the HMP shell!') -> None: - QMPShell.show_banner(self, msg) - - -def die(msg: str) -> NoReturn: - """Write an error to stderr, then exit with a return code of 1.""" - sys.stderr.write('ERROR: %s\n' % msg) - sys.exit(1) - - -def main() -> None: - """ - qmp-shell entry point: parse command line arguments and start the REPL. - """ - parser = argparse.ArgumentParser() - parser.add_argument('-H', '--hmp', action='store_true', - help='Use HMP interface') - parser.add_argument('-N', '--skip-negotiation', action='store_true', - help='Skip negotiate (for qemu-ga)') - 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') - - default_server = os.environ.get('QMP_SOCKET') - parser.add_argument('qmp_server', action='store', - default=default_server, - help='< UNIX socket path | TCP address:port >') - - args = parser.parse_args() - if args.qmp_server is None: - parser.error("QMP socket or TCP address must be specified") - - shell_class = HMPShell if args.hmp else QMPShell - - try: - address = shell_class.parse_address(args.qmp_server) - except qmp.QMPBadPortError: - parser.error(f"Bad port number: {args.qmp_server}") - return # pycharm doesn't know error() is noreturn - - with shell_class(address, args.pretty, args.verbose) as qemu: - try: - qemu.connect(negotiate=not args.skip_negotiation) - except qmp.QMPConnectError: - die("Didn't get QMP greeting message") - except qmp.QMPCapabilitiesError: - die("Couldn't negotiate capabilities") - except OSError as err: - die(f"Couldn't connect to {args.qmp_server}: {err!s}") - - for _ in qemu.repl(): - pass - - -if __name__ == '__main__': - main() -- cgit v1.2.3-55-g7522 From d08caefe6648fc0713af5361e2b88bee53b67ebb Mon Sep 17 00:00:00 2001 From: John Snow Date: Mon, 7 Jun 2021 16:06:49 -0400 Subject: scripts/qmp-shell: add redirection shim qmp-shell has a new home, add a redirect for a little while as the dust settles. Signed-off-by: John Snow Message-id: 20210607200649.1840382-43-jsnow@redhat.com Signed-off-by: John Snow --- scripts/qmp/qmp-shell | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100755 scripts/qmp/qmp-shell (limited to 'scripts') diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell new file mode 100755 index 0000000000..4a20f97db7 --- /dev/null +++ b/scripts/qmp/qmp-shell @@ -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.qmp import qmp_shell + + +if __name__ == '__main__': + qmp_shell.main() -- cgit v1.2.3-55-g7522