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 super(QMPShell, self).__init__(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 = super(QMPShell, self).connect(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('qmp-shell [ -v ] [ -p ] [ -H ] [ -N ] < UNIX socket path> | < TCP address:port >\n') 400 sys.stderr.write(' -v Verbose (echo command sent and received)\n') 401 sys.stderr.write(' -p Pretty-print JSON\n') 402 sys.stderr.write(' -H Use HMP interface\n') 403 sys.stderr.write(' -N Skip negotiate (for qemu-ga)\n') 404 sys.exit(1) 405 406def main(): 407 addr = '' 408 qemu = None 409 hmp = False 410 pretty = False 411 verbose = False 412 negotiate = True 413 414 try: 415 for arg in sys.argv[1:]: 416 if arg == "-H": 417 if qemu is not None: 418 fail_cmdline(arg) 419 hmp = True 420 elif arg == "-p": 421 pretty = True 422 elif arg == "-N": 423 negotiate = False 424 elif arg == "-v": 425 verbose = True 426 else: 427 if qemu is not None: 428 fail_cmdline(arg) 429 if hmp: 430 qemu = HMPShell(arg) 431 else: 432 qemu = QMPShell(arg, pretty) 433 addr = arg 434 435 if qemu is None: 436 fail_cmdline() 437 except QMPShellBadPort: 438 die('bad port number in command-line') 439 440 try: 441 qemu.connect(negotiate) 442 except qmp.QMPConnectError: 443 die('Didn\'t get QMP greeting message') 444 except qmp.QMPCapabilitiesError: 445 die('Could not negotiate capabilities') 446 except qemu.error: 447 die('Could not connect to %s' % addr) 448 449 qemu.show_banner() 450 qemu.set_verbosity(verbose) 451 while qemu.read_exec_command(qemu.get_prompt()): 452 pass 453 qemu.close() 454 455if __name__ == '__main__': 456 main() 457