xref: /openbmc/qemu/scripts/qmp/qmp-shell (revision daa5a72ebab20345da474ee2f6148a8dacd2cb17)
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        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 = qmp.QEMUMonitorProtocol.connect(self, negotiate)
285        self.__completer_setup()
286
287    def show_banner(self, msg='Welcome to the QMP low-level shell!'):
288        print msg
289        version = self._greeting['QMP']['version']['qemu']
290        print 'Connected to QEMU %d.%d.%d\n' % (version['major'],version['minor'],version['micro'])
291
292    def get_prompt(self):
293        if self._transmode:
294            return "TRANS> "
295        return "(QEMU) "
296
297    def read_exec_command(self, prompt):
298        """
299        Read and execute a command.
300
301        @return True if execution was ok, return False if disconnected.
302        """
303        try:
304            cmdline = raw_input(prompt)
305        except EOFError:
306            print
307            return False
308        if cmdline == '':
309            for ev in self.get_events():
310                print ev
311            self.clear_events()
312            return True
313        else:
314            return self._execute_cmd(cmdline)
315
316    def set_verbosity(self, verbose):
317        self._verbose = verbose
318
319class HMPShell(QMPShell):
320    def __init__(self, address):
321        QMPShell.__init__(self, address)
322        self.__cpu_index = 0
323
324    def __cmd_completion(self):
325        for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'):
326            if cmd and cmd[0] != '[' and cmd[0] != '\t':
327                name = cmd.split()[0] # drop help text
328                if name == 'info':
329                    continue
330                if name.find('|') != -1:
331                    # Command in the form 'foobar|f' or 'f|foobar', take the
332                    # full name
333                    opt = name.split('|')
334                    if len(opt[0]) == 1:
335                        name = opt[1]
336                    else:
337                        name = opt[0]
338                self._completer.append(name)
339                self._completer.append('help ' + name) # help completion
340
341    def __info_completion(self):
342        for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'):
343            if cmd:
344                self._completer.append('info ' + cmd.split()[1])
345
346    def __other_completion(self):
347        # special cases
348        self._completer.append('help info')
349
350    def _fill_completion(self):
351        self.__cmd_completion()
352        self.__info_completion()
353        self.__other_completion()
354
355    def __cmd_passthrough(self, cmdline, cpu_index = 0):
356        return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments':
357                              { 'command-line': cmdline,
358                                'cpu-index': cpu_index } })
359
360    def _execute_cmd(self, cmdline):
361        if cmdline.split()[0] == "cpu":
362            # trap the cpu command, it requires special setting
363            try:
364                idx = int(cmdline.split()[1])
365                if not 'return' in self.__cmd_passthrough('info version', idx):
366                    print 'bad CPU index'
367                    return True
368                self.__cpu_index = idx
369            except ValueError:
370                print 'cpu command takes an integer argument'
371                return True
372        resp = self.__cmd_passthrough(cmdline, self.__cpu_index)
373        if resp is None:
374            print 'Disconnected'
375            return False
376        assert 'return' in resp or 'error' in resp
377        if 'return' in resp:
378            # Success
379            if len(resp['return']) > 0:
380                print resp['return'],
381        else:
382            # Error
383            print '%s: %s' % (resp['error']['class'], resp['error']['desc'])
384        return True
385
386    def show_banner(self):
387        QMPShell.show_banner(self, msg='Welcome to the HMP shell!')
388
389def die(msg):
390    sys.stderr.write('ERROR: %s\n' % msg)
391    sys.exit(1)
392
393def fail_cmdline(option=None):
394    if option:
395        sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option)
396    sys.stderr.write('qemu-shell [ -v ] [ -p ] [ -H ] [ -N ] < UNIX socket path> | < TCP address:port >\n')
397    sys.exit(1)
398
399def main():
400    addr = ''
401    qemu = None
402    hmp = False
403    pretty = False
404    verbose = False
405    negotiate = True
406
407    try:
408        for arg in sys.argv[1:]:
409            if arg == "-H":
410                if qemu is not None:
411                    fail_cmdline(arg)
412                hmp = True
413            elif arg == "-p":
414                pretty = True
415            elif arg == "-N":
416                negotiate = False
417            elif arg == "-v":
418                verbose = True
419            else:
420                if qemu is not None:
421                    fail_cmdline(arg)
422                if hmp:
423                    qemu = HMPShell(arg)
424                else:
425                    qemu = QMPShell(arg, pretty)
426                addr = arg
427
428        if qemu is None:
429            fail_cmdline()
430    except QMPShellBadPort:
431        die('bad port number in command-line')
432
433    try:
434        qemu.connect(negotiate)
435    except qmp.QMPConnectError:
436        die('Didn\'t get QMP greeting message')
437    except qmp.QMPCapabilitiesError:
438        die('Could not negotiate capabilities')
439    except qemu.error:
440        die('Could not connect to %s' % addr)
441
442    qemu.show_banner()
443    qemu.set_verbosity(verbose)
444    while qemu.read_exec_command(qemu.get_prompt()):
445        pass
446    qemu.close()
447
448if __name__ == '__main__':
449    main()
450