]>
Commit | Line | Data |
---|---|---|
f122be60 | 1 | """ |
b0654f4f | 2 | (Legacy) Sync QMP Wrapper |
f122be60 | 3 | |
b0654f4f JS |
4 | This module provides the `QEMUMonitorProtocol` class, which is a |
5 | synchronous wrapper around `QMPClient`. | |
6 | ||
7 | Its design closely resembles that of the original QEMUMonitorProtocol | |
8 | class, originally written by Luiz Capitulino. It is provided here for | |
9 | compatibility with scripts inside the QEMU source tree that expect the | |
10 | old interface. | |
f122be60 JS |
11 | """ |
12 | ||
380fc8f3 JS |
13 | # |
14 | # Copyright (C) 2009-2022 Red Hat Inc. | |
15 | # | |
16 | # Authors: | |
17 | # Luiz Capitulino <lcapitulino@redhat.com> | |
18 | # John Snow <jsnow@redhat.com> | |
19 | # | |
20 | # This work is licensed under the terms of the GNU GPL, version 2. See | |
21 | # the COPYING file in the top-level directory. | |
22 | # | |
23 | ||
f122be60 | 24 | import asyncio |
0c78ebf7 | 25 | from types import TracebackType |
f122be60 | 26 | from typing import ( |
0e6bfd8b | 27 | Any, |
f122be60 | 28 | Awaitable, |
0e6bfd8b | 29 | Dict, |
f122be60 JS |
30 | List, |
31 | Optional, | |
0c78ebf7 | 32 | Type, |
f122be60 JS |
33 | TypeVar, |
34 | Union, | |
35 | ) | |
36 | ||
6e7751dc | 37 | from .error import QMPError |
0e6bfd8b | 38 | from .protocol import Runstate, SocketAddrT |
f122be60 JS |
39 | from .qmp_client import QMPClient |
40 | ||
41 | ||
0e6bfd8b JS |
42 | #: QMPMessage is an entire QMP message of any kind. |
43 | QMPMessage = Dict[str, Any] | |
44 | ||
45 | #: QMPReturnValue is the 'return' value of a command. | |
46 | QMPReturnValue = object | |
47 | ||
48 | #: QMPObject is any object in a QMP message. | |
49 | QMPObject = Dict[str, object] | |
50 | ||
51 | # QMPMessage can be outgoing commands or incoming events/returns. | |
52 | # QMPReturnValue is usually a dict/json object, but due to QAPI's | |
53 | # 'returns-whitelist', it can actually be anything. | |
54 | # | |
55 | # {'return': {}} is a QMPMessage, | |
56 | # {} is the QMPReturnValue. | |
57 | ||
58 | ||
9fcd3930 JS |
59 | class QMPBadPortError(QMPError): |
60 | """ | |
61 | Unable to parse socket address: Port was non-numerical. | |
62 | """ | |
63 | ||
64 | ||
0c78ebf7 | 65 | class QEMUMonitorProtocol: |
b0654f4f JS |
66 | """ |
67 | Provide an API to connect to QEMU via QEMU Monitor Protocol (QMP) | |
68 | and then allow to handle commands and events. | |
69 | ||
70 | :param address: QEMU address, can be either a unix socket path (string) | |
71 | or a tuple in the form ( address, port ) for a TCP | |
72 | connection | |
73 | :param server: Act as the socket server. (See 'accept') | |
74 | :param nickname: Optional nickname used for logging. | |
75 | """ | |
76 | ||
f122be60 JS |
77 | def __init__(self, address: SocketAddrT, |
78 | server: bool = False, | |
79 | nickname: Optional[str] = None): | |
80 | ||
37094b6d | 81 | self._qmp = QMPClient(nickname) |
f122be60 JS |
82 | self._aloop = asyncio.get_event_loop() |
83 | self._address = address | |
84 | self._timeout: Optional[float] = None | |
85 | ||
b0b662bb | 86 | if server: |
37094b6d | 87 | self._sync(self._qmp.start_server(self._address)) |
b0b662bb | 88 | |
f122be60 JS |
89 | _T = TypeVar('_T') |
90 | ||
91 | def _sync( | |
92 | self, future: Awaitable[_T], timeout: Optional[float] = None | |
93 | ) -> _T: | |
94 | return self._aloop.run_until_complete( | |
95 | asyncio.wait_for(future, timeout=timeout) | |
96 | ) | |
97 | ||
98 | def _get_greeting(self) -> Optional[QMPMessage]: | |
37094b6d | 99 | if self._qmp.greeting is not None: |
f122be60 | 100 | # pylint: disable=protected-access |
37094b6d | 101 | return self._qmp.greeting._asdict() |
f122be60 JS |
102 | return None |
103 | ||
0c78ebf7 JS |
104 | def __enter__(self: _T) -> _T: |
105 | # Implement context manager enter function. | |
106 | return self | |
107 | ||
108 | def __exit__(self, | |
0c78ebf7 JS |
109 | exc_type: Optional[Type[BaseException]], |
110 | exc_val: Optional[BaseException], | |
111 | exc_tb: Optional[TracebackType]) -> None: | |
112 | # Implement context manager exit function. | |
113 | self.close() | |
9fcd3930 JS |
114 | |
115 | @classmethod | |
116 | def parse_address(cls, address: str) -> SocketAddrT: | |
117 | """ | |
118 | Parse a string into a QMP address. | |
119 | ||
120 | Figure out if the argument is in the port:host form. | |
121 | If it's not, it's probably a file path. | |
122 | """ | |
123 | components = address.split(':') | |
124 | if len(components) == 2: | |
125 | try: | |
126 | port = int(components[1]) | |
127 | except ValueError: | |
128 | msg = f"Bad port: '{components[1]}' in '{address}'." | |
129 | raise QMPBadPortError(msg) from None | |
130 | return (components[0], port) | |
131 | ||
132 | # Treat as filepath. | |
133 | return address | |
f122be60 JS |
134 | |
135 | def connect(self, negotiate: bool = True) -> Optional[QMPMessage]: | |
b0654f4f JS |
136 | """ |
137 | Connect to the QMP Monitor and perform capabilities negotiation. | |
138 | ||
139 | :return: QMP greeting dict, or None if negotiate is false | |
140 | :raise ConnectError: on connection errors | |
141 | """ | |
37094b6d JS |
142 | self._qmp.await_greeting = negotiate |
143 | self._qmp.negotiate = negotiate | |
f122be60 JS |
144 | |
145 | self._sync( | |
37094b6d | 146 | self._qmp.connect(self._address) |
f122be60 JS |
147 | ) |
148 | return self._get_greeting() | |
149 | ||
150 | def accept(self, timeout: Optional[float] = 15.0) -> QMPMessage: | |
b0654f4f JS |
151 | """ |
152 | Await connection from QMP Monitor and perform capabilities negotiation. | |
153 | ||
154 | :param timeout: | |
155 | timeout in seconds (nonnegative float number, or None). | |
156 | If None, there is no timeout, and this may block forever. | |
157 | ||
158 | :return: QMP greeting dict | |
159 | :raise ConnectError: on connection errors | |
160 | """ | |
37094b6d JS |
161 | self._qmp.await_greeting = True |
162 | self._qmp.negotiate = True | |
f122be60 | 163 | |
37094b6d | 164 | self._sync(self._qmp.accept(), timeout) |
f122be60 JS |
165 | |
166 | ret = self._get_greeting() | |
167 | assert ret is not None | |
168 | return ret | |
169 | ||
170 | def cmd_obj(self, qmp_cmd: QMPMessage) -> QMPMessage: | |
b0654f4f JS |
171 | """ |
172 | Send a QMP command to the QMP Monitor. | |
173 | ||
174 | :param qmp_cmd: QMP command to be sent as a Python dict | |
175 | :return: QMP response as a Python dict | |
176 | """ | |
f122be60 JS |
177 | return dict( |
178 | self._sync( | |
179 | # pylint: disable=protected-access | |
180 | ||
181 | # _raw() isn't a public API, because turning off | |
182 | # automatic ID assignment is discouraged. For | |
183 | # compatibility with iotests *only*, do it anyway. | |
37094b6d | 184 | self._qmp._raw(qmp_cmd, assign_id=False), |
f122be60 JS |
185 | self._timeout |
186 | ) | |
187 | ) | |
188 | ||
0c78ebf7 JS |
189 | def cmd(self, name: str, |
190 | args: Optional[Dict[str, object]] = None, | |
191 | cmd_id: Optional[object] = None) -> QMPMessage: | |
192 | """ | |
193 | Build a QMP command and send it to the QMP Monitor. | |
194 | ||
b0654f4f JS |
195 | :param name: command name (string) |
196 | :param args: command arguments (dict) | |
197 | :param cmd_id: command id (dict, list, string or int) | |
0c78ebf7 JS |
198 | """ |
199 | qmp_cmd: QMPMessage = {'execute': name} | |
200 | if args: | |
201 | qmp_cmd['arguments'] = args | |
202 | if cmd_id: | |
203 | qmp_cmd['id'] = cmd_id | |
204 | return self.cmd_obj(qmp_cmd) | |
f122be60 JS |
205 | |
206 | def command(self, cmd: str, **kwds: object) -> QMPReturnValue: | |
b0654f4f JS |
207 | """ |
208 | Build and send a QMP command to the monitor, report errors if any | |
209 | """ | |
f122be60 | 210 | return self._sync( |
37094b6d | 211 | self._qmp.execute(cmd, kwds), |
f122be60 JS |
212 | self._timeout |
213 | ) | |
214 | ||
215 | def pull_event(self, | |
216 | wait: Union[bool, float] = False) -> Optional[QMPMessage]: | |
b0654f4f JS |
217 | """ |
218 | Pulls a single event. | |
219 | ||
220 | :param wait: | |
221 | If False or 0, do not wait. Return None if no events ready. | |
222 | If True, wait forever until the next event. | |
223 | Otherwise, wait for the specified number of seconds. | |
224 | ||
225 | :raise asyncio.TimeoutError: | |
226 | When a timeout is requested and the timeout period elapses. | |
227 | ||
228 | :return: The first available QMP event, or None. | |
229 | """ | |
f122be60 JS |
230 | if not wait: |
231 | # wait is False/0: "do not wait, do not except." | |
37094b6d | 232 | if self._qmp.events.empty(): |
f122be60 JS |
233 | return None |
234 | ||
235 | # If wait is 'True', wait forever. If wait is False/0, the events | |
236 | # queue must not be empty; but it still needs some real amount | |
237 | # of time to complete. | |
238 | timeout = None | |
239 | if wait and isinstance(wait, float): | |
240 | timeout = wait | |
241 | ||
242 | return dict( | |
243 | self._sync( | |
37094b6d | 244 | self._qmp.events.get(), |
f122be60 JS |
245 | timeout |
246 | ) | |
247 | ) | |
248 | ||
249 | def get_events(self, wait: Union[bool, float] = False) -> List[QMPMessage]: | |
b0654f4f JS |
250 | """ |
251 | Get a list of QMP events and clear all pending events. | |
252 | ||
253 | :param wait: | |
254 | If False or 0, do not wait. Return None if no events ready. | |
255 | If True, wait until we have at least one event. | |
256 | Otherwise, wait for up to the specified number of seconds for at | |
257 | least one event. | |
258 | ||
259 | :raise asyncio.TimeoutError: | |
260 | When a timeout is requested and the timeout period elapses. | |
261 | ||
262 | :return: A list of QMP events. | |
263 | """ | |
37094b6d | 264 | events = [dict(x) for x in self._qmp.events.clear()] |
f122be60 JS |
265 | if events: |
266 | return events | |
267 | ||
268 | event = self.pull_event(wait) | |
269 | return [event] if event is not None else [] | |
270 | ||
271 | def clear_events(self) -> None: | |
b0654f4f | 272 | """Clear current list of pending events.""" |
37094b6d | 273 | self._qmp.events.clear() |
f122be60 JS |
274 | |
275 | def close(self) -> None: | |
b0654f4f | 276 | """Close the connection.""" |
f122be60 | 277 | self._sync( |
37094b6d | 278 | self._qmp.disconnect() |
f122be60 JS |
279 | ) |
280 | ||
281 | def settimeout(self, timeout: Optional[float]) -> None: | |
b0654f4f JS |
282 | """ |
283 | Set the timeout for QMP RPC execution. | |
284 | ||
285 | This timeout affects the `cmd`, `cmd_obj`, and `command` methods. | |
286 | The `accept`, `pull_event` and `get_event` methods have their | |
287 | own configurable timeouts. | |
288 | ||
289 | :param timeout: | |
290 | timeout in seconds, or None. | |
291 | None will wait indefinitely. | |
292 | """ | |
f122be60 JS |
293 | self._timeout = timeout |
294 | ||
295 | def send_fd_scm(self, fd: int) -> None: | |
b0654f4f JS |
296 | """ |
297 | Send a file descriptor to the remote via SCM_RIGHTS. | |
298 | """ | |
37094b6d | 299 | self._qmp.send_fd_scm(fd) |
3bc72e3a JS |
300 | |
301 | def __del__(self) -> None: | |
37094b6d | 302 | if self._qmp.runstate == Runstate.IDLE: |
3bc72e3a JS |
303 | return |
304 | ||
305 | if not self._aloop.is_running(): | |
306 | self.close() | |
307 | else: | |
308 | # Garbage collection ran while the event loop was running. | |
309 | # Nothing we can do about it now, but if we don't raise our | |
310 | # own error, the user will be treated to a lot of traceback | |
311 | # they might not understand. | |
6e7751dc | 312 | raise QMPError( |
3bc72e3a JS |
313 | "QEMUMonitorProtocol.close()" |
314 | " was not called before object was garbage collected" | |
315 | ) |