2 # Copyright (C) 2009-2022 Red Hat Inc.
8 # This work is licensed under the terms of the GNU LGPL, version 2 or
9 # later. See the COPYING file in the top-level directory.
13 Low-level QEMU shell on top of QMP.
15 usage: qmp-shell [-h] [-H] [-N] [-v] [-p] qmp_server
18 qmp_server < UNIX socket path | TCP address:port >
21 -h, --help show this help message and exit
22 -H, --hmp Use HMP interface
23 -N, --skip-negotiation
24 Skip negotiate (for qemu-ga)
25 -v, --verbose Verbose (echo commands sent and received)
26 -p, --pretty Pretty-print JSON
31 # qemu [...] -qmp unix:./qmp-sock,server
35 $ qmp-shell ./qmp-sock
37 Commands have the following format:
39 < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
43 (QEMU) device_add driver=e1000 id=net1
47 key=value pairs also support Python or JSON object literal subset notations,
48 without spaces. Dictionaries/objects {} are supported as are arrays [].
50 example-command arg-name1={'key':'value','obj'={'prop':"value"}}
52 Both JSON and Python formatting should work, including both styles of
53 string literal quotes. Both paradigms of literal values should work,
54 including null/true/false for JSON and None/True/False for Python.
57 Transactions have the following multi-line format:
60 action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ]
62 action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ]
65 One line transactions are also supported:
67 transaction( action-name1 ... )
72 TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1
73 TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0
78 Use the -v and -p options to activate the verbose and pretty-print options,
79 which will echo back the properly formatted JSON-compliant QMP that is being
80 sent to QEMU, which is useful for debugging and documentation generation.
90 from subprocess import Popen
101 from qemu.qmp import ConnectError, QMPError, SocketAddrT
102 from qemu.qmp.legacy import (
110 LOG = logging.getLogger(__name__)
115 QMPCompleter provides a readline library tab-complete behavior.
117 # NB: Python 3.9+ will probably allow us to subclass list[str] directly,
118 # but pylint as of today does not know that List[str] is simply 'list'.
119 def __init__(self) -> None:
120 self._matches: List[str] = []
122 def append(self, value: str) -> None:
123 """Append a new valid completion to the list of possibilities."""
124 return self._matches.append(value)
126 def complete(self, text: str, state: int) -> Optional[str]:
127 """readline.set_completer() callback implementation."""
128 for cmd in self._matches:
129 if cmd.startswith(text):
136 class QMPShellError(QMPError):
138 QMP Shell Base error class.
142 class FuzzyJSON(ast.NodeTransformer):
144 This extension of ast.NodeTransformer filters literal "true/false/null"
145 values in a Python AST and replaces them by proper "True/False/None" values
146 that Python can properly evaluate.
150 def visit_Name(cls, # pylint: disable=invalid-name
151 node: ast.Name) -> ast.AST:
153 Transform Name nodes with certain values into Constant (keyword) nodes.
155 if node.id == 'true':
156 return ast.Constant(value=True)
157 if node.id == 'false':
158 return ast.Constant(value=False)
159 if node.id == 'null':
160 return ast.Constant(value=None)
164 class QMPShell(QEMUMonitorProtocol):
166 QMPShell provides a basic readline-based QMP shell.
168 :param address: Address of the QMP server.
169 :param pretty: Pretty-print QMP messages.
170 :param verbose: Echo outgoing QMP messages to console.
172 def __init__(self, address: SocketAddrT,
173 pretty: bool = False,
174 verbose: bool = False,
175 server: bool = False,
176 logfile: Optional[str] = None):
177 super().__init__(address, server=server)
178 self._greeting: Optional[QMPMessage] = None
179 self._completer = QMPCompleter()
180 self._transmode = False
181 self._actions: List[QMPMessage] = []
182 self._histfile = os.path.join(os.path.expanduser('~'),
183 '.qmp-shell_history')
185 self.verbose = verbose
188 if logfile is not None:
189 self.logfile = open(logfile, "w", encoding='utf-8')
191 def close(self) -> None:
192 # Hook into context manager of parent to save shell history.
196 def _fill_completion(self) -> None:
197 cmds = self.cmd('query-commands')
200 for cmd in cmds['return']:
201 self._completer.append(cmd['name'])
203 def _completer_setup(self) -> None:
204 self._completer = QMPCompleter()
205 self._fill_completion()
206 readline.set_history_length(1024)
207 readline.set_completer(self._completer.complete)
208 readline.parse_and_bind("tab: complete")
209 # NB: default delimiters conflict with some command names
210 # (eg. query-), clearing everything as it doesn't seem to matter
211 readline.set_completer_delims('')
213 readline.read_history_file(self._histfile)
214 except FileNotFoundError:
216 except IOError as err:
217 msg = f"Failed to read history '{self._histfile}': {err!s}"
220 def _save_history(self) -> None:
222 readline.write_history_file(self._histfile)
223 except IOError as err:
224 msg = f"Failed to save history file '{self._histfile}': {err!s}"
228 def _parse_value(cls, val: str) -> object:
234 if val.lower() == 'true':
236 if val.lower() == 'false':
238 if val.startswith(('{', '[')):
239 # Try first as pure JSON:
241 return json.loads(val)
244 # Try once again as FuzzyJSON:
246 tree = ast.parse(val, mode='eval')
247 transformed = FuzzyJSON().visit(tree)
248 return ast.literal_eval(transformed)
249 except (SyntaxError, ValueError):
254 tokens: Sequence[str],
255 parent: QMPObject) -> None:
257 (key, sep, val) = arg.partition('=')
260 f"Expected a key=value pair, got '{arg!s}'"
263 value = self._parse_value(val)
264 optpath = key.split('.')
266 for path in optpath[:-1]:
268 obj = parent.get(path, {})
269 if not isinstance(obj, dict):
270 msg = 'Cannot use "{:s}" as both leaf and non-leaf key'
271 raise QMPShellError(msg.format('.'.join(curpath)))
274 if optpath[-1] in parent:
275 if isinstance(parent[optpath[-1]], dict):
276 msg = 'Cannot use "{:s}" as both leaf and non-leaf key'
277 raise QMPShellError(msg.format('.'.join(curpath)))
278 raise QMPShellError(f'Cannot set "{key}" multiple times')
279 parent[optpath[-1]] = value
281 def _build_cmd(self, cmdline: str) -> Optional[QMPMessage]:
283 Build a QMP input object from a user provided command-line in the
286 < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
288 argument_regex = r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+'''
289 cmdargs = re.findall(argument_regex, cmdline)
292 # Transactional CLI entry:
293 if cmdargs and cmdargs[0] == 'transaction(':
294 self._transmode = True
298 # Transactional CLI exit:
299 if cmdargs and cmdargs[0] == ')' and self._transmode:
300 self._transmode = False
302 msg = 'Unexpected input after close of Transaction sub-shell'
303 raise QMPShellError(msg)
305 'execute': 'transaction',
306 'arguments': {'actions': self._actions}
310 # No args, or no args remaining
315 # Parse and cache this Transactional Action
317 action = {'type': cmdargs[0], 'data': {}}
318 if cmdargs[-1] == ')':
321 self._cli_expr(cmdargs[1:], action['data'])
322 self._actions.append(action)
323 return self._build_cmd(')') if finalize else None
325 # Standard command: parse and return it to be executed.
326 qmpcmd = {'execute': cmdargs[0], 'arguments': {}}
327 self._cli_expr(cmdargs[1:], qmpcmd['arguments'])
330 def _print(self, qmp_message: object, fh: IO[str] = sys.stdout) -> None:
331 jsobj = json.dumps(qmp_message,
332 indent=4 if self.pretty else None,
333 sort_keys=self.pretty)
334 print(str(jsobj), file=fh)
336 def _execute_cmd(self, cmdline: str) -> bool:
338 qmpcmd = self._build_cmd(cmdline)
339 except QMPShellError as err:
341 f"Error while parsing command line: {err!s}\n"
342 "command format: <command-name> "
343 "[arg-name1=arg1] ... [arg-nameN=argN",
347 # For transaction mode, we may have just cached the action:
352 resp = self.cmd_obj(qmpcmd)
354 print('Disconnected')
357 if self.logfile is not None:
358 cmd = {**qmpcmd, **resp}
359 self._print(cmd, fh=self.logfile)
362 def connect(self, negotiate: bool = True) -> None:
363 self._greeting = super().connect(negotiate)
364 self._completer_setup()
366 def show_banner(self,
367 msg: str = 'Welcome to the QMP low-level shell!') -> None:
369 Print to stdio a greeting, and the QEMU version if available.
372 if not self._greeting:
375 version = self._greeting['QMP']['version']['qemu']
376 print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version))
379 def prompt(self) -> str:
381 Return the current shell prompt, including a trailing space.
387 def read_exec_command(self) -> bool:
389 Read and execute a command.
391 @return True if execution was ok, return False if disconnected.
394 cmdline = input(self.prompt)
400 for event in self.get_events():
404 return self._execute_cmd(cmdline)
406 def repl(self) -> Iterator[None]:
408 Return an iterator that implements the REPL.
411 while self.read_exec_command():
416 class HMPShell(QMPShell):
418 HMPShell provides a basic readline-based HMP shell, tunnelled via QMP.
420 :param address: Address of the QMP server.
421 :param pretty: Pretty-print QMP messages.
422 :param verbose: Echo outgoing QMP messages to console.
424 def __init__(self, address: SocketAddrT,
425 pretty: bool = False,
426 verbose: bool = False,
427 server: bool = False,
428 logfile: Optional[str] = None):
429 super().__init__(address, pretty, verbose, server, logfile)
432 def _cmd_completion(self) -> None:
433 for cmd in self._cmd_passthrough('help')['return'].split('\r\n'):
434 if cmd and cmd[0] != '[' and cmd[0] != '\t':
435 name = cmd.split()[0] # drop help text
438 if name.find('|') != -1:
439 # Command in the form 'foobar|f' or 'f|foobar', take the
441 opt = name.split('|')
446 self._completer.append(name)
447 self._completer.append('help ' + name) # help completion
449 def _info_completion(self) -> None:
450 for cmd in self._cmd_passthrough('info')['return'].split('\r\n'):
452 self._completer.append('info ' + cmd.split()[1])
454 def _other_completion(self) -> None:
456 self._completer.append('help info')
458 def _fill_completion(self) -> None:
459 self._cmd_completion()
460 self._info_completion()
461 self._other_completion()
463 def _cmd_passthrough(self, cmdline: str,
464 cpu_index: int = 0) -> QMPMessage:
465 return self.cmd_obj({
466 'execute': 'human-monitor-command',
468 'command-line': cmdline,
469 'cpu-index': cpu_index
473 def _execute_cmd(self, cmdline: str) -> bool:
474 if cmdline.split()[0] == "cpu":
475 # trap the cpu command, it requires special setting
477 idx = int(cmdline.split()[1])
478 if 'return' not in self._cmd_passthrough('info version', idx):
479 print('bad CPU index')
481 self._cpu_index = idx
483 print('cpu command takes an integer argument')
485 resp = self._cmd_passthrough(cmdline, self._cpu_index)
487 print('Disconnected')
489 assert 'return' in resp or 'error' in resp
492 if len(resp['return']) > 0:
493 print(resp['return'], end=' ')
496 print('%s: %s' % (resp['error']['class'], resp['error']['desc']))
499 def show_banner(self, msg: str = 'Welcome to the HMP shell!') -> None:
500 QMPShell.show_banner(self, msg)
503 def die(msg: str) -> NoReturn:
504 """Write an error to stderr, then exit with a return code of 1."""
505 sys.stderr.write('ERROR: %s\n' % msg)
511 qmp-shell entry point: parse command line arguments and start the REPL.
513 parser = argparse.ArgumentParser()
514 parser.add_argument('-H', '--hmp', action='store_true',
515 help='Use HMP interface')
516 parser.add_argument('-N', '--skip-negotiation', action='store_true',
517 help='Skip negotiate (for qemu-ga)')
518 parser.add_argument('-v', '--verbose', action='store_true',
519 help='Verbose (echo commands sent and received)')
520 parser.add_argument('-p', '--pretty', action='store_true',
521 help='Pretty-print JSON')
522 parser.add_argument('-l', '--logfile',
523 help='Save log of all QMP messages to PATH')
525 default_server = os.environ.get('QMP_SOCKET')
526 parser.add_argument('qmp_server', action='store',
527 default=default_server,
528 help='< UNIX socket path | TCP address:port >')
530 args = parser.parse_args()
531 if args.qmp_server is None:
532 parser.error("QMP socket or TCP address must be specified")
534 shell_class = HMPShell if args.hmp else QMPShell
537 address = shell_class.parse_address(args.qmp_server)
538 except QMPBadPortError:
539 parser.error(f"Bad port number: {args.qmp_server}")
540 return # pycharm doesn't know error() is noreturn
542 with shell_class(address, args.pretty, args.verbose, args.logfile) as qemu:
544 qemu.connect(negotiate=not args.skip_negotiation)
545 except ConnectError as err:
546 if isinstance(err.exc, OSError):
547 die(f"Couldn't connect to {args.qmp_server}: {err!s}")
550 for _ in qemu.repl():
554 def main_wrap() -> None:
556 qmp-shell-wrap entry point: parse command line arguments and
559 parser = argparse.ArgumentParser()
560 parser.add_argument('-H', '--hmp', action='store_true',
561 help='Use HMP interface')
562 parser.add_argument('-v', '--verbose', action='store_true',
563 help='Verbose (echo commands sent and received)')
564 parser.add_argument('-p', '--pretty', action='store_true',
565 help='Pretty-print JSON')
566 parser.add_argument('-l', '--logfile',
567 help='Save log of all QMP messages to PATH')
569 parser.add_argument('command', nargs=argparse.REMAINDER,
570 help='QEMU command line to invoke')
572 args = parser.parse_args()
575 if len(cmd) != 0 and cmd[0] == '--':
578 cmd = ["qemu-system-x86_64"]
580 sockpath = "qmp-shell-wrap-%d" % os.getpid()
581 cmd += ["-qmp", "unix:%s" % sockpath]
583 shell_class = HMPShell if args.hmp else QMPShell
586 address = shell_class.parse_address(sockpath)
587 except QMPBadPortError:
588 parser.error(f"Bad port number: {sockpath}")
589 return # pycharm doesn't know error() is noreturn
592 with shell_class(address, args.pretty, args.verbose,
593 True, args.logfile) as qemu:
598 except ConnectError as err:
599 if isinstance(err.exc, OSError):
600 die(f"Couldn't connect to {args.qmp_server}: {err!s}")
603 for _ in qemu.repl():
609 if __name__ == '__main__':