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