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