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