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