1#!/usr/bin/env python3 2# 3# Low-level QEMU shell on top of QMP. 4# 5# Copyright (C) 2009, 2010 Red Hat Inc. 6# 7# Authors: 8# Luiz Capitulino <lcapitulino@redhat.com> 9# 10# This work is licensed under the terms of the GNU GPL, version 2. See 11# the COPYING file in the top-level directory. 12# 13# Usage: 14# 15# Start QEMU with: 16# 17# # qemu [...] -qmp unix:./qmp-sock,server 18# 19# Run the shell: 20# 21# $ qmp-shell ./qmp-sock 22# 23# Commands have the following format: 24# 25# < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] 26# 27# For example: 28# 29# (QEMU) device_add driver=e1000 id=net1 30# {u'return': {}} 31# (QEMU) 32# 33# key=value pairs also support Python or JSON object literal subset notations, 34# without spaces. Dictionaries/objects {} are supported as are arrays []. 35# 36# example-command arg-name1={'key':'value','obj'={'prop':"value"}} 37# 38# Both JSON and Python formatting should work, including both styles of 39# string literal quotes. Both paradigms of literal values should work, 40# including null/true/false for JSON and None/True/False for Python. 41# 42# 43# Transactions have the following multi-line format: 44# 45# transaction( 46# action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ] 47# ... 48# action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ] 49# ) 50# 51# One line transactions are also supported: 52# 53# transaction( action-name1 ... ) 54# 55# For example: 56# 57# (QEMU) transaction( 58# TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1 59# TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0 60# TRANS> ) 61# {"return": {}} 62# (QEMU) 63# 64# Use the -v and -p options to activate the verbose and pretty-print options, 65# which will echo back the properly formatted JSON-compliant QMP that is being 66# sent to QEMU, which is useful for debugging and documentation generation. 67 68import ast 69import atexit 70import errno 71import json 72import os 73import re 74import readline 75import sys 76 77 78sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 79from qemu import qmp 80 81 82class QMPCompleter(list): 83 def complete(self, text, state): 84 for cmd in self: 85 if cmd.startswith(text): 86 if not state: 87 return cmd 88 else: 89 state -= 1 90 91 92class QMPShellError(Exception): 93 pass 94 95 96class FuzzyJSON(ast.NodeTransformer): 97 """ 98 This extension of ast.NodeTransformer filters literal "true/false/null" 99 values in an AST and replaces them by proper "True/False/None" values that 100 Python can properly evaluate. 101 """ 102 103 @classmethod 104 def visit_Name(cls, node): # pylint: disable=invalid-name 105 if node.id == 'true': 106 node.id = 'True' 107 if node.id == 'false': 108 node.id = 'False' 109 if node.id == 'null': 110 node.id = 'None' 111 return node 112 113 114# TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and 115# _execute_cmd()). Let's design a better one. 116class QMPShell(qmp.QEMUMonitorProtocol): 117 def __init__(self, address, pretty=False): 118 super().__init__(self.parse_address(address)) 119 self._greeting = None 120 self._completer = None 121 self._pretty = pretty 122 self._transmode = False 123 self._actions = list() 124 self._histfile = os.path.join(os.path.expanduser('~'), 125 '.qmp-shell_history') 126 self._verbose = False 127 128 def _fill_completion(self): 129 cmds = self.cmd('query-commands') 130 if 'error' in cmds: 131 return 132 for cmd in cmds['return']: 133 self._completer.append(cmd['name']) 134 135 def __completer_setup(self): 136 self._completer = QMPCompleter() 137 self._fill_completion() 138 readline.set_history_length(1024) 139 readline.set_completer(self._completer.complete) 140 readline.parse_and_bind("tab: complete") 141 # NB: default delimiters conflict with some command names 142 # (eg. query-), clearing everything as it doesn't seem to matter 143 readline.set_completer_delims('') 144 try: 145 readline.read_history_file(self._histfile) 146 except Exception as e: 147 if isinstance(e, IOError) and e.errno == errno.ENOENT: 148 # File not found. No problem. 149 pass 150 else: 151 print("Failed to read history '%s'; %s" % (self._histfile, e)) 152 atexit.register(self.__save_history) 153 154 def __save_history(self): 155 try: 156 readline.write_history_file(self._histfile) 157 except Exception as e: 158 print("Failed to save history file '%s'; %s" % (self._histfile, e)) 159 160 @classmethod 161 def __parse_value(cls, val): 162 try: 163 return int(val) 164 except ValueError: 165 pass 166 167 if val.lower() == 'true': 168 return True 169 if val.lower() == 'false': 170 return False 171 if val.startswith(('{', '[')): 172 # Try first as pure JSON: 173 try: 174 return json.loads(val) 175 except ValueError: 176 pass 177 # Try once again as FuzzyJSON: 178 try: 179 st = ast.parse(val, mode='eval') 180 return ast.literal_eval(FuzzyJSON().visit(st)) 181 except SyntaxError: 182 pass 183 except ValueError: 184 pass 185 return val 186 187 def __cli_expr(self, tokens, parent): 188 for arg in tokens: 189 (key, sep, val) = arg.partition('=') 190 if sep != '=': 191 raise QMPShellError( 192 f"Expected a key=value pair, got '{arg!s}'" 193 ) 194 195 value = self.__parse_value(val) 196 optpath = key.split('.') 197 curpath = [] 198 for p in optpath[:-1]: 199 curpath.append(p) 200 d = parent.get(p, {}) 201 if type(d) is not dict: 202 msg = 'Cannot use "{:s}" as both leaf and non-leaf key' 203 raise QMPShellError(msg.format('.'.join(curpath))) 204 parent[p] = d 205 parent = d 206 if optpath[-1] in parent: 207 if type(parent[optpath[-1]]) is dict: 208 msg = 'Cannot use "{:s}" as both leaf and non-leaf key' 209 raise QMPShellError(msg.format('.'.join(curpath))) 210 else: 211 raise QMPShellError(f'Cannot set "{key}" multiple times') 212 parent[optpath[-1]] = value 213 214 def __build_cmd(self, cmdline): 215 """ 216 Build a QMP input object from a user provided command-line in the 217 following format: 218 219 < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ] 220 """ 221 argument_regex = r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''' 222 cmdargs = re.findall(argument_regex, cmdline) 223 224 # Transactional CLI entry/exit: 225 if cmdargs[0] == 'transaction(': 226 self._transmode = True 227 cmdargs.pop(0) 228 elif cmdargs[0] == ')' and self._transmode: 229 self._transmode = False 230 if len(cmdargs) > 1: 231 msg = 'Unexpected input after close of Transaction sub-shell' 232 raise QMPShellError(msg) 233 qmpcmd = { 234 'execute': 'transaction', 235 'arguments': {'actions': self._actions} 236 } 237 self._actions = list() 238 return qmpcmd 239 240 # Nothing to process? 241 if not cmdargs: 242 return None 243 244 # Parse and then cache this Transactional Action 245 if self._transmode: 246 finalize = False 247 action = {'type': cmdargs[0], 'data': {}} 248 if cmdargs[-1] == ')': 249 cmdargs.pop(-1) 250 finalize = True 251 self.__cli_expr(cmdargs[1:], action['data']) 252 self._actions.append(action) 253 return self.__build_cmd(')') if finalize else None 254 255 # Standard command: parse and return it to be executed. 256 qmpcmd = {'execute': cmdargs[0], 'arguments': {}} 257 self.__cli_expr(cmdargs[1:], qmpcmd['arguments']) 258 return qmpcmd 259 260 def _print(self, qmp_message): 261 indent = None 262 if self._pretty: 263 indent = 4 264 jsobj = json.dumps(qmp_message, indent=indent, sort_keys=self._pretty) 265 print(str(jsobj)) 266 267 def _execute_cmd(self, cmdline): 268 try: 269 qmpcmd = self.__build_cmd(cmdline) 270 except Exception as e: 271 print('Error while parsing command line: %s' % e) 272 print('command format: <command-name> ', end=' ') 273 print('[arg-name1=arg1] ... [arg-nameN=argN]') 274 return True 275 # For transaction mode, we may have just cached the action: 276 if qmpcmd is None: 277 return True 278 if self._verbose: 279 self._print(qmpcmd) 280 resp = self.cmd_obj(qmpcmd) 281 if resp is None: 282 print('Disconnected') 283 return False 284 self._print(resp) 285 return True 286 287 def connect(self, negotiate: bool = True): 288 self._greeting = super().connect(negotiate) 289 self.__completer_setup() 290 291 def show_banner(self, msg='Welcome to the QMP low-level shell!'): 292 print(msg) 293 if not self._greeting: 294 print('Connected') 295 return 296 version = self._greeting['QMP']['version']['qemu'] 297 print("Connected to QEMU {major}.{minor}.{micro}\n".format(**version)) 298 299 def get_prompt(self): 300 if self._transmode: 301 return "TRANS> " 302 return "(QEMU) " 303 304 def read_exec_command(self, prompt): 305 """ 306 Read and execute a command. 307 308 @return True if execution was ok, return False if disconnected. 309 """ 310 try: 311 cmdline = input(prompt) 312 except EOFError: 313 print() 314 return False 315 if cmdline == '': 316 for ev in self.get_events(): 317 print(ev) 318 self.clear_events() 319 return True 320 else: 321 return self._execute_cmd(cmdline) 322 323 def set_verbosity(self, verbose): 324 self._verbose = verbose 325 326 327class HMPShell(QMPShell): 328 def __init__(self, address): 329 super().__init__(address) 330 self.__cpu_index = 0 331 332 def __cmd_completion(self): 333 for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'): 334 if cmd and cmd[0] != '[' and cmd[0] != '\t': 335 name = cmd.split()[0] # drop help text 336 if name == 'info': 337 continue 338 if name.find('|') != -1: 339 # Command in the form 'foobar|f' or 'f|foobar', take the 340 # full name 341 opt = name.split('|') 342 if len(opt[0]) == 1: 343 name = opt[1] 344 else: 345 name = opt[0] 346 self._completer.append(name) 347 self._completer.append('help ' + name) # help completion 348 349 def __info_completion(self): 350 for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'): 351 if cmd: 352 self._completer.append('info ' + cmd.split()[1]) 353 354 def __other_completion(self): 355 # special cases 356 self._completer.append('help info') 357 358 def _fill_completion(self): 359 self.__cmd_completion() 360 self.__info_completion() 361 self.__other_completion() 362 363 def __cmd_passthrough(self, cmdline, cpu_index=0): 364 return self.cmd_obj({ 365 'execute': 'human-monitor-command', 366 'arguments': { 367 'command-line': cmdline, 368 'cpu-index': cpu_index 369 } 370 }) 371 372 def _execute_cmd(self, cmdline): 373 if cmdline.split()[0] == "cpu": 374 # trap the cpu command, it requires special setting 375 try: 376 idx = int(cmdline.split()[1]) 377 if 'return' not in self.__cmd_passthrough('info version', idx): 378 print('bad CPU index') 379 return True 380 self.__cpu_index = idx 381 except ValueError: 382 print('cpu command takes an integer argument') 383 return True 384 resp = self.__cmd_passthrough(cmdline, self.__cpu_index) 385 if resp is None: 386 print('Disconnected') 387 return False 388 assert 'return' in resp or 'error' in resp 389 if 'return' in resp: 390 # Success 391 if len(resp['return']) > 0: 392 print(resp['return'], end=' ') 393 else: 394 # Error 395 print('%s: %s' % (resp['error']['class'], resp['error']['desc'])) 396 return True 397 398 def show_banner(self, msg='Welcome to the HMP shell!'): 399 QMPShell.show_banner(self, msg) 400 401 402def die(msg): 403 sys.stderr.write('ERROR: %s\n' % msg) 404 sys.exit(1) 405 406 407def fail_cmdline(option=None): 408 if option: 409 sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option) 410 sys.stderr.write( 411 'qmp-shell [ -v ] [ -p ] [ -H ] [ -N ] ' 412 '< UNIX socket path> | < TCP address:port >\n' 413 ) 414 sys.stderr.write(' -v Verbose (echo command sent and received)\n') 415 sys.stderr.write(' -p Pretty-print JSON\n') 416 sys.stderr.write(' -H Use HMP interface\n') 417 sys.stderr.write(' -N Skip negotiate (for qemu-ga)\n') 418 sys.exit(1) 419 420 421def main(): 422 addr = '' 423 qemu = None 424 hmp = False 425 pretty = False 426 verbose = False 427 negotiate = True 428 429 try: 430 for arg in sys.argv[1:]: 431 if arg == "-H": 432 if qemu is not None: 433 fail_cmdline(arg) 434 hmp = True 435 elif arg == "-p": 436 pretty = True 437 elif arg == "-N": 438 negotiate = False 439 elif arg == "-v": 440 verbose = True 441 else: 442 if qemu is not None: 443 fail_cmdline(arg) 444 if hmp: 445 qemu = HMPShell(arg) 446 else: 447 qemu = QMPShell(arg, pretty) 448 addr = arg 449 450 if qemu is None: 451 fail_cmdline() 452 except qmp.QMPBadPortError: 453 die('bad port number in command-line') 454 455 try: 456 qemu.connect(negotiate) 457 except qmp.QMPConnectError: 458 die('Didn\'t get QMP greeting message') 459 except qmp.QMPCapabilitiesError: 460 die('Could not negotiate capabilities') 461 except OSError: 462 die('Could not connect to %s' % addr) 463 464 qemu.show_banner() 465 qemu.set_verbosity(verbose) 466 while qemu.read_exec_command(qemu.get_prompt()): 467 pass 468 qemu.close() 469 470 471if __name__ == '__main__': 472 main() 473