diff --git a/Python-aqmp-fix-type-definitions-for-myp.patch b/Python-aqmp-fix-type-definitions-for-myp.patch new file mode 100644 index 00000000..dfca8007 --- /dev/null +++ b/Python-aqmp-fix-type-definitions-for-myp.patch @@ -0,0 +1,48 @@ +From: John Snow +Date: Mon, 10 Jan 2022 14:13:48 -0500 +Subject: Python/aqmp: fix type definitions for mypy 0.920 + +Git-commit: 42d73f2894ea1855df5a25d58e0d9eac6023dcc3 + +0.920 (Released 2021-12-15) is not entirely happy with the +way that I was defining _FutureT: + +qemu/aqmp/protocol.py:601: error: Item "object" of the upper bound +"Optional[Future[Any]]" of type variable "_FutureT" has no attribute +"done" + +Update it with something a little mechanically simpler that works better +across a wider array of mypy versions. + +Signed-off-by: John Snow +Message-id: 20220110191349.1841027-3-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/protocol.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 5190b33b13df24fc2ca4aed934ed..c4fbe35a0e41c589059ec4fa37a8 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -43,8 +43,8 @@ from .util import ( + + + T = TypeVar('T') ++_U = TypeVar('_U') + _TaskFN = Callable[[], Awaitable[None]] # aka ``async def func() -> None`` +-_FutureT = TypeVar('_FutureT', bound=Optional['asyncio.Future[Any]']) + + + class Runstate(Enum): +@@ -591,7 +591,8 @@ class AsyncProtocol(Generic[T]): + """ + Fully reset this object to a clean state and return to `IDLE`. + """ +- def _paranoid_task_erase(task: _FutureT) -> Optional[_FutureT]: ++ def _paranoid_task_erase(task: Optional['asyncio.Future[_U]'] ++ ) -> Optional['asyncio.Future[_U]']: + # Help to erase a task, ENSURING it is fully quiesced first. + assert (task is None) or task.done() + return None if (task and task.done()) else task diff --git a/Revert-python-iotests-replace-qmp-with-a.patch b/Revert-python-iotests-replace-qmp-with-a.patch deleted file mode 100644 index 1c4b5aee..00000000 --- a/Revert-python-iotests-replace-qmp-with-a.patch +++ /dev/null @@ -1,39 +0,0 @@ -From: Li Zhang -Date: Tue, 29 Mar 2022 12:04:16 +0200 -Subject: Revert "python, iotests: replace qmp with aqmp" - -References: bsc#1197528 bsc#1197150 - -aqmp is still not stable, it causes failures. -This reverts commit 76cd358671e6b8e7c435ec65b1c44200254514a9. - -Signed-off-by: Li Zhang ---- - python/qemu/machine/machine.py | 7 +------ - 1 file changed, 1 insertion(+), 6 deletions(-) - -diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py -index a487c397459a1fa6889276ab2538..a0cf69786b4bb7e851b5eeb2517b 100644 ---- a/python/qemu/machine/machine.py -+++ b/python/qemu/machine/machine.py -@@ -41,6 +41,7 @@ from typing import ( - ) - - from qemu.qmp import ( # pylint: disable=import-error -+ QEMUMonitorProtocol, - QMPMessage, - QMPReturnValue, - SocketAddrT, -@@ -49,12 +50,6 @@ from qemu.qmp import ( # pylint: disable=import-error - from . import console_socket - - --if os.environ.get('QEMU_PYTHON_LEGACY_QMP'): -- from qemu.qmp import QEMUMonitorProtocol --else: -- from qemu.aqmp.legacy import QEMUMonitorProtocol -- -- - LOG = logging.getLogger(__name__) - - diff --git a/Revert-python-machine-add-instance-disam.patch b/Revert-python-machine-add-instance-disam.patch deleted file mode 100644 index 810e70e5..00000000 --- a/Revert-python-machine-add-instance-disam.patch +++ /dev/null @@ -1,28 +0,0 @@ -From: Li Zhang -Date: Tue, 29 Mar 2022 12:00:29 +0200 -Subject: Revert "python/machine: add instance disambiguator to default - nickname" - -References: bsc#1197528 bsc#1197150 - -To improve testsuit, these patches still need more testing. -This reverts commit 72b17fe715056c96ea73f187ab46721788b3a782. - -Signed-off-by: Li Zhang ---- - python/qemu/machine/machine.py | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py -index ad529fd92a6022150fd0156d005d..ea9e07805df10a57115dace06885 100644 ---- a/python/qemu/machine/machine.py -+++ b/python/qemu/machine/machine.py -@@ -133,7 +133,7 @@ class QEMUMachine: - self._wrapper = wrapper - self._qmp_timer = qmp_timer - -- self._name = name or f"qemu-{os.getpid()}-{id(self):02x}" -+ self._name = name or "qemu-%d" % os.getpid() - self._temp_dir: Optional[str] = None - self._base_temp_dir = base_temp_dir - self._sock_dir = sock_dir diff --git a/Revert-python-machine-add-sock_dir-prope.patch b/Revert-python-machine-add-sock_dir-prope.patch deleted file mode 100644 index dd7ddd98..00000000 --- a/Revert-python-machine-add-sock_dir-prope.patch +++ /dev/null @@ -1,70 +0,0 @@ -From: Li Zhang -Date: Tue, 29 Mar 2022 12:02:45 +0200 -Subject: Revert "python/machine: add @sock_dir property" - -References: bsc#1197528 bsc#1197150 - -To improve testsuit, these patches still need more testing. -This reverts commit 87bf1fe5cbffefe6b7ee13a7015ae285250ad2db. - -Signed-off-by: Li Zhang ---- - python/qemu/machine/machine.py | 17 ++++------------- - 1 file changed, 4 insertions(+), 13 deletions(-) - -diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py -index b1dd77b53885629eba452cdd1bc2..a487c397459a1fa6889276ab2538 100644 ---- a/python/qemu/machine/machine.py -+++ b/python/qemu/machine/machine.py -@@ -134,9 +134,8 @@ class QEMUMachine: - self._qmp_timer = qmp_timer - - self._name = name or "qemu-%d" % os.getpid() -- self._temp_dir: Optional[str] = None - self._base_temp_dir = base_temp_dir -- self._sock_dir = sock_dir -+ self._sock_dir = sock_dir or self._base_temp_dir - self._log_dir = log_dir - - if monitor_address is not None: -@@ -144,7 +143,7 @@ class QEMUMachine: - self._remove_monitor_sockfile = False - else: - self._monitor_address = os.path.join( -- self.sock_dir, f"{self._name}-monitor.sock" -+ self._sock_dir, f"{self._name}-monitor.sock" - ) - self._remove_monitor_sockfile = True - -@@ -164,13 +163,14 @@ class QEMUMachine: - self._qmp_set = True # Enable QMP monitor by default. - self._qmp_connection: Optional[QEMUMonitorProtocol] = None - self._qemu_full_args: Tuple[str, ...] = () -+ self._temp_dir: Optional[str] = None - self._launched = False - self._machine: Optional[str] = None - self._console_index = 0 - self._console_set = False - self._console_device_type: Optional[str] = None - self._console_address = os.path.join( -- self.sock_dir, f"{self._name}-console.sock" -+ self._sock_dir, f"{self._name}-console.sock" - ) - self._console_socket: Optional[socket.socket] = None - self._remove_files: List[str] = [] -@@ -816,15 +816,6 @@ class QEMUMachine: - dir=self._base_temp_dir) - return self._temp_dir - -- @property -- def sock_dir(self) -> str: -- """ -- Returns the directory used for sockfiles by this machine. -- """ -- if self._sock_dir: -- return self._sock_dir -- return self.temp_dir -- - @property - def log_dir(self) -> str: - """ diff --git a/Revert-python-machine-handle-fast-QEMU-t.patch b/Revert-python-machine-handle-fast-QEMU-t.patch deleted file mode 100644 index 15e84ea6..00000000 --- a/Revert-python-machine-handle-fast-QEMU-t.patch +++ /dev/null @@ -1,66 +0,0 @@ -From: Li Zhang -Date: Tue, 29 Mar 2022 11:51:54 +0200 -Subject: Revert "python/machine: handle "fast" QEMU terminations" - -References: bsc#1197528 bsc#1197150 - -This patch causes iotest failures, it needs to revert. -This reverts commit 1611e6cf4e7163f6102b37010a8b7e7120f468b5. - -Signed-off-by: Li Zhang ---- - python/qemu/machine/machine.py | 19 +++++++------------ - 1 file changed, 7 insertions(+), 12 deletions(-) - -diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py -index 67ab06ca2b6daa531b7c0ad9f7c2..f92e73de4010d10c9e062259c706 100644 ---- a/python/qemu/machine/machine.py -+++ b/python/qemu/machine/machine.py -@@ -349,6 +349,9 @@ class QEMUMachine: - Called to cleanup the VM instance after the process has exited. - May also be called after a failed launch. - """ -+ # Comprehensive reset for the failed launch case: -+ self._early_cleanup() -+ - try: - self._close_qmp_connection() - except Exception as err: # pylint: disable=broad-except -@@ -397,16 +400,9 @@ class QEMUMachine: - - try: - self._launch() -+ self._launched = True - except: -- # We may have launched the process but it may -- # have exited before we could connect via QMP. -- # Assume the VM didn't launch or is exiting. -- # If we don't wait for the process, exitcode() may still be -- # 'None' by the time control is ceded back to the caller. -- if self._launched: -- self.wait() -- else: -- self._post_shutdown() -+ self._post_shutdown() - - LOG.debug('Error launching VM') - if self._qemu_full_args: -@@ -430,7 +426,6 @@ class QEMUMachine: - stderr=subprocess.STDOUT, - shell=False, - close_fds=False) -- self._launched = True - self._post_launch() - - def _close_qmp_connection(self) -> None: -@@ -462,8 +457,8 @@ class QEMUMachine: - """ - Perform any cleanup that needs to happen before the VM exits. - -- This method may be called twice upon shutdown, once each by soft -- and hard shutdown in failover scenarios. -+ May be invoked by both soft and hard shutdown in failover scenarios. -+ Called additionally by _post_shutdown for comprehensive cleanup. - """ - # If we keep the console socket open, we may deadlock waiting - # for QEMU to exit, while QEMU is waiting for the socket to diff --git a/Revert-python-machine-move-more-variable.patch b/Revert-python-machine-move-more-variable.patch deleted file mode 100644 index 09504a4b..00000000 --- a/Revert-python-machine-move-more-variable.patch +++ /dev/null @@ -1,56 +0,0 @@ -From: Li Zhang -Date: Tue, 29 Mar 2022 11:57:11 +0200 -Subject: Revert "python/machine: move more variable initializations to - _pre_launch" - -References: bsc#1197528 bsc#1197150 - -To improve testsuit, these patches still need more testing. -This reverts commit b1ca99199320fcc010f407b84ac00d96e7e4baa1. - -Signed-off-by: Li Zhang ---- - python/qemu/machine/machine.py | 16 ++++++++-------- - 1 file changed, 8 insertions(+), 8 deletions(-) - -diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py -index f92e73de4010d10c9e062259c706..ad529fd92a6022150fd0156d005d 100644 ---- a/python/qemu/machine/machine.py -+++ b/python/qemu/machine/machine.py -@@ -327,14 +327,6 @@ class QEMUMachine: - self._qemu_log_path = os.path.join(self.log_dir, self._name + ".log") - self._qemu_log_file = open(self._qemu_log_path, 'wb') - -- self._iolog = None -- self._qemu_full_args = tuple(chain( -- self._wrapper, -- [self._binary], -- self._base_args, -- self._args -- )) -- - def _post_launch(self) -> None: - if self._qmp_connection: - self._qmp.accept(self._qmp_timer) -@@ -398,6 +390,8 @@ class QEMUMachine: - if self._launched: - raise QEMUMachineError('VM already launched') - -+ self._iolog = None -+ self._qemu_full_args = () - try: - self._launch() - self._launched = True -@@ -416,6 +410,12 @@ class QEMUMachine: - Launch the VM and establish a QMP connection - """ - self._pre_launch() -+ self._qemu_full_args = tuple( -+ chain(self._wrapper, -+ [self._binary], -+ self._base_args, -+ self._args) -+ ) - LOG.debug('VM launch command: %r', ' '.join(self._qemu_full_args)) - - # Cleaning up of this subprocess is guaranteed by _do_shutdown. diff --git a/Revert-python-machine-remove-_remove_mon.patch b/Revert-python-machine-remove-_remove_mon.patch deleted file mode 100644 index fe749df9..00000000 --- a/Revert-python-machine-remove-_remove_mon.patch +++ /dev/null @@ -1,41 +0,0 @@ -From: Li Zhang -Date: Tue, 29 Mar 2022 12:01:34 +0200 -Subject: Revert "python/machine: remove _remove_monitor_sockfile property" - -References: bsc#1197528 bsc#1197150 - -To improve testsuit, these patches still need more testing. -This reverts commit 6eeb3de7e1aff91ce6e092a39f85946d12664385. - -Signed-off-by: Li Zhang ---- - python/qemu/machine/machine.py | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py -index ea9e07805df10a57115dace06885..b1dd77b53885629eba452cdd1bc2 100644 ---- a/python/qemu/machine/machine.py -+++ b/python/qemu/machine/machine.py -@@ -141,10 +141,12 @@ class QEMUMachine: - - if monitor_address is not None: - self._monitor_address = monitor_address -+ self._remove_monitor_sockfile = False - else: - self._monitor_address = os.path.join( - self.sock_dir, f"{self._name}-monitor.sock" - ) -+ self._remove_monitor_sockfile = True - - self._console_log_path = console_log - if self._console_log_path: -@@ -313,7 +315,8 @@ class QEMUMachine: - self._remove_files.append(self._console_address) - - if self._qmp_set: -- if isinstance(self._monitor_address, str): -+ if self._remove_monitor_sockfile: -+ assert isinstance(self._monitor_address, str) - self._remove_files.append(self._monitor_address) - self._qmp_connection = QEMUMonitorProtocol( - self._monitor_address, diff --git a/bundles.tar.xz b/bundles.tar.xz index 21cf82db..23016b2f 100644 --- a/bundles.tar.xz +++ b/bundles.tar.xz @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b5a6e9d7d9c66f8f445153de1b4665c8abede132da0dae6c4231fac0cba49dc -size 98764 +oid sha256:0ecf0e91f78b91cabf0df72e0dd9c54c9d2d016e581abd8364eea6cc6103df94 +size 135948 diff --git a/numa-Support-SGX-numa-in-the-monitor-and.patch b/numa-Support-SGX-numa-in-the-monitor-and.patch index 02783741..efea423b 100644 --- a/numa-Support-SGX-numa-in-the-monitor-and.patch +++ b/numa-Support-SGX-numa-in-the-monitor-and.patch @@ -33,7 +33,6 @@ The QMP interface show: Signed-off-by: Yang Zhong Message-Id: <20211101162009.62161-4-yang.zhong@intel.com> Signed-off-by: Paolo Bonzini -(cherry picked from commit 4755927ae12547c2e7cb22c5fa1b39038c6c11b1) Signed-off-by: Li Zhang --- hw/i386/sgx.c | 51 +++++++++++++++++++++++++++++++++++-------- diff --git a/python-aqmp-Fix-negotiation-with-pre-oob.patch b/python-aqmp-Fix-negotiation-with-pre-oob.patch new file mode 100644 index 00000000..ca589640 --- /dev/null +++ b/python-aqmp-Fix-negotiation-with-pre-oob.patch @@ -0,0 +1,36 @@ +From: John Snow +Date: Mon, 31 Jan 2022 23:11:31 -0500 +Subject: python/aqmp: Fix negotiation with pre-"oob" QEMU + +Git-commit: fa73e6e4ca1a93c5bbf9d05fb2a25736ab810b35 + +QEMU versions prior to the "oob" capability *also* can't accept the +"enable" keyword argument at all. Fix the handshake process with older +QEMU versions. + +Signed-off-by: John Snow +Reviewed-by: Hanna Reitz +Reviewed-by: Kevin Wolf +Message-id: 20220201041134.1237016-2-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/qmp_client.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/python/qemu/aqmp/qmp_client.py b/python/qemu/aqmp/qmp_client.py +index 8105e29fa8f04297ec9390ec25ea..6b43e1dbbe38eded19fd0115e8bc 100644 +--- a/python/qemu/aqmp/qmp_client.py ++++ b/python/qemu/aqmp/qmp_client.py +@@ -292,9 +292,9 @@ class QMPClient(AsyncProtocol[Message], Events): + """ + self.logger.debug("Negotiating capabilities ...") + +- arguments: Dict[str, List[str]] = {'enable': []} ++ arguments: Dict[str, List[str]] = {} + if self._greeting and 'oob' in self._greeting.QMP.capabilities: +- arguments['enable'].append('oob') ++ arguments.setdefault('enable', []).append('oob') + msg = self.make_execute_msg('qmp_capabilities', arguments=arguments) + + # It's not safe to use execute() here, because the reader/writers diff --git a/python-aqmp-add-SocketAddrT-to-package-r.patch b/python-aqmp-add-SocketAddrT-to-package-r.patch new file mode 100644 index 00000000..c6368c41 --- /dev/null +++ b/python-aqmp-add-SocketAddrT-to-package-r.patch @@ -0,0 +1,42 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:48 -0500 +Subject: python/aqmp: add SocketAddrT to package root + +Git-commit: 728dcac5e356ce5b948943f21c0c72a1b2d96122 + +It's a commonly needed definition, it can be re-exported by the root. + +Signed-off-by: John Snow +Reviewed-by: Vladimir Sementsov-Ogievskiy +Reviewed-by: Beraldo Leal +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/__init__.py | 10 +++++++++- + 1 file changed, 9 insertions(+), 1 deletion(-) + +diff --git a/python/qemu/aqmp/__init__.py b/python/qemu/aqmp/__init__.py +index 880d5b6fa7f2c966542b12e25571..c6fa2dda58fdf4f1a867e677eadb 100644 +--- a/python/qemu/aqmp/__init__.py ++++ b/python/qemu/aqmp/__init__.py +@@ -26,7 +26,12 @@ import logging + from .error import AQMPError + from .events import EventListener + from .message import Message +-from .protocol import ConnectError, Runstate, StateError ++from .protocol import ( ++ ConnectError, ++ Runstate, ++ SocketAddrT, ++ StateError, ++) + from .qmp_client import ExecInterruptedError, ExecuteError, QMPClient + + +@@ -48,4 +53,7 @@ __all__ = ( + 'ConnectError', + 'ExecuteError', + 'ExecInterruptedError', ++ ++ # Type aliases ++ 'SocketAddrT', + ) diff --git a/python-aqmp-add-__del__-method-to-legacy.patch b/python-aqmp-add-__del__-method-to-legacy.patch new file mode 100644 index 00000000..1c276329 --- /dev/null +++ b/python-aqmp-add-__del__-method-to-legacy.patch @@ -0,0 +1,64 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:45 -0500 +Subject: python/aqmp: add __del__ method to legacy interface + +Git-commit: 3bc72e3aed76e0326703db81964b13f1da075cbf + +asyncio can complain *very* loudly if you forget to back out of things +gracefully before the garbage collector starts destroying objects that +contain live references to asyncio Tasks. + +The usual fix is just to remember to call aqmp.disconnect(), but for the +sake of the legacy wrapper and quick, one-off scripts where a graceful +shutdown is not necessarily of paramount imporance, add a courtesy +cleanup that will trigger prior to seeing screenfuls of confusing +asyncio tracebacks. + +Note that we can't *always* save you from yourself; depending on when +the GC runs, you might just seriously be out of luck. The best we can do +in this case is to gently remind you to clean up after yourself. + +(Still much better than multiple pages of incomprehensible python +warnings for the crime of forgetting to put your toys away.) + +Signed-off-by: John Snow +Reviewed-by: Vladimir Sementsov-Ogievskiy +Reviewed-by: Beraldo Leal +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/legacy.py | 18 ++++++++++++++++++ + 1 file changed, 18 insertions(+) + +diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py +index 9e7b9fb80b95ad5d442fea0dbbab..2ccb136b02c1fcf586507400c056 100644 +--- a/python/qemu/aqmp/legacy.py ++++ b/python/qemu/aqmp/legacy.py +@@ -16,6 +16,8 @@ from typing import ( + import qemu.qmp + from qemu.qmp import QMPMessage, QMPReturnValue, SocketAddrT + ++from .error import AQMPError ++from .protocol import Runstate + from .qmp_client import QMPClient + + +@@ -136,3 +138,19 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): + + def send_fd_scm(self, fd: int) -> None: + self._aqmp.send_fd_scm(fd) ++ ++ def __del__(self) -> None: ++ if self._aqmp.runstate == Runstate.IDLE: ++ return ++ ++ if not self._aloop.is_running(): ++ self.close() ++ else: ++ # Garbage collection ran while the event loop was running. ++ # Nothing we can do about it now, but if we don't raise our ++ # own error, the user will be treated to a lot of traceback ++ # they might not understand. ++ raise AQMPError( ++ "QEMUMonitorProtocol.close()" ++ " was not called before object was garbage collected" ++ ) diff --git a/python-aqmp-add-_session_guard.patch b/python-aqmp-add-_session_guard.patch new file mode 100644 index 00000000..f6bb696f --- /dev/null +++ b/python-aqmp-add-_session_guard.patch @@ -0,0 +1,140 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:39 -0500 +Subject: python/aqmp: add _session_guard() +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 40196c23939758abc5300e85333e676196e3ba6d + +In _new_session, there's a fairly complex except clause that's used to +give semantic errors to callers of accept() and connect(). We need to +create a new two-step replacement for accept(), so factoring out this +piece of logic will be useful. + +Bolster the comments and docstring here to try and demystify what's +going on in this fairly delicate piece of Python magic. + +(If we were using Python 3.7+, this would be an @asynccontextmanager. We +don't have that very nice piece of magic, however, so this must take an +Awaitable to manage the Exception contexts properly. We pay the price +for platform compatibility.) + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-2-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/protocol.py | 89 +++++++++++++++++++++++++----------- + 1 file changed, 62 insertions(+), 27 deletions(-) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 33358f5cd72b61bd060b8dea6091..009883f64d011e44dd003e9dcde3 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -317,6 +317,62 @@ class AsyncProtocol(Generic[T]): + # Section: Session machinery + # -------------------------- + ++ async def _session_guard(self, coro: Awaitable[None], emsg: str) -> None: ++ """ ++ Async guard function used to roll back to `IDLE` on any error. ++ ++ On any Exception, the state machine will be reset back to ++ `IDLE`. Most Exceptions will be wrapped with `ConnectError`, but ++ `BaseException` events will be left alone (This includes ++ asyncio.CancelledError, even prior to Python 3.8). ++ ++ :param error_message: ++ Human-readable string describing what connection phase failed. ++ ++ :raise BaseException: ++ When `BaseException` occurs in the guarded block. ++ :raise ConnectError: ++ When any other error is encountered in the guarded block. ++ """ ++ # Note: After Python 3.6 support is removed, this should be an ++ # @asynccontextmanager instead of accepting a callback. ++ try: ++ await coro ++ except BaseException as err: ++ self.logger.error("%s: %s", emsg, exception_summary(err)) ++ self.logger.debug("%s:\n%s\n", emsg, pretty_traceback()) ++ try: ++ # Reset the runstate back to IDLE. ++ await self.disconnect() ++ except: ++ # We don't expect any Exceptions from the disconnect function ++ # here, because we failed to connect in the first place. ++ # The disconnect() function is intended to perform ++ # only cannot-fail cleanup here, but you never know. ++ emsg = ( ++ "Unexpected bottom half exception. " ++ "This is a bug in the QMP library. " ++ "Please report it to and " ++ "CC: John Snow ." ++ ) ++ self.logger.critical("%s:\n%s\n", emsg, pretty_traceback()) ++ raise ++ ++ # CancelledError is an Exception with special semantic meaning; ++ # We do NOT want to wrap it up under ConnectError. ++ # NB: CancelledError is not a BaseException before Python 3.8 ++ if isinstance(err, asyncio.CancelledError): ++ raise ++ ++ # Any other kind of error can be treated as some kind of connection ++ # failure broadly. Inspect the 'exc' field to explore the root ++ # cause in greater detail. ++ if isinstance(err, Exception): ++ raise ConnectError(emsg, err) from err ++ ++ # Raise BaseExceptions un-wrapped, they're more important. ++ raise ++ + @property + def _runstate_event(self) -> asyncio.Event: + # asyncio.Event() objects should not be created prior to entrance into +@@ -371,34 +427,13 @@ class AsyncProtocol(Generic[T]): + """ + assert self.runstate == Runstate.IDLE + +- try: +- phase = "connection" +- await self._establish_connection(address, ssl, accept) +- +- phase = "session" +- await self._establish_session() ++ await self._session_guard( ++ self._establish_connection(address, ssl, accept), ++ 'Failed to establish connection') + +- except BaseException as err: +- emsg = f"Failed to establish {phase}" +- self.logger.error("%s: %s", emsg, exception_summary(err)) +- self.logger.debug("%s:\n%s\n", emsg, pretty_traceback()) +- try: +- # Reset from CONNECTING back to IDLE. +- await self.disconnect() +- except: +- emsg = "Unexpected bottom half exception" +- self.logger.critical("%s:\n%s\n", emsg, pretty_traceback()) +- raise +- +- # NB: CancelledError is not a BaseException before Python 3.8 +- if isinstance(err, asyncio.CancelledError): +- raise +- +- if isinstance(err, Exception): +- raise ConnectError(emsg, err) from err +- +- # Raise BaseExceptions un-wrapped, they're more important. +- raise ++ await self._session_guard( ++ self._establish_session(), ++ 'Failed to establish session') + + assert self.runstate == Runstate.RUNNING + diff --git a/python-aqmp-add-socket-bind-step-to-lega.patch b/python-aqmp-add-socket-bind-step-to-lega.patch new file mode 100644 index 00000000..c0a6b792 --- /dev/null +++ b/python-aqmp-add-socket-bind-step-to-lega.patch @@ -0,0 +1,135 @@ +From: John Snow +Date: Mon, 31 Jan 2022 23:11:34 -0500 +Subject: python/aqmp: add socket bind step to legacy.py + +Git-commit: b0b662bb2b340d63529672b5bdae596a6243c4d0 + +The synchronous QMP library would bind to the server address during +__init__(). The new library delays this to the accept() call, because +binding occurs inside of the call to start_[unix_]server(), which is an +async method -- so it cannot happen during __init__ anymore. + +Python 3.7+ adds the ability to create the server (and thus the bind() +call) and begin the active listening in separate steps, but we don't +have that functionality in 3.6, our current minimum. + +Therefore ... Add a temporary workaround that allows the synchronous +version of the client to bind the socket in advance, guaranteeing that +there will be a UNIX socket in the filesystem ready for the QEMU client +to connect to without a race condition. + +(Yes, it's a bit ugly. Fixing it more nicely will have to wait until our +minimum Python version is 3.7+.) + +Signed-off-by: John Snow +Reviewed-by: Kevin Wolf +Message-id: 20220201041134.1237016-5-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/legacy.py | 3 +++ + python/qemu/aqmp/protocol.py | 41 +++++++++++++++++++++++++++++++++--- + 2 files changed, 41 insertions(+), 3 deletions(-) + +diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py +index 0890f95b16875ecb815ed4560bc7..6baa5f3409a6b459c67097d3c2a0 100644 +--- a/python/qemu/aqmp/legacy.py ++++ b/python/qemu/aqmp/legacy.py +@@ -56,6 +56,9 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): + self._address = address + self._timeout: Optional[float] = None + ++ if server: ++ self._aqmp._bind_hack(address) # pylint: disable=protected-access ++ + _T = TypeVar('_T') + + def _sync( +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 50e973c2f2dc9c5fa759380ab3e9..33358f5cd72b61bd060b8dea6091 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -15,6 +15,7 @@ from asyncio import StreamReader, StreamWriter + from enum import Enum + from functools import wraps + import logging ++import socket + from ssl import SSLContext + from typing import ( + Any, +@@ -238,6 +239,9 @@ class AsyncProtocol(Generic[T]): + self._runstate = Runstate.IDLE + self._runstate_changed: Optional[asyncio.Event] = None + ++ # Workaround for bind() ++ self._sock: Optional[socket.socket] = None ++ + def __repr__(self) -> str: + cls_name = type(self).__name__ + tokens = [] +@@ -427,6 +431,34 @@ class AsyncProtocol(Generic[T]): + else: + await self._do_connect(address, ssl) + ++ def _bind_hack(self, address: Union[str, Tuple[str, int]]) -> None: ++ """ ++ Used to create a socket in advance of accept(). ++ ++ This is a workaround to ensure that we can guarantee timing of ++ precisely when a socket exists to avoid a connection attempt ++ bouncing off of nothing. ++ ++ Python 3.7+ adds a feature to separate the server creation and ++ listening phases instead, and should be used instead of this ++ hack. ++ """ ++ if isinstance(address, tuple): ++ family = socket.AF_INET ++ else: ++ family = socket.AF_UNIX ++ ++ sock = socket.socket(family, socket.SOCK_STREAM) ++ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) ++ ++ try: ++ sock.bind(address) ++ except: ++ sock.close() ++ raise ++ ++ self._sock = sock ++ + @upper_half + async def _do_accept(self, address: SocketAddrT, + ssl: Optional[SSLContext] = None) -> None: +@@ -464,24 +496,27 @@ class AsyncProtocol(Generic[T]): + if isinstance(address, tuple): + coro = asyncio.start_server( + _client_connected_cb, +- host=address[0], +- port=address[1], ++ host=None if self._sock else address[0], ++ port=None if self._sock else address[1], + ssl=ssl, + backlog=1, + limit=self._limit, ++ sock=self._sock, + ) + else: + coro = asyncio.start_unix_server( + _client_connected_cb, +- path=address, ++ path=None if self._sock else address, + ssl=ssl, + backlog=1, + limit=self._limit, ++ sock=self._sock, + ) + + server = await coro # Starts listening + await connected.wait() # Waits for the callback to fire (and finish) + assert server is None ++ self._sock = None + + self.logger.debug("Connection accepted.") + diff --git a/python-aqmp-add-start_server-and-accept-.patch b/python-aqmp-add-start_server-and-accept-.patch new file mode 100644 index 00000000..ca98d7b2 --- /dev/null +++ b/python-aqmp-add-start_server-and-accept-.patch @@ -0,0 +1,158 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:46 -0500 +Subject: python/aqmp: add start_server() and accept() methods +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 481607c7d35de2bc4d9bec7f4734036fc467f330 + +Add start_server() and accept() methods that can be used instead of +start_server_and_accept() to allow more fine-grained control over the +incoming connection process. + +(Eagle-eyed reviewers will surely notice that it's a bit weird that +"CONNECTING" is a state that's shared between both the start_server() +and connect() states. That's absolutely true, and it's very true that +checking on the presence of _accepted as an indicator of state is a +hack. That's also very certainly true. But ... this keeps client code an +awful lot simpler, as it doesn't have to care exactly *how* the +connection is being made, just that it *is*. Is it worth disrupting that +simplicity in order to provide a better state guard on `accept()`? Hm.) + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-9-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/protocol.py | 67 +++++++++++++++++++++++++++++++++--- + python/tests/protocol.py | 7 ++++ + 2 files changed, 69 insertions(+), 5 deletions(-) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index cdbc9cba0d15dea19cc1c60ca3c3..2ecba1455571a35e0e6c565e3641 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -280,6 +280,8 @@ class AsyncProtocol(Generic[T]): + Accept a connection and begin processing message queues. + + If this call fails, `runstate` is guaranteed to be set back to `IDLE`. ++ This method is precisely equivalent to calling `start_server()` ++ followed by `accept()`. + + :param address: + Address to listen on; UNIX socket path or TCP address/port. +@@ -294,9 +296,62 @@ class AsyncProtocol(Generic[T]): + protocol-level failure occurs while establishing a new + session, the wrapped error may also be an `QMPError`. + """ ++ await self.start_server(address, ssl) ++ await self.accept() ++ assert self.runstate == Runstate.RUNNING ++ ++ @upper_half ++ @require(Runstate.IDLE) ++ async def start_server(self, address: SocketAddrT, ++ ssl: Optional[SSLContext] = None) -> None: ++ """ ++ Start listening for an incoming connection, but do not wait for a peer. ++ ++ This method starts listening for an incoming connection, but ++ does not block waiting for a peer. This call will return ++ immediately after binding and listening on a socket. A later ++ call to `accept()` must be made in order to finalize the ++ incoming connection. ++ ++ :param address: ++ Address to listen on; UNIX socket path or TCP address/port. ++ :param ssl: SSL context to use, if any. ++ ++ :raise StateError: When the `Runstate` is not `IDLE`. ++ :raise ConnectError: ++ When the server could not start listening on this address. ++ ++ This exception will wrap a more concrete one. In most cases, ++ the wrapped exception will be `OSError`. ++ """ + await self._session_guard( + self._do_start_server(address, ssl), + 'Failed to establish connection') ++ assert self.runstate == Runstate.CONNECTING ++ ++ @upper_half ++ @require(Runstate.CONNECTING) ++ async def accept(self) -> None: ++ """ ++ Accept an incoming connection and begin processing message queues. ++ ++ If this call fails, `runstate` is guaranteed to be set back to `IDLE`. ++ ++ :raise StateError: When the `Runstate` is not `CONNECTING`. ++ :raise QMPError: When `start_server()` was not called yet. ++ :raise ConnectError: ++ When a connection or session cannot be established. ++ ++ This exception will wrap a more concrete one. In most cases, ++ the wrapped exception will be `OSError` or `EOFError`. If a ++ protocol-level failure occurs while establishing a new ++ session, the wrapped error may also be an `QMPError`. ++ """ ++ if self._accepted is None: ++ raise QMPError("Cannot call accept() before start_server().") ++ await self._session_guard( ++ self._do_accept(), ++ 'Failed to establish connection') + await self._session_guard( + self._establish_session(), + 'Failed to establish session') +@@ -512,7 +567,12 @@ class AsyncProtocol(Generic[T]): + async def _do_start_server(self, address: SocketAddrT, + ssl: Optional[SSLContext] = None) -> None: + """ +- Acting as the transport server, accept a single connection. ++ Start listening for an incoming connection, but do not wait for a peer. ++ ++ This method starts listening for an incoming connection, but does not ++ block waiting for a peer. This call will return immediately after ++ binding and listening to a socket. A later call to accept() must be ++ made in order to finalize the incoming connection. + + :param address: + Address to listen on; UNIX socket path or TCP address/port. +@@ -554,10 +614,7 @@ class AsyncProtocol(Generic[T]): + # This will start the server (bind(2), listen(2)). It will also + # call accept(2) if we yield, but we don't block on that here. + self._server = await coro +- +- # Just for this one commit, wait for a peer. +- # This gets split out in the next patch. +- await self._do_accept() ++ self.logger.debug("Server listening on %s", address) + + @upper_half + async def _do_accept(self) -> None: +diff --git a/python/tests/protocol.py b/python/tests/protocol.py +index 5e442e1efbd19bf95a95de371060..d6849ad3062081c62b29bf89dd2a 100644 +--- a/python/tests/protocol.py ++++ b/python/tests/protocol.py +@@ -43,11 +43,18 @@ class NullProtocol(AsyncProtocol[None]): + + async def _do_start_server(self, address, ssl=None): + if self.fake_session: ++ self._accepted = asyncio.Event() + self._set_state(Runstate.CONNECTING) + await asyncio.sleep(0) + else: + await super()._do_start_server(address, ssl) + ++ async def _do_accept(self): ++ if self.fake_session: ++ self._accepted = None ++ else: ++ await super()._do_accept() ++ + async def _do_connect(self, address, ssl=None): + if self.fake_session: + self._set_state(Runstate.CONNECTING) diff --git a/python-aqmp-copy-type-definitions-from-q.patch b/python-aqmp-copy-type-definitions-from-q.patch new file mode 100644 index 00000000..f4e837f1 --- /dev/null +++ b/python-aqmp-copy-type-definitions-from-q.patch @@ -0,0 +1,134 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:47 -0500 +Subject: python/aqmp: copy type definitions from qmp + +Git-commit: 0e6bfd8b96e407db7b0cb5e8c14cc315a7154f53 + +Copy the remaining type definitions from QMP into the qemu.aqmp.legacy +module. Now, users that require the legacy interface don't need to +import anything else but qemu.aqmp.legacy wrapper. + +Signed-off-by: John Snow +Reviewed-by: Vladimir Sementsov-Ogievskiy +Reviewed-by: Beraldo Leal +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/legacy.py | 22 ++++++++++++++++++++-- + python/qemu/aqmp/protocol.py | 16 ++++++++++------ + 2 files changed, 30 insertions(+), 8 deletions(-) + +diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py +index 2ccb136b02c1fcf586507400c056..9431fe933019b1e1c221ea3ab7bb 100644 +--- a/python/qemu/aqmp/legacy.py ++++ b/python/qemu/aqmp/legacy.py +@@ -6,7 +6,9 @@ This class pretends to be qemu.qmp.QEMUMonitorProtocol. + + import asyncio + from typing import ( ++ Any, + Awaitable, ++ Dict, + List, + Optional, + TypeVar, +@@ -14,13 +16,29 @@ from typing import ( + ) + + import qemu.qmp +-from qemu.qmp import QMPMessage, QMPReturnValue, SocketAddrT + + from .error import AQMPError +-from .protocol import Runstate ++from .protocol import Runstate, SocketAddrT + from .qmp_client import QMPClient + + ++#: QMPMessage is an entire QMP message of any kind. ++QMPMessage = Dict[str, Any] ++ ++#: QMPReturnValue is the 'return' value of a command. ++QMPReturnValue = object ++ ++#: QMPObject is any object in a QMP message. ++QMPObject = Dict[str, object] ++ ++# QMPMessage can be outgoing commands or incoming events/returns. ++# QMPReturnValue is usually a dict/json object, but due to QAPI's ++# 'returns-whitelist', it can actually be anything. ++# ++# {'return': {}} is a QMPMessage, ++# {} is the QMPReturnValue. ++ ++ + # pylint: disable=missing-docstring + + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index c4fbe35a0e41c589059ec4fa37a8..5b4f2f0d0a81a0d2902358e9b799 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -46,6 +46,10 @@ T = TypeVar('T') + _U = TypeVar('_U') + _TaskFN = Callable[[], Awaitable[None]] # aka ``async def func() -> None`` + ++InternetAddrT = Tuple[str, int] ++UnixAddrT = str ++SocketAddrT = Union[UnixAddrT, InternetAddrT] ++ + + class Runstate(Enum): + """Protocol session runstate.""" +@@ -257,7 +261,7 @@ class AsyncProtocol(Generic[T]): + + @upper_half + @require(Runstate.IDLE) +- async def accept(self, address: Union[str, Tuple[str, int]], ++ async def accept(self, address: SocketAddrT, + ssl: Optional[SSLContext] = None) -> None: + """ + Accept a connection and begin processing message queues. +@@ -275,7 +279,7 @@ class AsyncProtocol(Generic[T]): + + @upper_half + @require(Runstate.IDLE) +- async def connect(self, address: Union[str, Tuple[str, int]], ++ async def connect(self, address: SocketAddrT, + ssl: Optional[SSLContext] = None) -> None: + """ + Connect to the server and begin processing message queues. +@@ -337,7 +341,7 @@ class AsyncProtocol(Generic[T]): + + @upper_half + async def _new_session(self, +- address: Union[str, Tuple[str, int]], ++ address: SocketAddrT, + ssl: Optional[SSLContext] = None, + accept: bool = False) -> None: + """ +@@ -397,7 +401,7 @@ class AsyncProtocol(Generic[T]): + @upper_half + async def _establish_connection( + self, +- address: Union[str, Tuple[str, int]], ++ address: SocketAddrT, + ssl: Optional[SSLContext] = None, + accept: bool = False + ) -> None: +@@ -424,7 +428,7 @@ class AsyncProtocol(Generic[T]): + await self._do_connect(address, ssl) + + @upper_half +- async def _do_accept(self, address: Union[str, Tuple[str, int]], ++ async def _do_accept(self, address: SocketAddrT, + ssl: Optional[SSLContext] = None) -> None: + """ + Acting as the transport server, accept a single connection. +@@ -482,7 +486,7 @@ class AsyncProtocol(Generic[T]): + self.logger.debug("Connection accepted.") + + @upper_half +- async def _do_connect(self, address: Union[str, Tuple[str, int]], ++ async def _do_connect(self, address: SocketAddrT, + ssl: Optional[SSLContext] = None) -> None: + """ + Acting as the transport client, initiate a connection to a server. diff --git a/python-aqmp-drop-_bind_hack.patch b/python-aqmp-drop-_bind_hack.patch new file mode 100644 index 00000000..b0414c02 --- /dev/null +++ b/python-aqmp-drop-_bind_hack.patch @@ -0,0 +1,132 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:48 -0500 +Subject: python/aqmp: drop _bind_hack() +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 4c1fe7003c9b373acb0791b4356e2285a10365c0 + +_bind_hack() was a quick fix to allow async QMP to call bind(2) prior to +calling listen(2) and accept(2). This wasn't sufficient to fully address +the race condition present in synchronous clients. + +With the race condition in legacy.py fixed (see the previous commit), +there are no longer any users of _bind_hack(). Drop it. + +Fixes: b0b662bb2b3 +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-11-jsnow@redhat.com +[Expanded commit message. --js] +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/legacy.py | 2 +- + python/qemu/aqmp/protocol.py | 41 +++--------------------------------- + 2 files changed, 4 insertions(+), 39 deletions(-) + +diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py +index cb50e60564823fdc0aeeb194c5e3..46026e9fdc6c8859a8c944076c71 100644 +--- a/python/qemu/aqmp/legacy.py ++++ b/python/qemu/aqmp/legacy.py +@@ -57,7 +57,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): + self._timeout: Optional[float] = None + + if server: +- self._sync(self._aqmp.start_server(address)) ++ self._sync(self._aqmp.start_server(self._address)) + + _T = TypeVar('_T') + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 2ecba1455571a35e0e6c565e3641..36fae57f277826d5cd4026e0d079 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -18,7 +18,6 @@ from asyncio import StreamReader, StreamWriter + from enum import Enum + from functools import wraps + import logging +-import socket + from ssl import SSLContext + from typing import ( + Any, +@@ -242,9 +241,6 @@ class AsyncProtocol(Generic[T]): + self._runstate = Runstate.IDLE + self._runstate_changed: Optional[asyncio.Event] = None + +- # Workaround for bind() +- self._sock: Optional[socket.socket] = None +- + # Server state for start_server() and _incoming() + self._server: Optional[asyncio.AbstractServer] = None + self._accepted: Optional[asyncio.Event] = None +@@ -535,34 +531,6 @@ class AsyncProtocol(Generic[T]): + self._reader, self._writer = (reader, writer) + self._accepted.set() + +- def _bind_hack(self, address: Union[str, Tuple[str, int]]) -> None: +- """ +- Used to create a socket in advance of accept(). +- +- This is a workaround to ensure that we can guarantee timing of +- precisely when a socket exists to avoid a connection attempt +- bouncing off of nothing. +- +- Python 3.7+ adds a feature to separate the server creation and +- listening phases instead, and should be used instead of this +- hack. +- """ +- if isinstance(address, tuple): +- family = socket.AF_INET +- else: +- family = socket.AF_UNIX +- +- sock = socket.socket(family, socket.SOCK_STREAM) +- sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +- +- try: +- sock.bind(address) +- except: +- sock.close() +- raise +- +- self._sock = sock +- + @upper_half + async def _do_start_server(self, address: SocketAddrT, + ssl: Optional[SSLContext] = None) -> None: +@@ -589,21 +557,19 @@ class AsyncProtocol(Generic[T]): + if isinstance(address, tuple): + coro = asyncio.start_server( + self._incoming, +- host=None if self._sock else address[0], +- port=None if self._sock else address[1], ++ host=address[0], ++ port=address[1], + ssl=ssl, + backlog=1, + limit=self._limit, +- sock=self._sock, + ) + else: + coro = asyncio.start_unix_server( + self._incoming, +- path=None if self._sock else address, ++ path=address, + ssl=ssl, + backlog=1, + limit=self._limit, +- sock=self._sock, + ) + + # Allow runstate watchers to witness 'CONNECTING' state; some +@@ -630,7 +596,6 @@ class AsyncProtocol(Generic[T]): + await self._accepted.wait() + assert self._server is None + self._accepted = None +- self._sock = None + + self.logger.debug("Connection accepted.") + diff --git a/python-aqmp-fix-docstring-typo.patch b/python-aqmp-fix-docstring-typo.patch new file mode 100644 index 00000000..3095afa0 --- /dev/null +++ b/python-aqmp-fix-docstring-typo.patch @@ -0,0 +1,27 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:44 -0500 +Subject: python/aqmp: fix docstring typo + +Git-commit: dc6877bd2ea04a38700adcea2359d5d20c1082a6 + +Reported-by: Vladimir Sementsov-Ogievskiy +Signed-off-by: John Snow +Reviewed-by: Beraldo Leal +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/__init__.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/python/qemu/aqmp/__init__.py b/python/qemu/aqmp/__init__.py +index c6fa2dda58fdf4f1a867e677eadb..05f467c1415360169e9c66c8d27e 100644 +--- a/python/qemu/aqmp/__init__.py ++++ b/python/qemu/aqmp/__init__.py +@@ -6,7 +6,7 @@ asynchronously with QMP protocol servers, as implemented by QEMU, the + QEMU Guest Agent, and the QEMU Storage Daemon. + + `QMPClient` provides the main functionality of this package. All errors +-raised by this library dervive from `AQMPError`, see `aqmp.error` for ++raised by this library derive from `AQMPError`, see `aqmp.error` for + additional detail. See `aqmp.events` for an in-depth tutorial on + managing QMP events. + """ diff --git a/python-aqmp-fix-race-condition-in-legacy.patch b/python-aqmp-fix-race-condition-in-legacy.patch new file mode 100644 index 00000000..11243712 --- /dev/null +++ b/python-aqmp-fix-race-condition-in-legacy.patch @@ -0,0 +1,59 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:47 -0500 +Subject: python/aqmp: fix race condition in legacy.py +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 673856f9d889dc50b6a1a7964df960c4f00c7c93 + +legacy.py provides a synchronous model. iotests frequently uses this +paradigm: + + - create QMP client object + - start QEMU process + - await connection from QEMU process + +In the switch from sync to async QMP, the QMP client object stopped +calling bind() and listen() during the QMP object creation step, which +creates a race condition if the QEMU process dials in too quickly. + +With refactoring out of the way, restore the former behavior of calling +bind() and listen() during __init__() to fix this race condition. + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-10-jsnow@redhat.com +[Expanded commit message. --js] +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/legacy.py | 7 ++----- + 1 file changed, 2 insertions(+), 5 deletions(-) + +diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py +index dca1e76ed4994959caf542031363..cb50e60564823fdc0aeeb194c5e3 100644 +--- a/python/qemu/aqmp/legacy.py ++++ b/python/qemu/aqmp/legacy.py +@@ -57,7 +57,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): + self._timeout: Optional[float] = None + + if server: +- self._aqmp._bind_hack(address) # pylint: disable=protected-access ++ self._sync(self._aqmp.start_server(address)) + + _T = TypeVar('_T') + +@@ -90,10 +90,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): + self._aqmp.await_greeting = True + self._aqmp.negotiate = True + +- self._sync( +- self._aqmp.start_server_and_accept(self._address), +- timeout +- ) ++ self._sync(self._aqmp.accept(), timeout) + + ret = self._get_greeting() + assert ret is not None diff --git a/python-aqmp-handle-asyncio.TimeoutError-.patch b/python-aqmp-handle-asyncio.TimeoutError-.patch new file mode 100644 index 00000000..1a2c21cf --- /dev/null +++ b/python-aqmp-handle-asyncio.TimeoutError-.patch @@ -0,0 +1,46 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:46 -0500 +Subject: python/aqmp: handle asyncio.TimeoutError on execute() + +Git-commit: 3b5bf136f5798a4ea2c66875d6337ca3d6b79434 + +This exception can be injected into any await statement. If we are +canceled via timeout, we want to clear the pending execution record on +our way out. + +Signed-off-by: John Snow +Reviewed-by: Beraldo Leal +Reviewed-by: Vladimir Sementsov-Ogievskiy +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/qmp_client.py | 8 ++++++-- + 1 file changed, 6 insertions(+), 2 deletions(-) + +diff --git a/python/qemu/aqmp/qmp_client.py b/python/qemu/aqmp/qmp_client.py +index 6b43e1dbbe38eded19fd0115e8bc..45864f288e4fe5a7505c0022ed13 100644 +--- a/python/qemu/aqmp/qmp_client.py ++++ b/python/qemu/aqmp/qmp_client.py +@@ -435,7 +435,11 @@ class QMPClient(AsyncProtocol[Message], Events): + msg_id = msg['id'] + + self._pending[msg_id] = asyncio.Queue(maxsize=1) +- await self._outgoing.put(msg) ++ try: ++ await self._outgoing.put(msg) ++ except: ++ del self._pending[msg_id] ++ raise + + return msg_id + +@@ -452,9 +456,9 @@ class QMPClient(AsyncProtocol[Message], Events): + was lost, or some other problem. + """ + queue = self._pending[msg_id] +- result = await queue.get() + + try: ++ result = await queue.get() + if isinstance(result, ExecInterruptedError): + raise result + return result diff --git a/python-aqmp-refactor-_do_accept-into-two.patch b/python-aqmp-refactor-_do_accept-into-two.patch new file mode 100644 index 00000000..20770868 --- /dev/null +++ b/python-aqmp-refactor-_do_accept-into-two.patch @@ -0,0 +1,102 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:44 -0500 +Subject: python/aqmp: refactor _do_accept() into two distinct steps +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 5e9902a030ab832b0b6577764c65ce6a6f874af6 + +Refactor _do_accept() into _do_start_server() and _do_accept(). As of +this commit, the former calls the latter, but in subsequent commits +they'll be split apart. + +(So please forgive the misnomer for _do_start_server(); it will live up +to its name shortly, and the docstring will be updated then too. I'm +just cutting down on some churn.) + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-7-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/protocol.py | 29 ++++++++++++++++++++++++----- + python/tests/protocol.py | 4 ++-- + 2 files changed, 26 insertions(+), 7 deletions(-) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 631bcdaa554f4a104af4e25a3c61..e2bdad542dc0ef451dd200a1d679 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -295,7 +295,7 @@ class AsyncProtocol(Generic[T]): + session, the wrapped error may also be an `QMPError`. + """ + await self._session_guard( +- self._do_accept(address, ssl), ++ self._do_start_server(address, ssl), + 'Failed to establish connection') + await self._session_guard( + self._establish_session(), +@@ -509,8 +509,8 @@ class AsyncProtocol(Generic[T]): + self._sock = sock + + @upper_half +- async def _do_accept(self, address: SocketAddrT, +- ssl: Optional[SSLContext] = None) -> None: ++ async def _do_start_server(self, address: SocketAddrT, ++ ssl: Optional[SSLContext] = None) -> None: + """ + Acting as the transport server, accept a single connection. + +@@ -551,9 +551,28 @@ class AsyncProtocol(Generic[T]): + # otherwise yield. + await asyncio.sleep(0) + +- self._server = await coro # Starts listening +- await self._accepted.wait() # Waits for the callback to finish ++ # This will start the server (bind(2), listen(2)). It will also ++ # call accept(2) if we yield, but we don't block on that here. ++ self._server = await coro ++ ++ # Just for this one commit, wait for a peer. ++ # This gets split out in the next patch. ++ await self._do_accept() ++ ++ @upper_half ++ async def _do_accept(self) -> None: ++ """ ++ Wait for and accept an incoming connection. ++ ++ Requires that we have not yet accepted an incoming connection ++ from the upper_half, but it's OK if the server is no longer ++ running because the bottom_half has already accepted the ++ connection. ++ """ ++ assert self._accepted is not None ++ await self._accepted.wait() + assert self._server is None ++ self._accepted = None + self._sock = None + + self.logger.debug("Connection accepted.") +diff --git a/python/tests/protocol.py b/python/tests/protocol.py +index 8dd26c4ed1e0973b8058604c2373..5e442e1efbd19bf95a95de371060 100644 +--- a/python/tests/protocol.py ++++ b/python/tests/protocol.py +@@ -41,12 +41,12 @@ class NullProtocol(AsyncProtocol[None]): + self.trigger_input = asyncio.Event() + await super()._establish_session() + +- async def _do_accept(self, address, ssl=None): ++ async def _do_start_server(self, address, ssl=None): + if self.fake_session: + self._set_state(Runstate.CONNECTING) + await asyncio.sleep(0) + else: +- await super()._do_accept(address, ssl) ++ await super()._do_start_server(address, ssl) + + async def _do_connect(self, address, ssl=None): + if self.fake_session: diff --git a/python-aqmp-remove-_new_session-and-_est.patch b/python-aqmp-remove-_new_session-and-_est.patch new file mode 100644 index 00000000..0558ce78 --- /dev/null +++ b/python-aqmp-remove-_new_session-and-_est.patch @@ -0,0 +1,231 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:41 -0500 +Subject: python/aqmp: remove _new_session and _establish_connection +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 68a6cf3ffe3532c0655efbbf5910bd99a1b4a3fa + +These two methods attempted to entirely envelop the logic of +establishing a connection to a peer start to finish. However, we need to +break apart the incoming connection step into more granular steps. We +will no longer be able to reasonably constrain the logic inside of these +helper functions. + +So, remove them - with _session_guard(), they no longer serve a real +purpose. + +Although the public API doesn't change, the internal API does. Now that +there are no intermediary methods between e.g. connect() and +_do_connect(), there's no hook where the runstate is set. As a result, +the test suite changes a little to cope with the new semantics of +_do_accept() and _do_connect(). + +Lastly, take some pieces of the now-deleted docstrings and move +them up to the public interface level. They were a little more detailed, +and it won't hurt to keep them. + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-4-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/protocol.py | 117 ++++++++++++++--------------------- + python/tests/protocol.py | 10 ++- + 2 files changed, 53 insertions(+), 74 deletions(-) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 73719257e058b7e9e4d8a281bcd9..b7e5e635d886db0efc85f829f42e 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -275,13 +275,25 @@ class AsyncProtocol(Generic[T]): + If this call fails, `runstate` is guaranteed to be set back to `IDLE`. + + :param address: +- Address to listen to; UNIX socket path or TCP address/port. ++ Address to listen on; UNIX socket path or TCP address/port. + :param ssl: SSL context to use, if any. + + :raise StateError: When the `Runstate` is not `IDLE`. +- :raise ConnectError: If a connection could not be accepted. ++ :raise ConnectError: ++ When a connection or session cannot be established. ++ ++ This exception will wrap a more concrete one. In most cases, ++ the wrapped exception will be `OSError` or `EOFError`. If a ++ protocol-level failure occurs while establishing a new ++ session, the wrapped error may also be an `QMPError`. + """ +- await self._new_session(address, ssl, accept=True) ++ await self._session_guard( ++ self._do_accept(address, ssl), ++ 'Failed to establish connection') ++ await self._session_guard( ++ self._establish_session(), ++ 'Failed to establish session') ++ assert self.runstate == Runstate.RUNNING + + @upper_half + @require(Runstate.IDLE) +@@ -297,9 +309,21 @@ class AsyncProtocol(Generic[T]): + :param ssl: SSL context to use, if any. + + :raise StateError: When the `Runstate` is not `IDLE`. +- :raise ConnectError: If a connection cannot be made to the server. ++ :raise ConnectError: ++ When a connection or session cannot be established. ++ ++ This exception will wrap a more concrete one. In most cases, ++ the wrapped exception will be `OSError` or `EOFError`. If a ++ protocol-level failure occurs while establishing a new ++ session, the wrapped error may also be an `QMPError`. + """ +- await self._new_session(address, ssl) ++ await self._session_guard( ++ self._do_connect(address, ssl), ++ 'Failed to establish connection') ++ await self._session_guard( ++ self._establish_session(), ++ 'Failed to establish session') ++ assert self.runstate == Runstate.RUNNING + + @upper_half + async def disconnect(self) -> None: +@@ -401,73 +425,6 @@ class AsyncProtocol(Generic[T]): + self._runstate_event.set() + self._runstate_event.clear() + +- @upper_half +- async def _new_session(self, +- address: SocketAddrT, +- ssl: Optional[SSLContext] = None, +- accept: bool = False) -> None: +- """ +- Establish a new connection and initialize the session. +- +- Connect or accept a new connection, then begin the protocol +- session machinery. If this call fails, `runstate` is guaranteed +- to be set back to `IDLE`. +- +- :param address: +- Address to connect to/listen on; +- UNIX socket path or TCP address/port. +- :param ssl: SSL context to use, if any. +- :param accept: Accept a connection instead of connecting when `True`. +- +- :raise ConnectError: +- When a connection or session cannot be established. +- +- This exception will wrap a more concrete one. In most cases, +- the wrapped exception will be `OSError` or `EOFError`. If a +- protocol-level failure occurs while establishing a new +- session, the wrapped error may also be an `QMPError`. +- """ +- assert self.runstate == Runstate.IDLE +- +- await self._session_guard( +- self._establish_connection(address, ssl, accept), +- 'Failed to establish connection') +- +- await self._session_guard( +- self._establish_session(), +- 'Failed to establish session') +- +- assert self.runstate == Runstate.RUNNING +- +- @upper_half +- async def _establish_connection( +- self, +- address: SocketAddrT, +- ssl: Optional[SSLContext] = None, +- accept: bool = False +- ) -> None: +- """ +- Establish a new connection. +- +- :param address: +- Address to connect to/listen on; +- UNIX socket path or TCP address/port. +- :param ssl: SSL context to use, if any. +- :param accept: Accept a connection instead of connecting when `True`. +- """ +- assert self.runstate == Runstate.IDLE +- self._set_state(Runstate.CONNECTING) +- +- # Allow runstate watchers to witness 'CONNECTING' state; some +- # failures in the streaming layer are synchronous and will not +- # otherwise yield. +- await asyncio.sleep(0) +- +- if accept: +- await self._do_accept(address, ssl) +- else: +- await self._do_connect(address, ssl) +- + def _bind_hack(self, address: Union[str, Tuple[str, int]]) -> None: + """ + Used to create a socket in advance of accept(). +@@ -508,6 +465,9 @@ class AsyncProtocol(Generic[T]): + + :raise OSError: For stream-related errors. + """ ++ assert self.runstate == Runstate.IDLE ++ self._set_state(Runstate.CONNECTING) ++ + self.logger.debug("Awaiting connection on %s ...", address) + connected = asyncio.Event() + server: Optional[asyncio.AbstractServer] = None +@@ -550,6 +510,11 @@ class AsyncProtocol(Generic[T]): + sock=self._sock, + ) + ++ # Allow runstate watchers to witness 'CONNECTING' state; some ++ # failures in the streaming layer are synchronous and will not ++ # otherwise yield. ++ await asyncio.sleep(0) ++ + server = await coro # Starts listening + await connected.wait() # Waits for the callback to fire (and finish) + assert server is None +@@ -569,6 +534,14 @@ class AsyncProtocol(Generic[T]): + + :raise OSError: For stream-related errors. + """ ++ assert self.runstate == Runstate.IDLE ++ self._set_state(Runstate.CONNECTING) ++ ++ # Allow runstate watchers to witness 'CONNECTING' state; some ++ # failures in the streaming layer are synchronous and will not ++ # otherwise yield. ++ await asyncio.sleep(0) ++ + self.logger.debug("Connecting to %s ...", address) + + if isinstance(address, tuple): +diff --git a/python/tests/protocol.py b/python/tests/protocol.py +index 354d6559b9d1e3dc3ad29598af3c..8dd26c4ed1e0973b8058604c2373 100644 +--- a/python/tests/protocol.py ++++ b/python/tests/protocol.py +@@ -42,11 +42,17 @@ class NullProtocol(AsyncProtocol[None]): + await super()._establish_session() + + async def _do_accept(self, address, ssl=None): +- if not self.fake_session: ++ if self.fake_session: ++ self._set_state(Runstate.CONNECTING) ++ await asyncio.sleep(0) ++ else: + await super()._do_accept(address, ssl) + + async def _do_connect(self, address, ssl=None): +- if not self.fake_session: ++ if self.fake_session: ++ self._set_state(Runstate.CONNECTING) ++ await asyncio.sleep(0) ++ else: + await super()._do_connect(address, ssl) + + async def _do_recv(self) -> None: diff --git a/python-aqmp-rename-AQMPError-to-QMPError.patch b/python-aqmp-rename-AQMPError-to-QMPError.patch new file mode 100644 index 00000000..272398f0 --- /dev/null +++ b/python-aqmp-rename-AQMPError-to-QMPError.patch @@ -0,0 +1,221 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:49 -0500 +Subject: python/aqmp: rename AQMPError to QMPError + +Git-commit: 6e7751dc388df6daf425db0e245d4d3a10859803 + +This is in preparation for renaming qemu.aqmp to qemu.qmp. I should have +done this from this from the very beginning, but it's a convenient time +to make sure this churn is taken care of. + +Signed-off-by: John Snow +Reviewed-by: Vladimir Sementsov-Ogievskiy +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/__init__.py | 6 +++--- + python/qemu/aqmp/error.py | 12 ++++++------ + python/qemu/aqmp/events.py | 4 ++-- + python/qemu/aqmp/legacy.py | 4 ++-- + python/qemu/aqmp/protocol.py | 8 ++++---- + python/qemu/aqmp/qmp_client.py | 8 ++++---- + 6 files changed, 21 insertions(+), 21 deletions(-) + +diff --git a/python/qemu/aqmp/__init__.py b/python/qemu/aqmp/__init__.py +index 05f467c1415360169e9c66c8d27e..4c22c380790fd0f86402e402628b 100644 +--- a/python/qemu/aqmp/__init__.py ++++ b/python/qemu/aqmp/__init__.py +@@ -6,7 +6,7 @@ asynchronously with QMP protocol servers, as implemented by QEMU, the + QEMU Guest Agent, and the QEMU Storage Daemon. + + `QMPClient` provides the main functionality of this package. All errors +-raised by this library derive from `AQMPError`, see `aqmp.error` for ++raised by this library derive from `QMPError`, see `aqmp.error` for + additional detail. See `aqmp.events` for an in-depth tutorial on + managing QMP events. + """ +@@ -23,7 +23,7 @@ managing QMP events. + + import logging + +-from .error import AQMPError ++from .error import QMPError + from .events import EventListener + from .message import Message + from .protocol import ( +@@ -48,7 +48,7 @@ __all__ = ( + 'Runstate', + + # Exceptions, most generic to most explicit +- 'AQMPError', ++ 'QMPError', + 'StateError', + 'ConnectError', + 'ExecuteError', +diff --git a/python/qemu/aqmp/error.py b/python/qemu/aqmp/error.py +index 781f49b00877893d7a88f755c67f..24ba4d505410b5fe56390e3d4e02 100644 +--- a/python/qemu/aqmp/error.py ++++ b/python/qemu/aqmp/error.py +@@ -1,21 +1,21 @@ + """ +-AQMP Error Classes ++QMP Error Classes + + This package seeks to provide semantic error classes that are intended + to be used directly by clients when they would like to handle particular + semantic failures (e.g. "failed to connect") without needing to know the + enumeration of possible reasons for that failure. + +-AQMPError serves as the ancestor for all exceptions raised by this ++QMPError serves as the ancestor for all exceptions raised by this + package, and is suitable for use in handling semantic errors from this + library. In most cases, individual public methods will attempt to catch + and re-encapsulate various exceptions to provide a semantic + error-handling interface. + +-.. admonition:: AQMP Exception Hierarchy Reference ++.. admonition:: QMP Exception Hierarchy Reference + + | `Exception` +- | +-- `AQMPError` ++ | +-- `QMPError` + | +-- `ConnectError` + | +-- `StateError` + | +-- `ExecInterruptedError` +@@ -31,11 +31,11 @@ error-handling interface. + """ + + +-class AQMPError(Exception): ++class QMPError(Exception): + """Abstract error class for all errors originating from this package.""" + + +-class ProtocolError(AQMPError): ++class ProtocolError(QMPError): + """ + Abstract error class for protocol failures. + +diff --git a/python/qemu/aqmp/events.py b/python/qemu/aqmp/events.py +index 5f7150c78d49d9513978103dc9a7..f3d4e2b5e853c39db9e016009db0 100644 +--- a/python/qemu/aqmp/events.py ++++ b/python/qemu/aqmp/events.py +@@ -443,7 +443,7 @@ from typing import ( + Union, + ) + +-from .error import AQMPError ++from .error import QMPError + from .message import Message + + +@@ -451,7 +451,7 @@ EventNames = Union[str, Iterable[str], None] + EventFilter = Callable[[Message], bool] + + +-class ListenerError(AQMPError): ++class ListenerError(QMPError): + """ + Generic error class for `EventListener`-related problems. + """ +diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py +index 9431fe933019b1e1c221ea3ab7bb..27df22818a76190e872f08c0852e 100644 +--- a/python/qemu/aqmp/legacy.py ++++ b/python/qemu/aqmp/legacy.py +@@ -17,7 +17,7 @@ from typing import ( + + import qemu.qmp + +-from .error import AQMPError ++from .error import QMPError + from .protocol import Runstate, SocketAddrT + from .qmp_client import QMPClient + +@@ -168,7 +168,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): + # Nothing we can do about it now, but if we don't raise our + # own error, the user will be treated to a lot of traceback + # they might not understand. +- raise AQMPError( ++ raise QMPError( + "QEMUMonitorProtocol.close()" + " was not called before object was garbage collected" + ) +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 5b4f2f0d0a81a0d2902358e9b799..50e973c2f2dc9c5fa759380ab3e9 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -29,7 +29,7 @@ from typing import ( + cast, + ) + +-from .error import AQMPError ++from .error import QMPError + from .util import ( + bottom_half, + create_task, +@@ -65,7 +65,7 @@ class Runstate(Enum): + DISCONNECTING = 3 + + +-class ConnectError(AQMPError): ++class ConnectError(QMPError): + """ + Raised when the initial connection process has failed. + +@@ -90,7 +90,7 @@ class ConnectError(AQMPError): + return f"{self.error_message}: {cause}" + + +-class StateError(AQMPError): ++class StateError(QMPError): + """ + An API command (connect, execute, etc) was issued at an inappropriate time. + +@@ -363,7 +363,7 @@ class AsyncProtocol(Generic[T]): + This exception will wrap a more concrete one. In most cases, + the wrapped exception will be `OSError` or `EOFError`. If a + protocol-level failure occurs while establishing a new +- session, the wrapped error may also be an `AQMPError`. ++ session, the wrapped error may also be an `QMPError`. + """ + assert self.runstate == Runstate.IDLE + +diff --git a/python/qemu/aqmp/qmp_client.py b/python/qemu/aqmp/qmp_client.py +index 45864f288e4fe5a7505c0022ed13..90a8737f03a997f6813ee7cbcaac 100644 +--- a/python/qemu/aqmp/qmp_client.py ++++ b/python/qemu/aqmp/qmp_client.py +@@ -20,7 +20,7 @@ from typing import ( + cast, + ) + +-from .error import AQMPError, ProtocolError ++from .error import ProtocolError, QMPError + from .events import Events + from .message import Message + from .models import ErrorResponse, Greeting +@@ -66,7 +66,7 @@ class NegotiationError(_WrappedProtocolError): + """ + + +-class ExecuteError(AQMPError): ++class ExecuteError(QMPError): + """ + Exception raised by `QMPClient.execute()` on RPC failure. + +@@ -87,7 +87,7 @@ class ExecuteError(AQMPError): + self.error_class: str = error_response.error.class_ + + +-class ExecInterruptedError(AQMPError): ++class ExecInterruptedError(QMPError): + """ + Exception raised by `execute()` (et al) when an RPC is interrupted. + +@@ -641,7 +641,7 @@ class QMPClient(AsyncProtocol[Message], Events): + sock = self._writer.transport.get_extra_info('socket') + + if sock.family != socket.AF_UNIX: +- raise AQMPError("Sending file descriptors requires a UNIX socket.") ++ raise QMPError("Sending file descriptors requires a UNIX socket.") + + if not hasattr(sock, 'sendmsg'): + # We need to void the warranty sticker. diff --git a/python-aqmp-rename-accept-to-start_serve.patch b/python-aqmp-rename-accept-to-start_serve.patch new file mode 100644 index 00000000..47b4ba97 --- /dev/null +++ b/python-aqmp-rename-accept-to-start_serve.patch @@ -0,0 +1,163 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:40 -0500 +Subject: python/aqmp: rename 'accept()' to 'start_server_and_accept()' +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 0ba4e76b23fed77d09be7f56da783ab3f0b2d497 + +Previously, I had a method named "accept()" that under-the-hood calls +bind(2), listen(2) *and* accept(2). I meant this as a simplification and +counterpart to the one-shot "connect()" method. + +This is confusing to readers who expect accept() to mean *just* +accept(2). Since I need to split apart the "accept()" method into +multiple methods anyway (one of which strongly resembling accept(2)), it +feels pertinent to rename this method *now*. + +Rename this all-in-one method "start_server_and_accept()" instead. + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-3-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/legacy.py | 2 +- + python/qemu/aqmp/protocol.py | 6 ++++-- + python/tests/protocol.py | 24 ++++++++++++------------ + 3 files changed, 17 insertions(+), 15 deletions(-) + +diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py +index 6baa5f3409a6b459c67097d3c2a0..dca1e76ed4994959caf542031363 100644 +--- a/python/qemu/aqmp/legacy.py ++++ b/python/qemu/aqmp/legacy.py +@@ -91,7 +91,7 @@ class QEMUMonitorProtocol(qemu.qmp.QEMUMonitorProtocol): + self._aqmp.negotiate = True + + self._sync( +- self._aqmp.accept(self._address), ++ self._aqmp.start_server_and_accept(self._address), + timeout + ) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 009883f64d011e44dd003e9dcde3..73719257e058b7e9e4d8a281bcd9 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -265,8 +265,10 @@ class AsyncProtocol(Generic[T]): + + @upper_half + @require(Runstate.IDLE) +- async def accept(self, address: SocketAddrT, +- ssl: Optional[SSLContext] = None) -> None: ++ async def start_server_and_accept( ++ self, address: SocketAddrT, ++ ssl: Optional[SSLContext] = None ++ ) -> None: + """ + Accept a connection and begin processing message queues. + +diff --git a/python/tests/protocol.py b/python/tests/protocol.py +index 5cd7938be35ec61a1d412728f64e..354d6559b9d1e3dc3ad29598af3c 100644 +--- a/python/tests/protocol.py ++++ b/python/tests/protocol.py +@@ -413,14 +413,14 @@ class Accept(Connect): + assert family in ('INET', 'UNIX') + + if family == 'INET': +- await self.proto.accept(('example.com', 1)) ++ await self.proto.start_server_and_accept(('example.com', 1)) + elif family == 'UNIX': +- await self.proto.accept('/dev/null') ++ await self.proto.start_server_and_accept('/dev/null') + + async def _hanging_connection(self): + with TemporaryDirectory(suffix='.aqmp') as tmpdir: + sock = os.path.join(tmpdir, type(self.proto).__name__ + ".sock") +- await self.proto.accept(sock) ++ await self.proto.start_server_and_accept(sock) + + + class FakeSession(TestBase): +@@ -449,13 +449,13 @@ class FakeSession(TestBase): + @TestBase.async_test + async def testFakeAccept(self): + """Test the full state lifecycle (via accept) with a no-op session.""" +- await self.proto.accept('/not/a/real/path') ++ await self.proto.start_server_and_accept('/not/a/real/path') + self.assertEqual(self.proto.runstate, Runstate.RUNNING) + + @TestBase.async_test + async def testFakeRecv(self): + """Test receiving a fake/null message.""" +- await self.proto.accept('/not/a/real/path') ++ await self.proto.start_server_and_accept('/not/a/real/path') + + logname = self.proto.logger.name + with self.assertLogs(logname, level='DEBUG') as context: +@@ -471,7 +471,7 @@ class FakeSession(TestBase): + @TestBase.async_test + async def testFakeSend(self): + """Test sending a fake/null message.""" +- await self.proto.accept('/not/a/real/path') ++ await self.proto.start_server_and_accept('/not/a/real/path') + + logname = self.proto.logger.name + with self.assertLogs(logname, level='DEBUG') as context: +@@ -493,7 +493,7 @@ class FakeSession(TestBase): + ): + with self.assertRaises(StateError) as context: + if accept: +- await self.proto.accept('/not/a/real/path') ++ await self.proto.start_server_and_accept('/not/a/real/path') + else: + await self.proto.connect('/not/a/real/path') + +@@ -504,7 +504,7 @@ class FakeSession(TestBase): + @TestBase.async_test + async def testAcceptRequireRunning(self): + """Test that accept() cannot be called when Runstate=RUNNING""" +- await self.proto.accept('/not/a/real/path') ++ await self.proto.start_server_and_accept('/not/a/real/path') + + await self._prod_session_api( + Runstate.RUNNING, +@@ -515,7 +515,7 @@ class FakeSession(TestBase): + @TestBase.async_test + async def testConnectRequireRunning(self): + """Test that connect() cannot be called when Runstate=RUNNING""" +- await self.proto.accept('/not/a/real/path') ++ await self.proto.start_server_and_accept('/not/a/real/path') + + await self._prod_session_api( + Runstate.RUNNING, +@@ -526,7 +526,7 @@ class FakeSession(TestBase): + @TestBase.async_test + async def testAcceptRequireDisconnecting(self): + """Test that accept() cannot be called when Runstate=DISCONNECTING""" +- await self.proto.accept('/not/a/real/path') ++ await self.proto.start_server_and_accept('/not/a/real/path') + + # Cheat: force a disconnect. + await self.proto.simulate_disconnect() +@@ -541,7 +541,7 @@ class FakeSession(TestBase): + @TestBase.async_test + async def testConnectRequireDisconnecting(self): + """Test that connect() cannot be called when Runstate=DISCONNECTING""" +- await self.proto.accept('/not/a/real/path') ++ await self.proto.start_server_and_accept('/not/a/real/path') + + # Cheat: force a disconnect. + await self.proto.simulate_disconnect() +@@ -576,7 +576,7 @@ class SimpleSession(TestBase): + async def testSmoke(self): + with TemporaryDirectory(suffix='.aqmp') as tmpdir: + sock = os.path.join(tmpdir, type(self.proto).__name__ + ".sock") +- server_task = create_task(self.server.accept(sock)) ++ server_task = create_task(self.server.start_server_and_accept(sock)) + + # give the server a chance to start listening [...] + await asyncio.sleep(0) diff --git a/python-aqmp-split-_client_connected_cb-o.patch b/python-aqmp-split-_client_connected_cb-o.patch new file mode 100644 index 00000000..677a35ee --- /dev/null +++ b/python-aqmp-split-_client_connected_cb-o.patch @@ -0,0 +1,149 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:42 -0500 +Subject: python/aqmp: split _client_connected_cb() out as _incoming() +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 830e6fd36e2aef37b158a10dea6c3853ce43b20c + +As part of disentangling the monolithic nature of _do_accept(), split +out the incoming callback to prepare for factoring out the "wait for a +peer" step. Namely, this means using an event signal we can wait on from +outside of this method. + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-5-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/protocol.py | 83 +++++++++++++++++++++++++----------- + 1 file changed, 58 insertions(+), 25 deletions(-) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index b7e5e635d886db0efc85f829f42e..56f05b90308c44a86d0978fd2ce6 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -242,6 +242,10 @@ class AsyncProtocol(Generic[T]): + # Workaround for bind() + self._sock: Optional[socket.socket] = None + ++ # Server state for start_server() and _incoming() ++ self._server: Optional[asyncio.AbstractServer] = None ++ self._accepted: Optional[asyncio.Event] = None ++ + def __repr__(self) -> str: + cls_name = type(self).__name__ + tokens = [] +@@ -425,6 +429,54 @@ class AsyncProtocol(Generic[T]): + self._runstate_event.set() + self._runstate_event.clear() + ++ @bottom_half # However, it does not run from the R/W tasks. ++ async def _stop_server(self) -> None: ++ """ ++ Stop listening for / accepting new incoming connections. ++ """ ++ if self._server is None: ++ return ++ ++ try: ++ self.logger.debug("Stopping server.") ++ self._server.close() ++ await self._server.wait_closed() ++ self.logger.debug("Server stopped.") ++ finally: ++ self._server = None ++ ++ @bottom_half # However, it does not run from the R/W tasks. ++ async def _incoming(self, ++ reader: asyncio.StreamReader, ++ writer: asyncio.StreamWriter) -> None: ++ """ ++ Accept an incoming connection and signal the upper_half. ++ ++ This method does the minimum necessary to accept a single ++ incoming connection. It signals back to the upper_half ASAP so ++ that any errors during session initialization can occur ++ naturally in the caller's stack. ++ ++ :param reader: Incoming `asyncio.StreamReader` ++ :param writer: Incoming `asyncio.StreamWriter` ++ """ ++ peer = writer.get_extra_info('peername', 'Unknown peer') ++ self.logger.debug("Incoming connection from %s", peer) ++ ++ if self._reader or self._writer: ++ # Sadly, we can have more than one pending connection ++ # because of https://bugs.python.org/issue46715 ++ # Close any extra connections we don't actually want. ++ self.logger.warning("Extraneous connection inadvertently accepted") ++ writer.close() ++ return ++ ++ # A connection has been accepted; stop listening for new ones. ++ assert self._accepted is not None ++ await self._stop_server() ++ self._reader, self._writer = (reader, writer) ++ self._accepted.set() ++ + def _bind_hack(self, address: Union[str, Tuple[str, int]]) -> None: + """ + Used to create a socket in advance of accept(). +@@ -469,30 +521,11 @@ class AsyncProtocol(Generic[T]): + self._set_state(Runstate.CONNECTING) + + self.logger.debug("Awaiting connection on %s ...", address) +- connected = asyncio.Event() +- server: Optional[asyncio.AbstractServer] = None +- +- async def _client_connected_cb(reader: asyncio.StreamReader, +- writer: asyncio.StreamWriter) -> None: +- """Used to accept a single incoming connection, see below.""" +- nonlocal server +- nonlocal connected +- +- # A connection has been accepted; stop listening for new ones. +- assert server is not None +- server.close() +- await server.wait_closed() +- server = None +- +- # Register this client as being connected +- self._reader, self._writer = (reader, writer) +- +- # Signal back: We've accepted a client! +- connected.set() ++ self._accepted = asyncio.Event() + + if isinstance(address, tuple): + coro = asyncio.start_server( +- _client_connected_cb, ++ self._incoming, + host=None if self._sock else address[0], + port=None if self._sock else address[1], + ssl=ssl, +@@ -502,7 +535,7 @@ class AsyncProtocol(Generic[T]): + ) + else: + coro = asyncio.start_unix_server( +- _client_connected_cb, ++ self._incoming, + path=None if self._sock else address, + ssl=ssl, + backlog=1, +@@ -515,9 +548,9 @@ class AsyncProtocol(Generic[T]): + # otherwise yield. + await asyncio.sleep(0) + +- server = await coro # Starts listening +- await connected.wait() # Waits for the callback to fire (and finish) +- assert server is None ++ self._server = await coro # Starts listening ++ await self._accepted.wait() # Waits for the callback to finish ++ assert self._server is None + self._sock = None + + self.logger.debug("Connection accepted.") diff --git a/python-aqmp-squelch-pylint-warning-for-t.patch b/python-aqmp-squelch-pylint-warning-for-t.patch new file mode 100644 index 00000000..830c71f3 --- /dev/null +++ b/python-aqmp-squelch-pylint-warning-for-t.patch @@ -0,0 +1,37 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:43 -0500 +Subject: python/aqmp: squelch pylint warning for too many lines +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 1b9c8cb6ce6b5c5911eb715b2d5b0a2671999dde + +I would really like to keep this under 1000 lines, I promise. Doesn't +look like it's gonna happen. + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-6-jsnow@redhat.com +Signed-off-by: John Snow +(cherry picked from commit 1b9c8cb6ce6b5c5911eb715b2d5b0a2671999dde) +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/protocol.py | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index 56f05b90308c44a86d0978fd2ce6..631bcdaa554f4a104af4e25a3c61 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -10,6 +10,9 @@ In this package, it is used as the implementation for the `QMPClient` + class. + """ + ++# It's all the docstrings ... ! It's long for a good reason ^_^; ++# pylint: disable=too-many-lines ++ + import asyncio + from asyncio import StreamReader, StreamWriter + from enum import Enum diff --git a/python-aqmp-stop-the-server-during-disco.patch b/python-aqmp-stop-the-server-during-disco.patch new file mode 100644 index 00000000..d5696264 --- /dev/null +++ b/python-aqmp-stop-the-server-during-disco.patch @@ -0,0 +1,54 @@ +From: John Snow +Date: Fri, 25 Feb 2022 15:59:45 -0500 +Subject: python/aqmp: stop the server during disconnect() +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 32c5abf051d06ff103d9d30eb6a7f3e8bf582334 + +Before we allow the full separation of starting the server and accepting +new connections, make sure that the disconnect cleans up the server and +its new state, too. + +Signed-off-by: John Snow +Acked-by: Kevin Wolf +Reviewed-by: Daniel P. Berrangé +Message-id: 20220225205948.3693480-8-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/protocol.py | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/python/qemu/aqmp/protocol.py b/python/qemu/aqmp/protocol.py +index e2bdad542dc0ef451dd200a1d679..cdbc9cba0d15dea19cc1c60ca3c3 100644 +--- a/python/qemu/aqmp/protocol.py ++++ b/python/qemu/aqmp/protocol.py +@@ -432,7 +432,7 @@ class AsyncProtocol(Generic[T]): + self._runstate_event.set() + self._runstate_event.clear() + +- @bottom_half # However, it does not run from the R/W tasks. ++ @bottom_half + async def _stop_server(self) -> None: + """ + Stop listening for / accepting new incoming connections. +@@ -709,6 +709,7 @@ class AsyncProtocol(Generic[T]): + + self._reader = None + self._writer = None ++ self._accepted = None + + # NB: _runstate_changed cannot be cleared because we still need it to + # send the final runstate changed event ...! +@@ -732,6 +733,9 @@ class AsyncProtocol(Generic[T]): + def _done(task: Optional['asyncio.Future[Any]']) -> bool: + return task is not None and task.done() + ++ # If the server is running, stop it. ++ await self._stop_server() ++ + # Are we already in an error pathway? If either of the tasks are + # already done, or if we have no tasks but a reader/writer; we + # must be. diff --git a/python-introduce-qmp-shell-wrap-convenie.patch b/python-introduce-qmp-shell-wrap-convenie.patch new file mode 100644 index 00000000..32429819 --- /dev/null +++ b/python-introduce-qmp-shell-wrap-convenie.patch @@ -0,0 +1,167 @@ +From: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= +Date: Fri, 28 Jan 2022 16:11:56 +0000 +Subject: python: introduce qmp-shell-wrap convenience tool +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 439125293cc9cfb684eb4db23db04199f5f435a2 + +With the current 'qmp-shell' tool developers must first spawn QEMU with +a suitable -qmp arg and then spawn qmp-shell in a separate terminal +pointing to the right socket. + +With 'qmp-shell-wrap' developers can ignore QMP sockets entirely and +just pass the QEMU command and arguments they want. The program will +listen on a UNIX socket and tell QEMU to connect QMP to that. + +For example, this: + + # qmp-shell-wrap -- qemu-system-x86_64 -display none + +Is roughly equivalent of running: + + # qemu-system-x86_64 -display none -qmp qmp-shell-1234 & + # qmp-shell qmp-shell-1234 + +Except that 'qmp-shell-wrap' switches the socket peers around so that +it is the UNIX socket server and QEMU is the socket client. This makes +QEMU reliably go away when qmp-shell-wrap exits, closing the server +socket. + +Signed-off-by: Daniel P. Berrangé +Message-id: 20220128161157.36261-2-berrange@redhat.com +[Edited for rebase. --js] +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/qmp_shell.py | 65 ++++++++++++++++++++++++++++++++--- + python/setup.cfg | 1 + + scripts/qmp/qmp-shell-wrap | 11 ++++++ + 3 files changed, 73 insertions(+), 4 deletions(-) + +diff --git a/python/qemu/aqmp/qmp_shell.py b/python/qemu/aqmp/qmp_shell.py +index d11bf54b00e5d56616ae57be0006..c60df787fcd50bf8a0109e5f5cd3 100644 +--- a/python/qemu/aqmp/qmp_shell.py ++++ b/python/qemu/aqmp/qmp_shell.py +@@ -86,6 +86,7 @@ import logging + import os + import re + import readline ++from subprocess import Popen + import sys + from typing import ( + Iterator, +@@ -167,8 +168,10 @@ class QMPShell(QEMUMonitorProtocol): + :param verbose: Echo outgoing QMP messages to console. + """ + def __init__(self, address: SocketAddrT, +- pretty: bool = False, verbose: bool = False): +- super().__init__(address) ++ pretty: bool = False, ++ verbose: bool = False, ++ server: bool = False): ++ super().__init__(address, server=server) + self._greeting: Optional[QMPMessage] = None + self._completer = QMPCompleter() + self._transmode = False +@@ -409,8 +412,10 @@ class HMPShell(QMPShell): + :param verbose: Echo outgoing QMP messages to console. + """ + def __init__(self, address: SocketAddrT, +- pretty: bool = False, verbose: bool = False): +- super().__init__(address, pretty, verbose) ++ pretty: bool = False, ++ verbose: bool = False, ++ server: bool = False): ++ super().__init__(address, pretty, verbose, server) + self._cpu_index = 0 + + def _cmd_completion(self) -> None: +@@ -533,5 +538,57 @@ def main() -> None: + pass + + ++def main_wrap() -> None: ++ """ ++ qmp-shell-wrap 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('-v', '--verbose', action='store_true', ++ help='Verbose (echo commands sent and received)') ++ parser.add_argument('-p', '--pretty', action='store_true', ++ help='Pretty-print JSON') ++ ++ parser.add_argument('command', nargs=argparse.REMAINDER, ++ help='QEMU command line to invoke') ++ ++ args = parser.parse_args() ++ ++ cmd = args.command ++ if len(cmd) != 0 and cmd[0] == '--': ++ cmd = cmd[1:] ++ if len(cmd) == 0: ++ cmd = ["qemu-system-x86_64"] ++ ++ sockpath = "qmp-shell-wrap-%d" % os.getpid() ++ cmd += ["-qmp", "unix:%s" % sockpath] ++ ++ shell_class = HMPShell if args.hmp else QMPShell ++ ++ try: ++ address = shell_class.parse_address(sockpath) ++ except QMPBadPortError: ++ parser.error(f"Bad port number: {sockpath}") ++ return # pycharm doesn't know error() is noreturn ++ ++ try: ++ with shell_class(address, args.pretty, args.verbose, True) as qemu: ++ with Popen(cmd): ++ ++ try: ++ qemu.accept() ++ except ConnectError as err: ++ if isinstance(err.exc, OSError): ++ die(f"Couldn't connect to {args.qmp_server}: {err!s}") ++ die(str(err)) ++ ++ for _ in qemu.repl(): ++ pass ++ finally: ++ os.unlink(sockpath) ++ ++ + if __name__ == '__main__': + main() +diff --git a/python/setup.cfg b/python/setup.cfg +index 0063c757b78638ef651a362af338..bec54e8b0d663191e2b7afbfa350 100644 +--- a/python/setup.cfg ++++ b/python/setup.cfg +@@ -68,6 +68,7 @@ console_scripts = + qom-fuse = qemu.utils.qom_fuse:QOMFuse.entry_point [fuse] + qemu-ga-client = qemu.utils.qemu_ga_client:main + qmp-shell = qemu.aqmp.qmp_shell:main ++ qmp-shell-wrap = qemu.aqmp.qmp_shell:main_wrap + aqmp-tui = qemu.aqmp.aqmp_tui:main [tui] + + [flake8] +diff --git a/scripts/qmp/qmp-shell-wrap b/scripts/qmp/qmp-shell-wrap +new file mode 100755 +index 0000000000000000000000000000000000000000..9e94da114f5f87588639f6b2cc636391e80c3864 +--- /dev/null ++++ b/scripts/qmp/qmp-shell-wrap +@@ -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_wrap() diff --git a/python-machine-raise-VMLaunchFailure-exc.patch b/python-machine-raise-VMLaunchFailure-exc.patch new file mode 100644 index 00000000..4d6207cb --- /dev/null +++ b/python-machine-raise-VMLaunchFailure-exc.patch @@ -0,0 +1,126 @@ +From: John Snow +Date: Mon, 31 Jan 2022 23:11:32 -0500 +Subject: python/machine: raise VMLaunchFailure exception from launch() + +Git-commit: 50465f94d211beabfbfc80e4f85ec4fad0757570 + +This allows us to pack in some extra information about the failure, +which guarantees that if the caller did not *intentionally* cause a +failure (by capturing this Exception), some pretty good clues will be +printed at the bottom of the traceback information. + +This will help make failures in the event of a non-negative return code +more obvious when they go unhandled; the current behavior in +_post_shutdown() is to print a warning message only in the event of +signal-based terminations (for negative return codes). + +(Note: In Python, catching BaseException instead of Exception catches a +broader array of Exception events, including SystemExit and +KeyboardInterrupt. We do not want to "wrap" such exceptions as a +VMLaunchFailure, because that will 'downgrade' the exception from a +BaseException to a regular Exception. We do, however, want to perform +cleanup in either case, so catch on the broadest scope and +wrap-and-re-raise only in the more targeted scope.) + +Signed-off-by: John Snow +Reviewed-by: Hanna Reitz +Reviewed-by: Kevin Wolf +Message-id: 20220201041134.1237016-3-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/machine/machine.py | 45 ++++++++++++++++++++--- + tests/qemu-iotests/tests/mirror-top-perms | 3 +- + 2 files changed, 40 insertions(+), 8 deletions(-) + +diff --git a/python/qemu/machine/machine.py b/python/qemu/machine/machine.py +index 67ab06ca2b6daa531b7c0ad9f7c2..a5972fab4d2b1fddfa2d5a7db882 100644 +--- a/python/qemu/machine/machine.py ++++ b/python/qemu/machine/machine.py +@@ -74,6 +74,35 @@ class QEMUMachineAddDeviceError(QEMUMachineError): + """ + + ++class VMLaunchFailure(QEMUMachineError): ++ """ ++ Exception raised when a VM launch was attempted, but failed. ++ """ ++ def __init__(self, exitcode: Optional[int], ++ command: str, output: Optional[str]): ++ super().__init__(exitcode, command, output) ++ self.exitcode = exitcode ++ self.command = command ++ self.output = output ++ ++ def __str__(self) -> str: ++ ret = '' ++ if self.__cause__ is not None: ++ name = type(self.__cause__).__name__ ++ reason = str(self.__cause__) ++ if reason: ++ ret += f"{name}: {reason}" ++ else: ++ ret += f"{name}" ++ ret += '\n' ++ ++ if self.exitcode is not None: ++ ret += f"\tExit code: {self.exitcode}\n" ++ ret += f"\tCommand: {self.command}\n" ++ ret += f"\tOutput: {self.output}\n" ++ return ret ++ ++ + class AbnormalShutdown(QEMUMachineError): + """ + Exception raised when a graceful shutdown was requested, but not performed. +@@ -397,7 +426,7 @@ class QEMUMachine: + + try: + self._launch() +- except: ++ except BaseException as exc: + # We may have launched the process but it may + # have exited before we could connect via QMP. + # Assume the VM didn't launch or is exiting. +@@ -408,11 +437,15 @@ class QEMUMachine: + else: + self._post_shutdown() + +- LOG.debug('Error launching VM') +- if self._qemu_full_args: +- LOG.debug('Command: %r', ' '.join(self._qemu_full_args)) +- if self._iolog: +- LOG.debug('Output: %r', self._iolog) ++ if isinstance(exc, Exception): ++ raise VMLaunchFailure( ++ exitcode=self.exitcode(), ++ command=' '.join(self._qemu_full_args), ++ output=self._iolog ++ ) from exc ++ ++ # Don't wrap 'BaseException'; doing so would downgrade ++ # that exception. However, we still want to clean up. + raise + + def _launch(self) -> None: +diff --git a/tests/qemu-iotests/tests/mirror-top-perms b/tests/qemu-iotests/tests/mirror-top-perms +index 0a51a613f39764b2b3ab3fa460ef..b5849978c4158c35e18480186ea2 100755 +--- a/tests/qemu-iotests/tests/mirror-top-perms ++++ b/tests/qemu-iotests/tests/mirror-top-perms +@@ -21,7 +21,6 @@ + + import os + +-from qemu.aqmp import ConnectError + from qemu.machine import machine + from qemu.qmp import QMPConnectError + +@@ -107,7 +106,7 @@ class TestMirrorTopPerms(iotests.QMPTestCase): + self.vm_b.launch() + print('ERROR: VM B launched successfully, ' + 'this should not have happened') +- except (QMPConnectError, ConnectError): ++ except (QMPConnectError, machine.VMLaunchFailure): + assert 'Is another process using the image' in self.vm_b.get_log() + + result = self.vm.qmp('block-job-cancel', diff --git a/python-move-qmp-shell-under-the-AQMP-pac.patch b/python-move-qmp-shell-under-the-AQMP-pac.patch new file mode 100644 index 00000000..4dc8a6e8 --- /dev/null +++ b/python-move-qmp-shell-under-the-AQMP-pac.patch @@ -0,0 +1,1143 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:55 -0500 +Subject: python: move qmp-shell under the AQMP package + +Git-commit: fd9c3a6219b0470c356c8486188052d353846806 + +Signed-off-by: John Snow +Reviewed-by: Vladimir Sementsov-Ogievskiy +Reviewed-by: Beraldo Leal +Signed-off-by: Li Zhang +--- + python/README.rst | 2 +- + python/qemu/aqmp/qmp_shell.py | 537 ++++++++++++++++++++++++++++++++++ + python/qemu/qmp/qmp_shell.py | 537 ---------------------------------- + python/setup.cfg | 2 +- + scripts/qmp/qmp-shell | 2 +- + 5 files changed, 540 insertions(+), 540 deletions(-) + +diff --git a/python/README.rst b/python/README.rst +index 9c1fceaee73b11cb313b71a9a558..fcf74f69eae011aafd1e089e1f92 100644 +--- a/python/README.rst ++++ b/python/README.rst +@@ -59,7 +59,7 @@ Package installation also normally provides executable console scripts, + so that tools like ``qmp-shell`` are always available via $PATH. To + invoke them without installation, you can invoke e.g.: + +-``> PYTHONPATH=~/src/qemu/python python3 -m qemu.qmp.qmp_shell`` ++``> PYTHONPATH=~/src/qemu/python python3 -m qemu.aqmp.qmp_shell`` + + The mappings between console script name and python module path can be + found in ``setup.cfg``. +diff --git a/python/qemu/aqmp/qmp_shell.py b/python/qemu/aqmp/qmp_shell.py +new file mode 100644 +index 0000000000000000000000000000000000000000..d11bf54b00e5d56616ae57be0006b9b0629dd9f4 +--- /dev/null ++++ b/python/qemu/aqmp/qmp_shell.py +@@ -0,0 +1,537 @@ ++# ++# 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.aqmp import ConnectError, QMPError, SocketAddrT ++from qemu.aqmp.legacy import ( ++ QEMUMonitorProtocol, ++ QMPBadPortError, ++ QMPMessage, ++ QMPObject, ++) ++ ++ ++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(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(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: 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: 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) ++ 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: 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 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 ConnectError as err: ++ if isinstance(err.exc, OSError): ++ die(f"Couldn't connect to {args.qmp_server}: {err!s}") ++ die(str(err)) ++ ++ for _ in qemu.repl(): ++ pass ++ ++ ++if __name__ == '__main__': ++ main() +diff --git a/python/qemu/qmp/qmp_shell.py b/python/qemu/qmp/qmp_shell.py +deleted file mode 100644 +index d11bf54b00e5d56616ae57be0006b9b0629dd9f4..0000000000000000000000000000000000000000 +--- a/python/qemu/qmp/qmp_shell.py ++++ /dev/null +@@ -1,537 +0,0 @@ +-# +-# 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.aqmp import ConnectError, QMPError, SocketAddrT +-from qemu.aqmp.legacy import ( +- QEMUMonitorProtocol, +- QMPBadPortError, +- QMPMessage, +- QMPObject, +-) +- +- +-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(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(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: 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: 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) +- 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: 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 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 ConnectError as err: +- if isinstance(err.exc, OSError): +- die(f"Couldn't connect to {args.qmp_server}: {err!s}") +- die(str(err)) +- +- for _ in qemu.repl(): +- pass +- +- +-if __name__ == '__main__': +- main() +diff --git a/python/setup.cfg b/python/setup.cfg +index 91ccef7e8fd85d0d6d3d86adbc8d..0063c757b78638ef651a362af338 100644 +--- a/python/setup.cfg ++++ b/python/setup.cfg +@@ -67,7 +67,7 @@ console_scripts = + qom-tree = qemu.utils.qom:QOMTree.entry_point + qom-fuse = qemu.utils.qom_fuse:QOMFuse.entry_point [fuse] + qemu-ga-client = qemu.utils.qemu_ga_client:main +- qmp-shell = qemu.qmp.qmp_shell:main ++ qmp-shell = qemu.aqmp.qmp_shell:main + aqmp-tui = qemu.aqmp.aqmp_tui:main [tui] + + [flake8] +diff --git a/scripts/qmp/qmp-shell b/scripts/qmp/qmp-shell +index 4a20f97db708b74ebfed715e38ea..31b19d73e22a4d9c91aaf0ce9725 100755 +--- a/scripts/qmp/qmp-shell ++++ b/scripts/qmp/qmp-shell +@@ -4,7 +4,7 @@ import os + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) +-from qemu.qmp import qmp_shell ++from qemu.aqmp import qmp_shell + + + if __name__ == '__main__': diff --git a/python-move-qmp-utilities-to-python-qemu.patch b/python-move-qmp-utilities-to-python-qemu.patch new file mode 100644 index 00000000..e8f33517 --- /dev/null +++ b/python-move-qmp-utilities-to-python-qemu.patch @@ -0,0 +1,2153 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:54 -0500 +Subject: python: move qmp utilities to python/qemu/utils + +Git-commit: 0347c4c4cfed47e54d9dc275ceb28d35b250749f + +In order to upload a QMP package to PyPI, I want to remove any scripts +that I am not 100% confident I want to support upstream, beyond our +castle walls. + +Move most of our QMP utilities into the utils package so we can split +them out from the PyPI upload. + +Signed-off-by: John Snow +Reviewed-by: Vladimir Sementsov-Ogievskiy +Reviewed-by: Beraldo Leal +Signed-off-by: Li Zhang +--- + python/qemu/qmp/qemu_ga_client.py | 323 ---------------------------- + python/qemu/qmp/qom.py | 272 ----------------------- + python/qemu/qmp/qom_common.py | 178 --------------- + python/qemu/qmp/qom_fuse.py | 206 ------------------ + python/qemu/utils/qemu_ga_client.py | 323 ++++++++++++++++++++++++++++ + python/qemu/utils/qom.py | 272 +++++++++++++++++++++++ + python/qemu/utils/qom_common.py | 178 +++++++++++++++ + python/qemu/utils/qom_fuse.py | 206 ++++++++++++++++++ + python/setup.cfg | 16 +- + scripts/qmp/qemu-ga-client | 2 +- + scripts/qmp/qom-fuse | 2 +- + scripts/qmp/qom-get | 2 +- + scripts/qmp/qom-list | 2 +- + scripts/qmp/qom-set | 2 +- + scripts/qmp/qom-tree | 2 +- + 15 files changed, 993 insertions(+), 993 deletions(-) + +diff --git a/python/qemu/qmp/qemu_ga_client.py b/python/qemu/qmp/qemu_ga_client.py +deleted file mode 100644 +index 67ac0b421129dd03e973886ac4ac1e1e3de3d358..0000000000000000000000000000000000000000 +--- a/python/qemu/qmp/qemu_ga_client.py ++++ /dev/null +@@ -1,323 +0,0 @@ +-""" +-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/python/qemu/qmp/qom.py b/python/qemu/qmp/qom.py +deleted file mode 100644 +index 8ff28a83439767ce37db21d8790c50ddb4845f50..0000000000000000000000000000000000000000 +--- a/python/qemu/qmp/qom.py ++++ /dev/null +@@ -1,272 +0,0 @@ +-""" +-QEMU Object Model testing tools. +- +-usage: qom [-h] {set,get,list,tree,fuse} ... +- +-Query and manipulate QOM data +- +-optional arguments: +- -h, --help show this help message and exit +- +-QOM commands: +- {set,get,list,tree,fuse} +- set Set a QOM property value +- get Get a QOM property value +- list List QOM properties at a given path +- tree Show QOM tree from a given path +- fuse Mount a QOM tree as a FUSE filesystem +-""" +-## +-# Copyright John Snow 2020, for Red Hat, Inc. +-# Copyright IBM, Corp. 2011 +-# +-# Authors: +-# John Snow +-# 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. +-# +-# Based on ./scripts/qmp/qom-[set|get|tree|list] +-## +- +-import argparse +- +-from . import QMPResponseError +-from .qom_common import QOMCommand +- +- +-try: +- from .qom_fuse import QOMFuse +-except ModuleNotFoundError as _err: +- if _err.name != 'fuse': +- raise +-else: +- assert issubclass(QOMFuse, QOMCommand) +- +- +-class QOMSet(QOMCommand): +- """ +- QOM Command - Set a property to a given value. +- +- usage: qom-set [-h] [--socket SOCKET] . +- +- Set a QOM property value +- +- positional arguments: +- . QOM path and property, separated by a period '.' +- new QOM property value +- +- optional arguments: +- -h, --help show this help message and exit +- --socket SOCKET, -s SOCKET +- QMP socket path or address (addr:port). May also be +- set via QMP_SOCKET environment variable. +- """ +- name = 'set' +- help = 'Set a QOM property value' +- +- @classmethod +- def configure_parser(cls, parser: argparse.ArgumentParser) -> None: +- super().configure_parser(parser) +- cls.add_path_prop_arg(parser) +- parser.add_argument( +- 'value', +- metavar='', +- action='store', +- help='new QOM property value' +- ) +- +- def __init__(self, args: argparse.Namespace): +- super().__init__(args) +- self.path, self.prop = args.path_prop.rsplit('.', 1) +- self.value = args.value +- +- def run(self) -> int: +- rsp = self.qmp.command( +- 'qom-set', +- path=self.path, +- property=self.prop, +- value=self.value +- ) +- print(rsp) +- return 0 +- +- +-class QOMGet(QOMCommand): +- """ +- QOM Command - Get a property's current value. +- +- usage: qom-get [-h] [--socket SOCKET] . +- +- Get a QOM property value +- +- positional arguments: +- . QOM path and property, separated by a period '.' +- +- optional arguments: +- -h, --help show this help message and exit +- --socket SOCKET, -s SOCKET +- QMP socket path or address (addr:port). May also be +- set via QMP_SOCKET environment variable. +- """ +- name = 'get' +- help = 'Get a QOM property value' +- +- @classmethod +- def configure_parser(cls, parser: argparse.ArgumentParser) -> None: +- super().configure_parser(parser) +- cls.add_path_prop_arg(parser) +- +- def __init__(self, args: argparse.Namespace): +- super().__init__(args) +- try: +- tmp = args.path_prop.rsplit('.', 1) +- except ValueError as err: +- raise ValueError('Invalid format for .') from err +- self.path = tmp[0] +- self.prop = tmp[1] +- +- def run(self) -> int: +- rsp = self.qmp.command( +- 'qom-get', +- path=self.path, +- property=self.prop +- ) +- if isinstance(rsp, dict): +- for key, value in rsp.items(): +- print(f"{key}: {value}") +- else: +- print(rsp) +- return 0 +- +- +-class QOMList(QOMCommand): +- """ +- QOM Command - List the properties at a given path. +- +- usage: qom-list [-h] [--socket SOCKET] +- +- List QOM properties at a given path +- +- positional arguments: +- QOM path +- +- optional arguments: +- -h, --help show this help message and exit +- --socket SOCKET, -s SOCKET +- QMP socket path or address (addr:port). May also be +- set via QMP_SOCKET environment variable. +- """ +- name = 'list' +- help = 'List QOM properties at a given path' +- +- @classmethod +- def configure_parser(cls, parser: argparse.ArgumentParser) -> None: +- super().configure_parser(parser) +- parser.add_argument( +- 'path', +- metavar='', +- action='store', +- help='QOM path', +- ) +- +- def __init__(self, args: argparse.Namespace): +- super().__init__(args) +- self.path = args.path +- +- def run(self) -> int: +- rsp = self.qom_list(self.path) +- for item in rsp: +- if item.child: +- print(f"{item.name}/") +- elif item.link: +- print(f"@{item.name}/") +- else: +- print(item.name) +- return 0 +- +- +-class QOMTree(QOMCommand): +- """ +- QOM Command - Show the full tree below a given path. +- +- usage: qom-tree [-h] [--socket SOCKET] [] +- +- Show QOM tree from a given path +- +- positional arguments: +- QOM path +- +- optional arguments: +- -h, --help show this help message and exit +- --socket SOCKET, -s SOCKET +- QMP socket path or address (addr:port). May also be +- set via QMP_SOCKET environment variable. +- """ +- name = 'tree' +- help = 'Show QOM tree from a given path' +- +- @classmethod +- def configure_parser(cls, parser: argparse.ArgumentParser) -> None: +- super().configure_parser(parser) +- parser.add_argument( +- 'path', +- metavar='', +- action='store', +- help='QOM path', +- nargs='?', +- default='/' +- ) +- +- def __init__(self, args: argparse.Namespace): +- super().__init__(args) +- self.path = args.path +- +- def _list_node(self, path: str) -> None: +- print(path) +- items = self.qom_list(path) +- for item in items: +- if item.child: +- continue +- try: +- rsp = self.qmp.command('qom-get', path=path, +- property=item.name) +- print(f" {item.name}: {rsp} ({item.type})") +- except QMPResponseError as err: +- print(f" {item.name}: ({item.type})") +- print('') +- for item in items: +- if not item.child: +- continue +- if path == '/': +- path = '' +- self._list_node(f"{path}/{item.name}") +- +- def run(self) -> int: +- self._list_node(self.path) +- return 0 +- +- +-def main() -> int: +- """QOM script main entry point.""" +- parser = argparse.ArgumentParser( +- description='Query and manipulate QOM data' +- ) +- subparsers = parser.add_subparsers( +- title='QOM commands', +- dest='command' +- ) +- +- for command in QOMCommand.__subclasses__(): +- command.register(subparsers) +- +- args = parser.parse_args() +- +- if args.command is None: +- parser.error('Command not specified.') +- return 1 +- +- cmd_class = args.cmd_class +- assert isinstance(cmd_class, type(QOMCommand)) +- return cmd_class.command_runner(args) +diff --git a/python/qemu/qmp/qom_common.py b/python/qemu/qmp/qom_common.py +deleted file mode 100644 +index a59ae1a2a1883cb4d89b0e44507c5001f44357a0..0000000000000000000000000000000000000000 +--- a/python/qemu/qmp/qom_common.py ++++ /dev/null +@@ -1,178 +0,0 @@ +-""" +-QOM Command abstractions. +-""" +-## +-# Copyright John Snow 2020, for Red Hat, Inc. +-# Copyright IBM, Corp. 2011 +-# +-# Authors: +-# John Snow +-# 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. +-# +-# Based on ./scripts/qmp/qom-[set|get|tree|list] +-## +- +-import argparse +-import os +-import sys +-from typing import ( +- Any, +- Dict, +- List, +- Optional, +- Type, +- TypeVar, +-) +- +-from . import QEMUMonitorProtocol, QMPError +- +- +-# The following is needed only for a type alias. +-Subparsers = argparse._SubParsersAction # pylint: disable=protected-access +- +- +-class ObjectPropertyInfo: +- """ +- Represents the return type from e.g. qom-list. +- """ +- def __init__(self, name: str, type_: str, +- description: Optional[str] = None, +- default_value: Optional[object] = None): +- self.name = name +- self.type = type_ +- self.description = description +- self.default_value = default_value +- +- @classmethod +- def make(cls, value: Dict[str, Any]) -> 'ObjectPropertyInfo': +- """ +- Build an ObjectPropertyInfo from a Dict with an unknown shape. +- """ +- assert value.keys() >= {'name', 'type'} +- assert value.keys() <= {'name', 'type', 'description', 'default-value'} +- return cls(value['name'], value['type'], +- value.get('description'), +- value.get('default-value')) +- +- @property +- def child(self) -> bool: +- """Is this property a child property?""" +- return self.type.startswith('child<') +- +- @property +- def link(self) -> bool: +- """Is this property a link property?""" +- return self.type.startswith('link<') +- +- +-CommandT = TypeVar('CommandT', bound='QOMCommand') +- +- +-class QOMCommand: +- """ +- Represents a QOM sub-command. +- +- :param args: Parsed arguments, as returned from parser.parse_args. +- """ +- name: str +- help: str +- +- def __init__(self, args: argparse.Namespace): +- if args.socket is None: +- raise QMPError("No QMP socket path or address given") +- self.qmp = QEMUMonitorProtocol( +- QEMUMonitorProtocol.parse_address(args.socket) +- ) +- self.qmp.connect() +- +- @classmethod +- def register(cls, subparsers: Subparsers) -> None: +- """ +- Register this command with the argument parser. +- +- :param subparsers: argparse subparsers object, from "add_subparsers". +- """ +- subparser = subparsers.add_parser(cls.name, help=cls.help, +- description=cls.help) +- cls.configure_parser(subparser) +- +- @classmethod +- def configure_parser(cls, parser: argparse.ArgumentParser) -> None: +- """ +- Configure a parser with this command's arguments. +- +- :param parser: argparse parser or subparser object. +- """ +- default_path = os.environ.get('QMP_SOCKET') +- parser.add_argument( +- '--socket', '-s', +- dest='socket', +- action='store', +- help='QMP socket path or address (addr:port).' +- ' May also be set via QMP_SOCKET environment variable.', +- default=default_path +- ) +- parser.set_defaults(cmd_class=cls) +- +- @classmethod +- def add_path_prop_arg(cls, parser: argparse.ArgumentParser) -> None: +- """ +- Add the . positional argument to this command. +- +- :param parser: The parser to add the argument to. +- """ +- parser.add_argument( +- 'path_prop', +- metavar='.', +- action='store', +- help="QOM path and property, separated by a period '.'" +- ) +- +- def run(self) -> int: +- """ +- Run this command. +- +- :return: 0 on success, 1 otherwise. +- """ +- raise NotImplementedError +- +- def qom_list(self, path: str) -> List[ObjectPropertyInfo]: +- """ +- :return: a strongly typed list from the 'qom-list' command. +- """ +- rsp = self.qmp.command('qom-list', path=path) +- # qom-list returns List[ObjectPropertyInfo] +- assert isinstance(rsp, list) +- return [ObjectPropertyInfo.make(x) for x in rsp] +- +- @classmethod +- def command_runner( +- cls: Type[CommandT], +- args: argparse.Namespace +- ) -> int: +- """ +- Run a fully-parsed subcommand, with error-handling for the CLI. +- +- :return: The return code from `run()`. +- """ +- try: +- cmd = cls(args) +- return cmd.run() +- except QMPError as err: +- print(f"{type(err).__name__}: {err!s}", file=sys.stderr) +- return -1 +- +- @classmethod +- def entry_point(cls) -> int: +- """ +- Build this command's parser, parse arguments, and run the command. +- +- :return: `run`'s return code. +- """ +- parser = argparse.ArgumentParser(description=cls.help) +- cls.configure_parser(parser) +- args = parser.parse_args() +- return cls.command_runner(args) +diff --git a/python/qemu/qmp/qom_fuse.py b/python/qemu/qmp/qom_fuse.py +deleted file mode 100644 +index 43f4671fdb18c6aa1b11df9694855167fced8f10..0000000000000000000000000000000000000000 +--- a/python/qemu/qmp/qom_fuse.py ++++ /dev/null +@@ -1,206 +0,0 @@ +-""" +-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/python/qemu/utils/qemu_ga_client.py b/python/qemu/utils/qemu_ga_client.py +new file mode 100644 +index 0000000000000000000000000000000000000000..67ac0b421129dd03e973886ac4ac1e1e3de3d358 +--- /dev/null ++++ b/python/qemu/utils/qemu_ga_client.py +@@ -0,0 +1,323 @@ ++""" ++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/python/qemu/utils/qom.py b/python/qemu/utils/qom.py +new file mode 100644 +index 0000000000000000000000000000000000000000..8ff28a83439767ce37db21d8790c50ddb4845f50 +--- /dev/null ++++ b/python/qemu/utils/qom.py +@@ -0,0 +1,272 @@ ++""" ++QEMU Object Model testing tools. ++ ++usage: qom [-h] {set,get,list,tree,fuse} ... ++ ++Query and manipulate QOM data ++ ++optional arguments: ++ -h, --help show this help message and exit ++ ++QOM commands: ++ {set,get,list,tree,fuse} ++ set Set a QOM property value ++ get Get a QOM property value ++ list List QOM properties at a given path ++ tree Show QOM tree from a given path ++ fuse Mount a QOM tree as a FUSE filesystem ++""" ++## ++# Copyright John Snow 2020, for Red Hat, Inc. ++# Copyright IBM, Corp. 2011 ++# ++# Authors: ++# John Snow ++# 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. ++# ++# Based on ./scripts/qmp/qom-[set|get|tree|list] ++## ++ ++import argparse ++ ++from . import QMPResponseError ++from .qom_common import QOMCommand ++ ++ ++try: ++ from .qom_fuse import QOMFuse ++except ModuleNotFoundError as _err: ++ if _err.name != 'fuse': ++ raise ++else: ++ assert issubclass(QOMFuse, QOMCommand) ++ ++ ++class QOMSet(QOMCommand): ++ """ ++ QOM Command - Set a property to a given value. ++ ++ usage: qom-set [-h] [--socket SOCKET] . ++ ++ Set a QOM property value ++ ++ positional arguments: ++ . QOM path and property, separated by a period '.' ++ new QOM property value ++ ++ optional arguments: ++ -h, --help show this help message and exit ++ --socket SOCKET, -s SOCKET ++ QMP socket path or address (addr:port). May also be ++ set via QMP_SOCKET environment variable. ++ """ ++ name = 'set' ++ help = 'Set a QOM property value' ++ ++ @classmethod ++ def configure_parser(cls, parser: argparse.ArgumentParser) -> None: ++ super().configure_parser(parser) ++ cls.add_path_prop_arg(parser) ++ parser.add_argument( ++ 'value', ++ metavar='', ++ action='store', ++ help='new QOM property value' ++ ) ++ ++ def __init__(self, args: argparse.Namespace): ++ super().__init__(args) ++ self.path, self.prop = args.path_prop.rsplit('.', 1) ++ self.value = args.value ++ ++ def run(self) -> int: ++ rsp = self.qmp.command( ++ 'qom-set', ++ path=self.path, ++ property=self.prop, ++ value=self.value ++ ) ++ print(rsp) ++ return 0 ++ ++ ++class QOMGet(QOMCommand): ++ """ ++ QOM Command - Get a property's current value. ++ ++ usage: qom-get [-h] [--socket SOCKET] . ++ ++ Get a QOM property value ++ ++ positional arguments: ++ . QOM path and property, separated by a period '.' ++ ++ optional arguments: ++ -h, --help show this help message and exit ++ --socket SOCKET, -s SOCKET ++ QMP socket path or address (addr:port). May also be ++ set via QMP_SOCKET environment variable. ++ """ ++ name = 'get' ++ help = 'Get a QOM property value' ++ ++ @classmethod ++ def configure_parser(cls, parser: argparse.ArgumentParser) -> None: ++ super().configure_parser(parser) ++ cls.add_path_prop_arg(parser) ++ ++ def __init__(self, args: argparse.Namespace): ++ super().__init__(args) ++ try: ++ tmp = args.path_prop.rsplit('.', 1) ++ except ValueError as err: ++ raise ValueError('Invalid format for .') from err ++ self.path = tmp[0] ++ self.prop = tmp[1] ++ ++ def run(self) -> int: ++ rsp = self.qmp.command( ++ 'qom-get', ++ path=self.path, ++ property=self.prop ++ ) ++ if isinstance(rsp, dict): ++ for key, value in rsp.items(): ++ print(f"{key}: {value}") ++ else: ++ print(rsp) ++ return 0 ++ ++ ++class QOMList(QOMCommand): ++ """ ++ QOM Command - List the properties at a given path. ++ ++ usage: qom-list [-h] [--socket SOCKET] ++ ++ List QOM properties at a given path ++ ++ positional arguments: ++ QOM path ++ ++ optional arguments: ++ -h, --help show this help message and exit ++ --socket SOCKET, -s SOCKET ++ QMP socket path or address (addr:port). May also be ++ set via QMP_SOCKET environment variable. ++ """ ++ name = 'list' ++ help = 'List QOM properties at a given path' ++ ++ @classmethod ++ def configure_parser(cls, parser: argparse.ArgumentParser) -> None: ++ super().configure_parser(parser) ++ parser.add_argument( ++ 'path', ++ metavar='', ++ action='store', ++ help='QOM path', ++ ) ++ ++ def __init__(self, args: argparse.Namespace): ++ super().__init__(args) ++ self.path = args.path ++ ++ def run(self) -> int: ++ rsp = self.qom_list(self.path) ++ for item in rsp: ++ if item.child: ++ print(f"{item.name}/") ++ elif item.link: ++ print(f"@{item.name}/") ++ else: ++ print(item.name) ++ return 0 ++ ++ ++class QOMTree(QOMCommand): ++ """ ++ QOM Command - Show the full tree below a given path. ++ ++ usage: qom-tree [-h] [--socket SOCKET] [] ++ ++ Show QOM tree from a given path ++ ++ positional arguments: ++ QOM path ++ ++ optional arguments: ++ -h, --help show this help message and exit ++ --socket SOCKET, -s SOCKET ++ QMP socket path or address (addr:port). May also be ++ set via QMP_SOCKET environment variable. ++ """ ++ name = 'tree' ++ help = 'Show QOM tree from a given path' ++ ++ @classmethod ++ def configure_parser(cls, parser: argparse.ArgumentParser) -> None: ++ super().configure_parser(parser) ++ parser.add_argument( ++ 'path', ++ metavar='', ++ action='store', ++ help='QOM path', ++ nargs='?', ++ default='/' ++ ) ++ ++ def __init__(self, args: argparse.Namespace): ++ super().__init__(args) ++ self.path = args.path ++ ++ def _list_node(self, path: str) -> None: ++ print(path) ++ items = self.qom_list(path) ++ for item in items: ++ if item.child: ++ continue ++ try: ++ rsp = self.qmp.command('qom-get', path=path, ++ property=item.name) ++ print(f" {item.name}: {rsp} ({item.type})") ++ except QMPResponseError as err: ++ print(f" {item.name}: ({item.type})") ++ print('') ++ for item in items: ++ if not item.child: ++ continue ++ if path == '/': ++ path = '' ++ self._list_node(f"{path}/{item.name}") ++ ++ def run(self) -> int: ++ self._list_node(self.path) ++ return 0 ++ ++ ++def main() -> int: ++ """QOM script main entry point.""" ++ parser = argparse.ArgumentParser( ++ description='Query and manipulate QOM data' ++ ) ++ subparsers = parser.add_subparsers( ++ title='QOM commands', ++ dest='command' ++ ) ++ ++ for command in QOMCommand.__subclasses__(): ++ command.register(subparsers) ++ ++ args = parser.parse_args() ++ ++ if args.command is None: ++ parser.error('Command not specified.') ++ return 1 ++ ++ cmd_class = args.cmd_class ++ assert isinstance(cmd_class, type(QOMCommand)) ++ return cmd_class.command_runner(args) +diff --git a/python/qemu/utils/qom_common.py b/python/qemu/utils/qom_common.py +new file mode 100644 +index 0000000000000000000000000000000000000000..a59ae1a2a1883cb4d89b0e44507c5001f44357a0 +--- /dev/null ++++ b/python/qemu/utils/qom_common.py +@@ -0,0 +1,178 @@ ++""" ++QOM Command abstractions. ++""" ++## ++# Copyright John Snow 2020, for Red Hat, Inc. ++# Copyright IBM, Corp. 2011 ++# ++# Authors: ++# John Snow ++# 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. ++# ++# Based on ./scripts/qmp/qom-[set|get|tree|list] ++## ++ ++import argparse ++import os ++import sys ++from typing import ( ++ Any, ++ Dict, ++ List, ++ Optional, ++ Type, ++ TypeVar, ++) ++ ++from . import QEMUMonitorProtocol, QMPError ++ ++ ++# The following is needed only for a type alias. ++Subparsers = argparse._SubParsersAction # pylint: disable=protected-access ++ ++ ++class ObjectPropertyInfo: ++ """ ++ Represents the return type from e.g. qom-list. ++ """ ++ def __init__(self, name: str, type_: str, ++ description: Optional[str] = None, ++ default_value: Optional[object] = None): ++ self.name = name ++ self.type = type_ ++ self.description = description ++ self.default_value = default_value ++ ++ @classmethod ++ def make(cls, value: Dict[str, Any]) -> 'ObjectPropertyInfo': ++ """ ++ Build an ObjectPropertyInfo from a Dict with an unknown shape. ++ """ ++ assert value.keys() >= {'name', 'type'} ++ assert value.keys() <= {'name', 'type', 'description', 'default-value'} ++ return cls(value['name'], value['type'], ++ value.get('description'), ++ value.get('default-value')) ++ ++ @property ++ def child(self) -> bool: ++ """Is this property a child property?""" ++ return self.type.startswith('child<') ++ ++ @property ++ def link(self) -> bool: ++ """Is this property a link property?""" ++ return self.type.startswith('link<') ++ ++ ++CommandT = TypeVar('CommandT', bound='QOMCommand') ++ ++ ++class QOMCommand: ++ """ ++ Represents a QOM sub-command. ++ ++ :param args: Parsed arguments, as returned from parser.parse_args. ++ """ ++ name: str ++ help: str ++ ++ def __init__(self, args: argparse.Namespace): ++ if args.socket is None: ++ raise QMPError("No QMP socket path or address given") ++ self.qmp = QEMUMonitorProtocol( ++ QEMUMonitorProtocol.parse_address(args.socket) ++ ) ++ self.qmp.connect() ++ ++ @classmethod ++ def register(cls, subparsers: Subparsers) -> None: ++ """ ++ Register this command with the argument parser. ++ ++ :param subparsers: argparse subparsers object, from "add_subparsers". ++ """ ++ subparser = subparsers.add_parser(cls.name, help=cls.help, ++ description=cls.help) ++ cls.configure_parser(subparser) ++ ++ @classmethod ++ def configure_parser(cls, parser: argparse.ArgumentParser) -> None: ++ """ ++ Configure a parser with this command's arguments. ++ ++ :param parser: argparse parser or subparser object. ++ """ ++ default_path = os.environ.get('QMP_SOCKET') ++ parser.add_argument( ++ '--socket', '-s', ++ dest='socket', ++ action='store', ++ help='QMP socket path or address (addr:port).' ++ ' May also be set via QMP_SOCKET environment variable.', ++ default=default_path ++ ) ++ parser.set_defaults(cmd_class=cls) ++ ++ @classmethod ++ def add_path_prop_arg(cls, parser: argparse.ArgumentParser) -> None: ++ """ ++ Add the . positional argument to this command. ++ ++ :param parser: The parser to add the argument to. ++ """ ++ parser.add_argument( ++ 'path_prop', ++ metavar='.', ++ action='store', ++ help="QOM path and property, separated by a period '.'" ++ ) ++ ++ def run(self) -> int: ++ """ ++ Run this command. ++ ++ :return: 0 on success, 1 otherwise. ++ """ ++ raise NotImplementedError ++ ++ def qom_list(self, path: str) -> List[ObjectPropertyInfo]: ++ """ ++ :return: a strongly typed list from the 'qom-list' command. ++ """ ++ rsp = self.qmp.command('qom-list', path=path) ++ # qom-list returns List[ObjectPropertyInfo] ++ assert isinstance(rsp, list) ++ return [ObjectPropertyInfo.make(x) for x in rsp] ++ ++ @classmethod ++ def command_runner( ++ cls: Type[CommandT], ++ args: argparse.Namespace ++ ) -> int: ++ """ ++ Run a fully-parsed subcommand, with error-handling for the CLI. ++ ++ :return: The return code from `run()`. ++ """ ++ try: ++ cmd = cls(args) ++ return cmd.run() ++ except QMPError as err: ++ print(f"{type(err).__name__}: {err!s}", file=sys.stderr) ++ return -1 ++ ++ @classmethod ++ def entry_point(cls) -> int: ++ """ ++ Build this command's parser, parse arguments, and run the command. ++ ++ :return: `run`'s return code. ++ """ ++ parser = argparse.ArgumentParser(description=cls.help) ++ cls.configure_parser(parser) ++ args = parser.parse_args() ++ return cls.command_runner(args) +diff --git a/python/qemu/utils/qom_fuse.py b/python/qemu/utils/qom_fuse.py +new file mode 100644 +index 0000000000000000000000000000000000000000..43f4671fdb18c6aa1b11df9694855167fced8f10 +--- /dev/null ++++ b/python/qemu/utils/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/python/setup.cfg b/python/setup.cfg +index 4f4f20571f304507e20ce16cee66..91ccef7e8fd85d0d6d3d86adbc8d 100644 +--- a/python/setup.cfg ++++ b/python/setup.cfg +@@ -60,13 +60,13 @@ tui = + + [options.entry_points] + console_scripts = +- qom = qemu.qmp.qom:main +- qom-set = qemu.qmp.qom:QOMSet.entry_point +- qom-get = qemu.qmp.qom:QOMGet.entry_point +- qom-list = qemu.qmp.qom:QOMList.entry_point +- qom-tree = qemu.qmp.qom:QOMTree.entry_point +- qom-fuse = qemu.qmp.qom_fuse:QOMFuse.entry_point [fuse] +- qemu-ga-client = qemu.qmp.qemu_ga_client:main ++ qom = qemu.utils.qom:main ++ qom-set = qemu.utils.qom:QOMSet.entry_point ++ qom-get = qemu.utils.qom:QOMGet.entry_point ++ qom-list = qemu.utils.qom:QOMList.entry_point ++ qom-tree = qemu.utils.qom:QOMTree.entry_point ++ qom-fuse = qemu.utils.qom_fuse:QOMFuse.entry_point [fuse] ++ qemu-ga-client = qemu.utils.qemu_ga_client:main + qmp-shell = qemu.qmp.qmp_shell:main + aqmp-tui = qemu.aqmp.aqmp_tui:main [tui] + +@@ -80,7 +80,7 @@ python_version = 3.6 + warn_unused_configs = True + namespace_packages = True + +-[mypy-qemu.qmp.qom_fuse] ++[mypy-qemu.utils.qom_fuse] + # fusepy has no type stubs: + allow_subclassing_any = True + +diff --git a/scripts/qmp/qemu-ga-client b/scripts/qmp/qemu-ga-client +index 102fd2cad93fcc76e428e841241a..56edd0234a62cc25a069bb6a65dc 100755 +--- a/scripts/qmp/qemu-ga-client ++++ b/scripts/qmp/qemu-ga-client +@@ -4,7 +4,7 @@ import os + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) +-from qemu.qmp import qemu_ga_client ++from qemu.utils import qemu_ga_client + + + if __name__ == '__main__': +diff --git a/scripts/qmp/qom-fuse b/scripts/qmp/qom-fuse +index a58c8ef9793b387eeaec6777a5c3..d453807b273f5576c90c8809df2b 100755 +--- a/scripts/qmp/qom-fuse ++++ b/scripts/qmp/qom-fuse +@@ -4,7 +4,7 @@ import os + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) +-from qemu.qmp.qom_fuse import QOMFuse ++from qemu.utils.qom_fuse import QOMFuse + + + if __name__ == '__main__': +diff --git a/scripts/qmp/qom-get b/scripts/qmp/qom-get +index e4f3e0c01381a0240379016340d4..04ebe052e82a97bbadde1dfc76c4 100755 +--- a/scripts/qmp/qom-get ++++ b/scripts/qmp/qom-get +@@ -4,7 +4,7 @@ import os + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) +-from qemu.qmp.qom import QOMGet ++from qemu.utils.qom import QOMGet + + + if __name__ == '__main__': +diff --git a/scripts/qmp/qom-list b/scripts/qmp/qom-list +index 7a071a54e1e7785142b2dc262ac4..853b85a8d3fc4e032f40747c3c08 100755 +--- a/scripts/qmp/qom-list ++++ b/scripts/qmp/qom-list +@@ -4,7 +4,7 @@ import os + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) +-from qemu.qmp.qom import QOMList ++from qemu.utils.qom import QOMList + + + if __name__ == '__main__': +diff --git a/scripts/qmp/qom-set b/scripts/qmp/qom-set +index 9ca9e2ba106b158e0926184533b9..06820feec424ecd30235c6ed9006 100755 +--- a/scripts/qmp/qom-set ++++ b/scripts/qmp/qom-set +@@ -4,7 +4,7 @@ import os + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) +-from qemu.qmp.qom import QOMSet ++from qemu.utils.qom import QOMSet + + + if __name__ == '__main__': +diff --git a/scripts/qmp/qom-tree b/scripts/qmp/qom-tree +index 7d0ccca3a4dd87edb92b1ac7a6e7..760e172277e9b66ff2a6d770b526 100755 +--- a/scripts/qmp/qom-tree ++++ b/scripts/qmp/qom-tree +@@ -4,7 +4,7 @@ import os + import sys + + sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) +-from qemu.qmp.qom import QOMTree ++from qemu.utils.qom import QOMTree + + + if __name__ == '__main__': diff --git a/python-qmp-switch-qmp-shell-to-AQMP.patch b/python-qmp-switch-qmp-shell-to-AQMP.patch new file mode 100644 index 00000000..198a3020 --- /dev/null +++ b/python-qmp-switch-qmp-shell-to-AQMP.patch @@ -0,0 +1,121 @@ +From: John Snow +Date: Mon, 10 Jan 2022 18:28:53 -0500 +Subject: python/qmp: switch qmp-shell to AQMP + +Git-commit: f3efd12930f34b9724e15d8fd2ff56a97b67219d + +We have a replacement for async QMP, but it doesn't have feature parity +yet. For now, then, port the old tool onto the new backend. + +Signed-off-by: John Snow +Reviewed-by: Vladimir Sementsov-Ogievskiy +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/legacy.py | 3 +++ + python/qemu/qmp/qmp_shell.py | 31 +++++++++++++++++-------------- + 2 files changed, 20 insertions(+), 14 deletions(-) + +diff --git a/python/qemu/aqmp/legacy.py b/python/qemu/aqmp/legacy.py +index 27df22818a76190e872f08c0852e..0890f95b16875ecb815ed4560bc7 100644 +--- a/python/qemu/aqmp/legacy.py ++++ b/python/qemu/aqmp/legacy.py +@@ -22,6 +22,9 @@ from .protocol import Runstate, SocketAddrT + from .qmp_client import QMPClient + + ++# (Temporarily) Re-export QMPBadPortError ++QMPBadPortError = qemu.qmp.QMPBadPortError ++ + #: QMPMessage is an entire QMP message of any kind. + QMPMessage = Dict[str, Any] + +diff --git a/python/qemu/qmp/qmp_shell.py b/python/qemu/qmp/qmp_shell.py +index e7d7eb18f19cae7ac185b333013e..d11bf54b00e5d56616ae57be0006 100644 +--- a/python/qemu/qmp/qmp_shell.py ++++ b/python/qemu/qmp/qmp_shell.py +@@ -95,8 +95,13 @@ from typing import ( + Sequence, + ) + +-from qemu import qmp +-from qemu.qmp import QMPMessage ++from qemu.aqmp import ConnectError, QMPError, SocketAddrT ++from qemu.aqmp.legacy import ( ++ QEMUMonitorProtocol, ++ QMPBadPortError, ++ QMPMessage, ++ QMPObject, ++) + + + LOG = logging.getLogger(__name__) +@@ -125,7 +130,7 @@ class QMPCompleter: + return None + + +-class QMPShellError(qmp.QMPError): ++class QMPShellError(QMPError): + """ + QMP Shell Base error class. + """ +@@ -153,7 +158,7 @@ class FuzzyJSON(ast.NodeTransformer): + return node + + +-class QMPShell(qmp.QEMUMonitorProtocol): ++class QMPShell(QEMUMonitorProtocol): + """ + QMPShell provides a basic readline-based QMP shell. + +@@ -161,7 +166,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): + :param pretty: Pretty-print QMP messages. + :param verbose: Echo outgoing QMP messages to console. + """ +- def __init__(self, address: qmp.SocketAddrT, ++ def __init__(self, address: SocketAddrT, + pretty: bool = False, verbose: bool = False): + super().__init__(address) + self._greeting: Optional[QMPMessage] = None +@@ -237,7 +242,7 @@ class QMPShell(qmp.QEMUMonitorProtocol): + + def _cli_expr(self, + tokens: Sequence[str], +- parent: qmp.QMPObject) -> None: ++ parent: QMPObject) -> None: + for arg in tokens: + (key, sep, val) = arg.partition('=') + if sep != '=': +@@ -403,7 +408,7 @@ class HMPShell(QMPShell): + :param pretty: Pretty-print QMP messages. + :param verbose: Echo outgoing QMP messages to console. + """ +- def __init__(self, address: qmp.SocketAddrT, ++ def __init__(self, address: SocketAddrT, + pretty: bool = False, verbose: bool = False): + super().__init__(address, pretty, verbose) + self._cpu_index = 0 +@@ -512,19 +517,17 @@ def main() -> None: + + try: + address = shell_class.parse_address(args.qmp_server) +- except qmp.QMPBadPortError: ++ except 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}") ++ except ConnectError as err: ++ if isinstance(err.exc, OSError): ++ die(f"Couldn't connect to {args.qmp_server}: {err!s}") ++ die(str(err)) + + for _ in qemu.repl(): + pass diff --git a/python-support-recording-QMP-session-to-.patch b/python-support-recording-QMP-session-to-.patch new file mode 100644 index 00000000..58088a36 --- /dev/null +++ b/python-support-recording-QMP-session-to-.patch @@ -0,0 +1,184 @@ +From: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= +Date: Fri, 28 Jan 2022 16:11:57 +0000 +Subject: python: support recording QMP session to a file +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Git-commit: 5c66d7d8de9a00460199669d26cd83fba135bee5 + +When running QMP commands with very large response payloads, it is often +not easy to spot the info you want. If we can save the response to a +file then tools like 'grep' or 'jq' can be used to extract information. + +For convenience of processing, we merge the QMP command and response +dictionaries together: + + { + "arguments": {}, + "execute": "query-kvm", + "return": { + "enabled": false, + "present": true + } + } + +Example usage + + $ ./scripts/qmp/qmp-shell-wrap -l q.log -p -- ./build/qemu-system-x86_64 -display none + Welcome to the QMP low-level shell! + Connected + (QEMU) query-kvm + { + "return": { + "enabled": false, + "present": true + } + } + (QEMU) query-mice + { + "return": [ + { + "absolute": false, + "current": true, + "index": 2, + "name": "QEMU PS/2 Mouse" + } + ] + } + + $ jq --slurp '. | to_entries[] | select(.value.execute == "query-kvm") | + .value.return.enabled' < q.log + false + +Reviewed-by: Philippe Mathieu-Daudé +Signed-off-by: Daniel P. Berrangé +Message-id: 20220128161157.36261-3-berrange@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/qemu/aqmp/qmp_shell.py | 29 ++++++++++++++++++++++------- + python/setup.cfg | 3 +++ + 2 files changed, 25 insertions(+), 7 deletions(-) + +diff --git a/python/qemu/aqmp/qmp_shell.py b/python/qemu/aqmp/qmp_shell.py +index c60df787fcd50bf8a0109e5f5cd3..35691494d0a88070bcf1ad691699 100644 +--- a/python/qemu/aqmp/qmp_shell.py ++++ b/python/qemu/aqmp/qmp_shell.py +@@ -89,6 +89,7 @@ import readline + from subprocess import Popen + import sys + from typing import ( ++ IO, + Iterator, + List, + NoReturn, +@@ -170,7 +171,8 @@ class QMPShell(QEMUMonitorProtocol): + def __init__(self, address: SocketAddrT, + pretty: bool = False, + verbose: bool = False, +- server: bool = False): ++ server: bool = False, ++ logfile: Optional[str] = None): + super().__init__(address, server=server) + self._greeting: Optional[QMPMessage] = None + self._completer = QMPCompleter() +@@ -180,6 +182,10 @@ class QMPShell(QEMUMonitorProtocol): + '.qmp-shell_history') + self.pretty = pretty + self.verbose = verbose ++ self.logfile = None ++ ++ if logfile is not None: ++ self.logfile = open(logfile, "w", encoding='utf-8') + + def close(self) -> None: + # Hook into context manager of parent to save shell history. +@@ -320,11 +326,11 @@ class QMPShell(QEMUMonitorProtocol): + self._cli_expr(cmdargs[1:], qmpcmd['arguments']) + return qmpcmd + +- def _print(self, qmp_message: object) -> None: ++ def _print(self, qmp_message: object, fh: IO[str] = sys.stdout) -> None: + jsobj = json.dumps(qmp_message, + indent=4 if self.pretty else None, + sort_keys=self.pretty) +- print(str(jsobj)) ++ print(str(jsobj), file=fh) + + def _execute_cmd(self, cmdline: str) -> bool: + try: +@@ -347,6 +353,9 @@ class QMPShell(QEMUMonitorProtocol): + print('Disconnected') + return False + self._print(resp) ++ if self.logfile is not None: ++ cmd = {**qmpcmd, **resp} ++ self._print(cmd, fh=self.logfile) + return True + + def connect(self, negotiate: bool = True) -> None: +@@ -414,8 +423,9 @@ class HMPShell(QMPShell): + def __init__(self, address: SocketAddrT, + pretty: bool = False, + verbose: bool = False, +- server: bool = False): +- super().__init__(address, pretty, verbose, server) ++ server: bool = False, ++ logfile: Optional[str] = None): ++ super().__init__(address, pretty, verbose, server, logfile) + self._cpu_index = 0 + + def _cmd_completion(self) -> None: +@@ -508,6 +518,8 @@ def main() -> None: + help='Verbose (echo commands sent and received)') + parser.add_argument('-p', '--pretty', action='store_true', + help='Pretty-print JSON') ++ parser.add_argument('-l', '--logfile', ++ help='Save log of all QMP messages to PATH') + + default_server = os.environ.get('QMP_SOCKET') + parser.add_argument('qmp_server', action='store', +@@ -526,7 +538,7 @@ def main() -> None: + 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: ++ with shell_class(address, args.pretty, args.verbose, args.logfile) as qemu: + try: + qemu.connect(negotiate=not args.skip_negotiation) + except ConnectError as err: +@@ -550,6 +562,8 @@ def main_wrap() -> None: + help='Verbose (echo commands sent and received)') + parser.add_argument('-p', '--pretty', action='store_true', + help='Pretty-print JSON') ++ parser.add_argument('-l', '--logfile', ++ help='Save log of all QMP messages to PATH') + + parser.add_argument('command', nargs=argparse.REMAINDER, + help='QEMU command line to invoke') +@@ -574,7 +588,8 @@ def main_wrap() -> None: + return # pycharm doesn't know error() is noreturn + + try: +- with shell_class(address, args.pretty, args.verbose, True) as qemu: ++ with shell_class(address, args.pretty, args.verbose, ++ True, args.logfile) as qemu: + with Popen(cmd): + + try: +diff --git a/python/setup.cfg b/python/setup.cfg +index bec54e8b0d663191e2b7afbfa350..241f243e8b94417f9b032c41576b 100644 +--- a/python/setup.cfg ++++ b/python/setup.cfg +@@ -114,7 +114,10 @@ ignore_missing_imports = True + # no Warning level messages displayed, use "--disable=all --enable=classes + # --disable=W". + disable=consider-using-f-string, ++ consider-using-with, ++ too-many-arguments, + too-many-function-args, # mypy handles this with less false positives. ++ too-many-instance-attributes, + no-member, # mypy also handles this better. + + [pylint.basic] diff --git a/python-upgrade-mypy-to-0.780.patch b/python-upgrade-mypy-to-0.780.patch new file mode 100644 index 00000000..f7b9618e --- /dev/null +++ b/python-upgrade-mypy-to-0.780.patch @@ -0,0 +1,232 @@ +From: John Snow +Date: Mon, 31 Jan 2022 23:11:33 -0500 +Subject: python: upgrade mypy to 0.780 + +Git-commit: 74a1505d279897d2a448c876820a33cbe1f0f22e + +We need a slightly newer version of mypy in order to use some features +of the asyncio server functions in the next commit. + +(Note: pipenv is not really suited to upgrading individual packages; I +need to replace this tool with something better for the task. For now, +the miscellaneous updates not related to the mypy upgrade are simply +beyond my control. It's on my list to take care of soon.) + +Signed-off-by: John Snow +Reviewed-by: Kevin Wolf +Message-id: 20220201041134.1237016-4-jsnow@redhat.com +Signed-off-by: John Snow +Signed-off-by: Li Zhang +--- + python/Pipfile.lock | 66 ++++++++++++++++++++++++++------------------- + python/setup.cfg | 2 +- + 2 files changed, 40 insertions(+), 28 deletions(-) + +diff --git a/python/Pipfile.lock b/python/Pipfile.lock +index d2a7dbd88be19fd6db0baa083d8a..ce46404ce0840c693d3c982674ac 100644 +--- a/python/Pipfile.lock ++++ b/python/Pipfile.lock +@@ -1,7 +1,7 @@ + { + "_meta": { + "hash": { +- "sha256": "784b327272db32403d5a488507853b5afba850ba26a5948e5b6a90c1baef2d9c" ++ "sha256": "f1a25654d884a5b450e38d78b1f2e3ebb9073e421cc4358d4bbb83ac251a5670" + }, + "pipfile-spec": 6, + "requires": { +@@ -34,7 +34,7 @@ + "sha256:09bdb456e02564731f8b5957cdd0c98a7f01d2db5e90eb1d794c353c28bfd705", + "sha256:6a8a51f64dae307f6e0c9db752b66a7951e282389d8362cc1d39a56f3feeb31d" + ], +- "markers": "python_version ~= '3.6'", ++ "index": "pypi", + "version": "==2.6.0" + }, + "avocado-framework": { +@@ -50,6 +50,7 @@ + "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736", + "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c" + ], ++ "index": "pypi", + "version": "==0.3.2" + }, + "filelock": { +@@ -57,6 +58,7 @@ + "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59", + "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836" + ], ++ "index": "pypi", + "version": "==3.0.12" + }, + "flake8": { +@@ -88,7 +90,7 @@ + "sha256:54161657e8ffc76596c4ede7080ca68cb02962a2e074a2586b695a93a925d36e", + "sha256:e962bff7440364183203d179d7ae9ad90cb1f2b74dcb84300e88ecc42dca3351" + ], +- "markers": "python_version < '3.7'", ++ "index": "pypi", + "version": "==5.1.4" + }, + "isort": { +@@ -124,7 +126,7 @@ + "sha256:ed361bb83436f117f9917d282a456f9e5009ea12fd6de8742d1a4752c3017e93", + "sha256:f5144c75445ae3ca2057faac03fda5a902eff196702b0a24daf1d6ce0650514b" + ], +- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", ++ "index": "pypi", + "version": "==1.6.0" + }, + "mccabe": { +@@ -136,23 +138,23 @@ + }, + "mypy": { + "hashes": [ +- "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2", +- "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1", +- "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164", +- "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761", +- "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce", +- "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27", +- "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754", +- "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae", +- "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9", +- "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600", +- "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65", +- "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8", +- "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913", +- "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3" ++ "sha256:00cb1964a7476e871d6108341ac9c1a857d6bd20bf5877f4773ac5e9d92cd3cd", ++ "sha256:127de5a9b817a03a98c5ae8a0c46a20dc44442af6dcfa2ae7f96cb519b312efa", ++ "sha256:1f3976a945ad7f0a0727aafdc5651c2d3278e3c88dee94e2bf75cd3386b7b2f4", ++ "sha256:2f8c098f12b402c19b735aec724cc9105cc1a9eea405d08814eb4b14a6fb1a41", ++ "sha256:4ef13b619a289aa025f2273e05e755f8049bb4eaba6d703a425de37d495d178d", ++ "sha256:5d142f219bf8c7894dfa79ebfb7d352c4c63a325e75f10dfb4c3db9417dcd135", ++ "sha256:62eb5dd4ea86bda8ce386f26684f7f26e4bfe6283c9f2b6ca6d17faf704dcfad", ++ "sha256:64c36eb0936d0bfb7d8da49f92c18e312ad2e3ed46e5548ae4ca997b0d33bd59", ++ "sha256:75eed74d2faf2759f79c5f56f17388defd2fc994222312ec54ee921e37b31ad4", ++ "sha256:974bebe3699b9b46278a7f076635d219183da26e1a675c1f8243a69221758273", ++ "sha256:a5e5bb12b7982b179af513dddb06fca12285f0316d74f3964078acbfcf4c68f2", ++ "sha256:d31291df31bafb997952dc0a17ebb2737f802c754aed31dd155a8bfe75112c57", ++ "sha256:d3b4941de44341227ece1caaf5b08b23e42ad4eeb8b603219afb11e9d4cfb437", ++ "sha256:eadb865126da4e3c4c95bdb47fe1bb087a3e3ea14d39a3b13224b8a4d9f9a102" + ], + "index": "pypi", +- "version": "==0.770" ++ "version": "==0.780" + }, + "mypy-extensions": { + "hashes": [ +@@ -166,7 +168,7 @@ + "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", + "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + ], +- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", ++ "index": "pypi", + "version": "==20.9" + }, + "pluggy": { +@@ -174,7 +176,7 @@ + "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", + "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + ], +- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", ++ "index": "pypi", + "version": "==0.13.1" + }, + "py": { +@@ -182,7 +184,7 @@ + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], +- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", ++ "index": "pypi", + "version": "==1.10.0" + }, + "pycodestyle": { +@@ -205,7 +207,7 @@ + "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", + "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + ], +- "markers": "python_version >= '3.5'", ++ "index": "pypi", + "version": "==2.9.0" + }, + "pylint": { +@@ -221,13 +223,21 @@ + "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1", + "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b" + ], +- "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", ++ "index": "pypi", + "version": "==2.4.7" + }, + "qemu": { + "editable": true, + "path": "." + }, ++ "setuptools": { ++ "hashes": [ ++ "sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373", ++ "sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e" ++ ], ++ "markers": "python_version >= '3.6'", ++ "version": "==59.6.0" ++ }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", +@@ -294,19 +304,21 @@ + "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", + "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + ], +- "markers": "python_version < '3.8'", ++ "index": "pypi", + "version": "==3.10.0.0" + }, + "urwid": { + "hashes": [ + "sha256:588bee9c1cb208d0906a9f73c613d2bd32c3ed3702012f51efe318a3f2127eae" + ], ++ "index": "pypi", + "version": "==2.1.2" + }, + "urwid-readline": { + "hashes": [ + "sha256:018020cbc864bb5ed87be17dc26b069eae2755cb29f3a9c569aac3bded1efaf4" + ], ++ "index": "pypi", + "version": "==0.13" + }, + "virtualenv": { +@@ -314,7 +326,7 @@ + "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467", + "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76" + ], +- "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", ++ "index": "pypi", + "version": "==20.4.7" + }, + "wrapt": { +@@ -328,7 +340,7 @@ + "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76", + "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098" + ], +- "markers": "python_version < '3.10'", ++ "index": "pypi", + "version": "==3.4.1" + } + } +diff --git a/python/setup.cfg b/python/setup.cfg +index 417e937839b85eecd752b29ad7df..4f4f20571f304507e20ce16cee66 100644 +--- a/python/setup.cfg ++++ b/python/setup.cfg +@@ -41,7 +41,7 @@ devel = + flake8 >= 3.6.0 + fusepy >= 2.0.4 + isort >= 5.1.2 +- mypy >= 0.770 ++ mypy >= 0.780 + pylint >= 2.8.0 + tox >= 3.18.0 + urwid >= 2.1.2 diff --git a/qemu.changes b/qemu.changes index e03c8a17..efce0794 100644 --- a/qemu.changes +++ b/qemu.changes @@ -1,3 +1,47 @@ +------------------------------------------------------------------- +Tue Apr 5 08:54:51 UTC 2022 - Li Zhang + +- Backport aqmp patches from upstream which can fix iotest issues +* Patches added: + python-aqmp-add-__del__-method-to-legacy.patch + python-aqmp-add-_session_guard.patch + python-aqmp-add-SocketAddrT-to-package-r.patch + python-aqmp-add-socket-bind-step-to-lega.patch + python-aqmp-add-start_server-and-accept-.patch + python-aqmp-copy-type-definitions-from-q.patch + python-aqmp-drop-_bind_hack.patch + python-aqmp-fix-docstring-typo.patch + python-aqmp-Fix-negotiation-with-pre-oob.patch + python-aqmp-fix-race-condition-in-legacy.patch + Python-aqmp-fix-type-definitions-for-myp.patch + python-aqmp-handle-asyncio.TimeoutError-.patch + python-aqmp-refactor-_do_accept-into-two.patch + python-aqmp-remove-_new_session-and-_est.patch + python-aqmp-rename-accept-to-start_serve.patch + python-aqmp-rename-AQMPError-to-QMPError.patch + python-aqmp-split-_client_connected_cb-o.patch + python-aqmp-squelch-pylint-warning-for-t.patch + python-aqmp-stop-the-server-during-disco.patch + python-introduce-qmp-shell-wrap-convenie.patch + python-machine-raise-VMLaunchFailure-exc.patch + python-move-qmp-shell-under-the-AQMP-pac.patch + python-move-qmp-utilities-to-python-qemu.patch + python-qmp-switch-qmp-shell-to-AQMP.patch + python-support-recording-QMP-session-to-.patch + python-upgrade-mypy-to-0.780.patch + +------------------------------------------------------------------- +Tue Apr 5 08:24:58 UTC 2022 - Li Zhang + +- Drop the patches which are workaround to fix iotest issues +* Patches dropped: + Revert-python-iotests-replace-qmp-with-a.patch + Revert-python-machine-add-instance-disam.patch + Revert-python-machine-add-sock_dir-prope.patch + Revert-python-machine-handle-fast-QEMU-t.patch + Revert-python-machine-move-more-variable.patch + Revert-python-machine-remove-_remove_mon.patch + ------------------------------------------------------------------- Thu Mar 31 10:35:44 UTC 2022 - Li Zhang diff --git a/qemu.spec b/qemu.spec index 9349ab17..a283d829 100644 --- a/qemu.spec +++ b/qemu.spec @@ -220,16 +220,36 @@ Patch00073: tests-qemu-iotests-040-Skip-TestCommitWi.patch Patch00074: tests-qemu-iotests-testrunner-Quote-case.patch Patch00075: Fix-the-module-building-problem-for-s390.patch Patch00076: scsi-generic-check-for-additional-SG_IO-.patch -Patch00077: Revert-python-machine-handle-fast-QEMU-t.patch -Patch00078: Revert-python-machine-move-more-variable.patch -Patch00079: Revert-python-machine-add-instance-disam.patch -Patch00080: Revert-python-machine-remove-_remove_mon.patch -Patch00081: Revert-python-machine-add-sock_dir-prope.patch -Patch00082: Revert-python-iotests-replace-qmp-with-a.patch -Patch00083: hw-nvme-fix-CVE-2021-3929.patch -Patch00084: numa-Enable-numa-for-SGX-EPC-sections.patch -Patch00085: numa-Support-SGX-numa-in-the-monitor-and.patch -Patch00086: doc-Add-the-SGX-numa-description.patch +Patch00077: hw-nvme-fix-CVE-2021-3929.patch +Patch00078: numa-Enable-numa-for-SGX-EPC-sections.patch +Patch00079: numa-Support-SGX-numa-in-the-monitor-and.patch +Patch00080: doc-Add-the-SGX-numa-description.patch +Patch00081: python-aqmp-Fix-negotiation-with-pre-oob.patch +Patch00082: python-machine-raise-VMLaunchFailure-exc.patch +Patch00083: python-upgrade-mypy-to-0.780.patch +Patch00084: Python-aqmp-fix-type-definitions-for-myp.patch +Patch00085: python-aqmp-add-__del__-method-to-legacy.patch +Patch00086: python-aqmp-copy-type-definitions-from-q.patch +Patch00087: python-aqmp-handle-asyncio.TimeoutError-.patch +Patch00088: python-aqmp-add-SocketAddrT-to-package-r.patch +Patch00089: python-aqmp-fix-docstring-typo.patch +Patch00090: python-aqmp-rename-AQMPError-to-QMPError.patch +Patch00091: python-qmp-switch-qmp-shell-to-AQMP.patch +Patch00092: python-aqmp-add-socket-bind-step-to-lega.patch +Patch00093: python-move-qmp-utilities-to-python-qemu.patch +Patch00094: python-move-qmp-shell-under-the-AQMP-pac.patch +Patch00095: python-introduce-qmp-shell-wrap-convenie.patch +Patch00096: python-support-recording-QMP-session-to-.patch +Patch00097: python-aqmp-add-_session_guard.patch +Patch00098: python-aqmp-rename-accept-to-start_serve.patch +Patch00099: python-aqmp-remove-_new_session-and-_est.patch +Patch00100: python-aqmp-split-_client_connected_cb-o.patch +Patch00101: python-aqmp-squelch-pylint-warning-for-t.patch +Patch00102: python-aqmp-refactor-_do_accept-into-two.patch +Patch00103: python-aqmp-stop-the-server-during-disco.patch +Patch00104: python-aqmp-add-start_server-and-accept-.patch +Patch00105: python-aqmp-fix-race-condition-in-legacy.patch +Patch00106: python-aqmp-drop-_bind_hack.patch # Patches applied in roms/seabios/: Patch01000: seabios-use-python2-explicitly-as-needed.patch Patch01001: seabios-switch-to-python3-as-needed.patch @@ -1239,6 +1259,26 @@ This package records qemu testsuite results and represents successful testing. %patch00084 -p1 %patch00085 -p1 %patch00086 -p1 +%patch00087 -p1 +%patch00088 -p1 +%patch00089 -p1 +%patch00090 -p1 +%patch00091 -p1 +%patch00092 -p1 +%patch00093 -p1 +%patch00094 -p1 +%patch00095 -p1 +%patch00096 -p1 +%patch00097 -p1 +%patch00098 -p1 +%patch00099 -p1 +%patch00100 -p1 +%patch00101 -p1 +%patch00102 -p1 +%patch00103 -p1 +%patch00104 -p1 +%patch00105 -p1 +%patch00106 -p1 %patch01000 -p1 %patch01001 -p1 %patch01002 -p1