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