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