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