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