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