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