qom/object: factor out the initialization of hash table of properties
[qemu/ar7.git] / scripts / qmp / qmp-shell
blobc5eef06f3fd99641f019b3f39e05131035ad435b
1 #!/usr/bin/env python3
3 # Low-level QEMU shell on top of QMP.
5 # Copyright (C) 2009, 2010 Red Hat Inc.
7 # Authors:
8 #  Luiz Capitulino <lcapitulino@redhat.com>
10 # This work is licensed under the terms of the GNU GPL, version 2.  See
11 # the COPYING file in the top-level directory.
13 # Usage:
15 # Start QEMU with:
17 # # qemu [...] -qmp unix:./qmp-sock,server
19 # Run the shell:
21 # $ qmp-shell ./qmp-sock
23 # Commands have the following format:
25 #    < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
27 # For example:
29 # (QEMU) device_add driver=e1000 id=net1
30 # {u'return': {}}
31 # (QEMU)
33 # key=value pairs also support Python or JSON object literal subset notations,
34 # without spaces. Dictionaries/objects {} are supported as are arrays [].
36 #    example-command arg-name1={'key':'value','obj'={'prop':"value"}}
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.
43 # Transactions have the following multi-line format:
45 #    transaction(
46 #    action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ]
47 #    ...
48 #    action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ]
49 #    )
51 # One line transactions are also supported:
53 #    transaction( action-name1 ... )
55 # For example:
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)
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.
68 import json
69 import ast
70 import readline
71 import sys
72 import os
73 import errno
74 import atexit
75 import re
77 sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
78 from qemu import qmp
80 class 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
89 class QMPShellError(Exception):
90     pass
92 class QMPShellBadPort(QMPShellError):
93     pass
95 class 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
108 # TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and
109 #       _execute_cmd()). Let's design a better one.
110 class 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')
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
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'])
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)
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))
168     def __parse_value(self, val):
169         try:
170             return int(val)
171         except ValueError:
172             pass
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
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)
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
217     def __build_cmd(self, cmdline):
218         """
219         Build a QMP input object from a user provided command-line in the
220         following format:
222             < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
223         """
224         cmdargs = re.findall(r'''(?:[^\s"']|"(?:\\.|[^"])*"|'(?:\\.|[^'])*')+''', cmdline)
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
239         # Nothing to process?
240         if not cmdargs:
241             return None
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
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
259     def _print(self, qmp):
260         indent = None
261         if self._pretty:
262             indent = 4
263         jsobj = json.dumps(qmp, indent=indent)
264         print(str(jsobj))
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
286     def connect(self, negotiate):
287         self._greeting = super(QMPShell, self).connect(negotiate)
288         self.__completer_setup()
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']))
298     def get_prompt(self):
299         if self._transmode:
300             return "TRANS> "
301         return "(QEMU) "
303     def read_exec_command(self, prompt):
304         """
305         Read and execute a command.
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)
322     def set_verbosity(self, verbose):
323         self._verbose = verbose
325 class HMPShell(QMPShell):
326     def __init__(self, address):
327         QMPShell.__init__(self, address)
328         self.__cpu_index = 0
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
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])
352     def __other_completion(self):
353         # special cases
354         self._completer.append('help info')
356     def _fill_completion(self):
357         self.__cmd_completion()
358         self.__info_completion()
359         self.__other_completion()
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 } })
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
392     def show_banner(self):
393         QMPShell.show_banner(self, msg='Welcome to the HMP shell!')
395 def die(msg):
396     sys.stderr.write('ERROR: %s\n' % msg)
397     sys.exit(1)
399 def 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)
409 def main():
410     addr = ''
411     qemu = None
412     hmp = False
413     pretty = False
414     verbose = False
415     negotiate = True
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
438         if qemu is None:
439             fail_cmdline()
440     except QMPShellBadPort:
441         die('bad port number in command-line')
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)
452     qemu.show_banner()
453     qemu.set_verbosity(verbose)
454     while qemu.read_exec_command(qemu.get_prompt()):
455         pass
456     qemu.close()
458 if __name__ == '__main__':
459     main()