xref: /openbmc/qemu/scripts/qmp/qmp-shell (revision 502d0f36)
1#!/usr/bin/python
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 qmp
69import json
70import ast
71import readline
72import sys
73import os
74import errno
75import atexit
76
77class QMPCompleter(list):
78    def complete(self, text, state):
79        for cmd in self:
80            if cmd.startswith(text):
81                if not state:
82                    return cmd
83                else:
84                    state -= 1
85
86class QMPShellError(Exception):
87    pass
88
89class QMPShellBadPort(QMPShellError):
90    pass
91
92class FuzzyJSON(ast.NodeTransformer):
93    '''This extension of ast.NodeTransformer filters literal "true/false/null"
94    values in an AST and replaces them by proper "True/False/None" values that
95    Python can properly evaluate.'''
96    def visit_Name(self, node):
97        if node.id == 'true':
98            node.id = 'True'
99        if node.id == 'false':
100            node.id = 'False'
101        if node.id == 'null':
102            node.id = 'None'
103        return node
104
105# TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and
106#       _execute_cmd()). Let's design a better one.
107class QMPShell(qmp.QEMUMonitorProtocol):
108    def __init__(self, address, pretty=False):
109        super(QMPShell, self).__init__(self.__get_address(address))
110        self._greeting = None
111        self._completer = None
112        self._pretty = pretty
113        self._transmode = False
114        self._actions = list()
115        self._histfile = os.path.join(os.path.expanduser('~'),
116                                      '.qmp-shell_history')
117
118    def __get_address(self, arg):
119        """
120        Figure out if the argument is in the port:host form, if it's not it's
121        probably a file path.
122        """
123        addr = arg.split(':')
124        if len(addr) == 2:
125            try:
126                port = int(addr[1])
127            except ValueError:
128                raise QMPShellBadPort
129            return ( addr[0], port )
130        # socket path
131        return arg
132
133    def _fill_completion(self):
134        cmds = self.cmd('query-commands')
135        if cmds.has_key('error'):
136            return
137        for cmd in cmds['return']:
138            self._completer.append(cmd['name'])
139
140    def __completer_setup(self):
141        self._completer = QMPCompleter()
142        self._fill_completion()
143        readline.set_history_length(1024)
144        readline.set_completer(self._completer.complete)
145        readline.parse_and_bind("tab: complete")
146        # XXX: default delimiters conflict with some command names (eg. query-),
147        # clearing everything as it doesn't seem to matter
148        readline.set_completer_delims('')
149        try:
150            readline.read_history_file(self._histfile)
151        except Exception as e:
152            if isinstance(e, IOError) and e.errno == errno.ENOENT:
153                # File not found. No problem.
154                pass
155            else:
156                print "Failed to read history '%s'; %s" % (self._histfile, e)
157        atexit.register(self.__save_history)
158
159    def __save_history(self):
160        try:
161            readline.write_history_file(self._histfile)
162        except Exception as e:
163            print "Failed to save history file '%s'; %s" % (self._histfile, e)
164
165    def __parse_value(self, val):
166        try:
167            return int(val)
168        except ValueError:
169            pass
170
171        if val.lower() == 'true':
172            return True
173        if val.lower() == 'false':
174            return False
175        if val.startswith(('{', '[')):
176            # Try first as pure JSON:
177            try:
178                return json.loads(val)
179            except ValueError:
180                pass
181            # Try once again as FuzzyJSON:
182            try:
183                st = ast.parse(val, mode='eval')
184                return ast.literal_eval(FuzzyJSON().visit(st))
185            except SyntaxError:
186                pass
187            except ValueError:
188                pass
189        return val
190
191    def __cli_expr(self, tokens, parent):
192        for arg in tokens:
193            (key, sep, val) = arg.partition('=')
194            if sep != '=':
195                raise QMPShellError("Expected a key=value pair, got '%s'" % arg)
196
197            value = self.__parse_value(val)
198            optpath = key.split('.')
199            curpath = []
200            for p in optpath[:-1]:
201                curpath.append(p)
202                d = parent.get(p, {})
203                if type(d) is not dict:
204                    raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath))
205                parent[p] = d
206                parent = d
207            if optpath[-1] in parent:
208                if type(parent[optpath[-1]]) is dict:
209                    raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath))
210                else:
211                    raise QMPShellError('Cannot set "%s" multiple times' % key)
212            parent[optpath[-1]] = value
213
214    def __build_cmd(self, cmdline):
215        """
216        Build a QMP input object from a user provided command-line in the
217        following format:
218
219            < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
220        """
221        cmdargs = cmdline.split()
222
223        # Transactional CLI entry/exit:
224        if cmdargs[0] == 'transaction(':
225            self._transmode = True
226            cmdargs.pop(0)
227        elif cmdargs[0] == ')' and self._transmode:
228            self._transmode = False
229            if len(cmdargs) > 1:
230                raise QMPShellError("Unexpected input after close of Transaction sub-shell")
231            qmpcmd = { 'execute': 'transaction',
232                       'arguments': { 'actions': self._actions } }
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):
257        indent = None
258        if self._pretty:
259            indent = 4
260        jsobj = json.dumps(qmp, indent=indent)
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> ',
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):
284        self._greeting = super(QMPShell, self).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 %d.%d.%d\n' % (version['major'],version['minor'],version['micro'])
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 = raw_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
322class HMPShell(QMPShell):
323    def __init__(self, address):
324        QMPShell.__init__(self, address)
325        self.__cpu_index = 0
326
327    def __cmd_completion(self):
328        for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'):
329            if cmd and cmd[0] != '[' and cmd[0] != '\t':
330                name = cmd.split()[0] # drop help text
331                if name == 'info':
332                    continue
333                if name.find('|') != -1:
334                    # Command in the form 'foobar|f' or 'f|foobar', take the
335                    # full name
336                    opt = name.split('|')
337                    if len(opt[0]) == 1:
338                        name = opt[1]
339                    else:
340                        name = opt[0]
341                self._completer.append(name)
342                self._completer.append('help ' + name) # help completion
343
344    def __info_completion(self):
345        for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'):
346            if cmd:
347                self._completer.append('info ' + cmd.split()[1])
348
349    def __other_completion(self):
350        # special cases
351        self._completer.append('help info')
352
353    def _fill_completion(self):
354        self.__cmd_completion()
355        self.__info_completion()
356        self.__other_completion()
357
358    def __cmd_passthrough(self, cmdline, cpu_index = 0):
359        return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments':
360                              { 'command-line': cmdline,
361                                'cpu-index': cpu_index } })
362
363    def _execute_cmd(self, cmdline):
364        if cmdline.split()[0] == "cpu":
365            # trap the cpu command, it requires special setting
366            try:
367                idx = int(cmdline.split()[1])
368                if not 'return' in self.__cmd_passthrough('info version', idx):
369                    print 'bad CPU index'
370                    return True
371                self.__cpu_index = idx
372            except ValueError:
373                print 'cpu command takes an integer argument'
374                return True
375        resp = self.__cmd_passthrough(cmdline, self.__cpu_index)
376        if resp is None:
377            print 'Disconnected'
378            return False
379        assert 'return' in resp or 'error' in resp
380        if 'return' in resp:
381            # Success
382            if len(resp['return']) > 0:
383                print resp['return'],
384        else:
385            # Error
386            print '%s: %s' % (resp['error']['class'], resp['error']['desc'])
387        return True
388
389    def show_banner(self):
390        QMPShell.show_banner(self, msg='Welcome to the HMP shell!')
391
392def die(msg):
393    sys.stderr.write('ERROR: %s\n' % msg)
394    sys.exit(1)
395
396def fail_cmdline(option=None):
397    if option:
398        sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option)
399    sys.stderr.write('qmp-shell [ -v ] [ -p ] [ -H ] [ -N ] < UNIX socket path> | < TCP address:port >\n')
400    sys.stderr.write('    -v     Verbose (echo command sent and received)\n')
401    sys.stderr.write('    -p     Pretty-print JSON\n')
402    sys.stderr.write('    -H     Use HMP interface\n')
403    sys.stderr.write('    -N     Skip negotiate (for qemu-ga)\n')
404    sys.exit(1)
405
406def main():
407    addr = ''
408    qemu = None
409    hmp = False
410    pretty = False
411    verbose = False
412    negotiate = True
413
414    try:
415        for arg in sys.argv[1:]:
416            if arg == "-H":
417                if qemu is not None:
418                    fail_cmdline(arg)
419                hmp = True
420            elif arg == "-p":
421                pretty = True
422            elif arg == "-N":
423                negotiate = False
424            elif arg == "-v":
425                verbose = True
426            else:
427                if qemu is not None:
428                    fail_cmdline(arg)
429                if hmp:
430                    qemu = HMPShell(arg)
431                else:
432                    qemu = QMPShell(arg, pretty)
433                addr = arg
434
435        if qemu is None:
436            fail_cmdline()
437    except QMPShellBadPort:
438        die('bad port number in command-line')
439
440    try:
441        qemu.connect(negotiate)
442    except qmp.QMPConnectError:
443        die('Didn\'t get QMP greeting message')
444    except qmp.QMPCapabilitiesError:
445        die('Could not negotiate capabilities')
446    except qemu.error:
447        die('Could not connect to %s' % addr)
448
449    qemu.show_banner()
450    qemu.set_verbosity(verbose)
451    while qemu.read_exec_command(qemu.get_prompt()):
452        pass
453    qemu.close()
454
455if __name__ == '__main__':
456    main()
457