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