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