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