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