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