xref: /openbmc/qemu/scripts/qmp/qmp-shell (revision 0b2ff2ce)
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
33import qmp
34import json
35import ast
36import readline
37import sys
38import pprint
39
40class QMPCompleter(list):
41    def complete(self, text, state):
42        for cmd in self:
43            if cmd.startswith(text):
44                if not state:
45                    return cmd
46                else:
47                    state -= 1
48
49class QMPShellError(Exception):
50    pass
51
52class QMPShellBadPort(QMPShellError):
53    pass
54
55class FuzzyJSON(ast.NodeTransformer):
56    '''This extension of ast.NodeTransformer filters literal "true/false/null"
57    values in an AST and replaces them by proper "True/False/None" values that
58    Python can properly evaluate.'''
59    def visit_Name(self, node):
60        if node.id == 'true':
61            node.id = 'True'
62        if node.id == 'false':
63            node.id = 'False'
64        if node.id == 'null':
65            node.id = 'None'
66        return node
67
68# TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and
69#       _execute_cmd()). Let's design a better one.
70class QMPShell(qmp.QEMUMonitorProtocol):
71    def __init__(self, address, pp=None):
72        qmp.QEMUMonitorProtocol.__init__(self, self.__get_address(address))
73        self._greeting = None
74        self._completer = None
75        self._pp = pp
76        self._transmode = False
77        self._actions = list()
78
79    def __get_address(self, arg):
80        """
81        Figure out if the argument is in the port:host form, if it's not it's
82        probably a file path.
83        """
84        addr = arg.split(':')
85        if len(addr) == 2:
86            try:
87                port = int(addr[1])
88            except ValueError:
89                raise QMPShellBadPort
90            return ( addr[0], port )
91        # socket path
92        return arg
93
94    def _fill_completion(self):
95        for cmd in self.cmd('query-commands')['return']:
96            self._completer.append(cmd['name'])
97
98    def __completer_setup(self):
99        self._completer = QMPCompleter()
100        self._fill_completion()
101        readline.set_completer(self._completer.complete)
102        readline.parse_and_bind("tab: complete")
103        # XXX: default delimiters conflict with some command names (eg. query-),
104        # clearing everything as it doesn't seem to matter
105        readline.set_completer_delims('')
106
107    def __parse_value(self, val):
108        try:
109            return int(val)
110        except ValueError:
111            pass
112
113        if val.lower() == 'true':
114            return True
115        if val.lower() == 'false':
116            return False
117        if val.startswith(('{', '[')):
118            # Try first as pure JSON:
119            try:
120                return json.loads(val)
121            except ValueError:
122                pass
123            # Try once again as FuzzyJSON:
124            try:
125                st = ast.parse(val, mode='eval')
126                return ast.literal_eval(FuzzyJSON().visit(st))
127            except SyntaxError:
128                pass
129            except ValueError:
130                pass
131        return val
132
133    def __cli_expr(self, tokens, parent):
134        for arg in tokens:
135            (key, _, val) = arg.partition('=')
136            if not val:
137                raise QMPShellError("Expected a key=value pair, got '%s'" % arg)
138
139            value = self.__parse_value(val)
140            optpath = key.split('.')
141            curpath = []
142            for p in optpath[:-1]:
143                curpath.append(p)
144                d = parent.get(p, {})
145                if type(d) is not dict:
146                    raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath))
147                parent[p] = d
148                parent = d
149            if optpath[-1] in parent:
150                if type(parent[optpath[-1]]) is dict:
151                    raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath))
152                else:
153                    raise QMPShellError('Cannot set "%s" multiple times' % key)
154            parent[optpath[-1]] = value
155
156    def __build_cmd(self, cmdline):
157        """
158        Build a QMP input object from a user provided command-line in the
159        following format:
160
161            < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
162        """
163        cmdargs = cmdline.split()
164
165        # Transactional CLI entry/exit:
166        if cmdargs[0] == 'transaction(':
167            self._transmode = True
168            cmdargs.pop(0)
169        elif cmdargs[0] == ')' and self._transmode:
170            self._transmode = False
171            if len(cmdargs) > 1:
172                raise QMPShellError("Unexpected input after close of Transaction sub-shell")
173            qmpcmd = { 'execute': 'transaction',
174                       'arguments': { 'actions': self._actions } }
175            self._actions = list()
176            return qmpcmd
177
178        # Nothing to process?
179        if not cmdargs:
180            return None
181
182        # Parse and then cache this Transactional Action
183        if self._transmode:
184            finalize = False
185            action = { 'type': cmdargs[0], 'data': {} }
186            if cmdargs[-1] == ')':
187                cmdargs.pop(-1)
188                finalize = True
189            self.__cli_expr(cmdargs[1:], action['data'])
190            self._actions.append(action)
191            return self.__build_cmd(')') if finalize else None
192
193        # Standard command: parse and return it to be executed.
194        qmpcmd = { 'execute': cmdargs[0], 'arguments': {} }
195        self.__cli_expr(cmdargs[1:], qmpcmd['arguments'])
196        return qmpcmd
197
198    def _print(self, qmp):
199        jsobj = json.dumps(qmp)
200        if self._pp is not None:
201            self._pp.pprint(jsobj)
202        else:
203            print str(jsobj)
204
205    def _execute_cmd(self, cmdline):
206        try:
207            qmpcmd = self.__build_cmd(cmdline)
208        except Exception, e:
209            print 'Error while parsing command line: %s' % e
210            print 'command format: <command-name> ',
211            print '[arg-name1=arg1] ... [arg-nameN=argN]'
212            return True
213        # For transaction mode, we may have just cached the action:
214        if qmpcmd is None:
215            return True
216        if self._verbose:
217            self._print(qmpcmd)
218        resp = self.cmd_obj(qmpcmd)
219        if resp is None:
220            print 'Disconnected'
221            return False
222        self._print(resp)
223        return True
224
225    def connect(self):
226        self._greeting = qmp.QEMUMonitorProtocol.connect(self)
227        self.__completer_setup()
228
229    def show_banner(self, msg='Welcome to the QMP low-level shell!'):
230        print msg
231        version = self._greeting['QMP']['version']['qemu']
232        print 'Connected to QEMU %d.%d.%d\n' % (version['major'],version['minor'],version['micro'])
233
234    def get_prompt(self):
235        if self._transmode:
236            return "TRANS> "
237        return "(QEMU) "
238
239    def read_exec_command(self, prompt):
240        """
241        Read and execute a command.
242
243        @return True if execution was ok, return False if disconnected.
244        """
245        try:
246            cmdline = raw_input(prompt)
247        except EOFError:
248            print
249            return False
250        if cmdline == '':
251            for ev in self.get_events():
252                print ev
253            self.clear_events()
254            return True
255        else:
256            return self._execute_cmd(cmdline)
257
258    def set_verbosity(self, verbose):
259        self._verbose = verbose
260
261class HMPShell(QMPShell):
262    def __init__(self, address):
263        QMPShell.__init__(self, address)
264        self.__cpu_index = 0
265
266    def __cmd_completion(self):
267        for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'):
268            if cmd and cmd[0] != '[' and cmd[0] != '\t':
269                name = cmd.split()[0] # drop help text
270                if name == 'info':
271                    continue
272                if name.find('|') != -1:
273                    # Command in the form 'foobar|f' or 'f|foobar', take the
274                    # full name
275                    opt = name.split('|')
276                    if len(opt[0]) == 1:
277                        name = opt[1]
278                    else:
279                        name = opt[0]
280                self._completer.append(name)
281                self._completer.append('help ' + name) # help completion
282
283    def __info_completion(self):
284        for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'):
285            if cmd:
286                self._completer.append('info ' + cmd.split()[1])
287
288    def __other_completion(self):
289        # special cases
290        self._completer.append('help info')
291
292    def _fill_completion(self):
293        self.__cmd_completion()
294        self.__info_completion()
295        self.__other_completion()
296
297    def __cmd_passthrough(self, cmdline, cpu_index = 0):
298        return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments':
299                              { 'command-line': cmdline,
300                                'cpu-index': cpu_index } })
301
302    def _execute_cmd(self, cmdline):
303        if cmdline.split()[0] == "cpu":
304            # trap the cpu command, it requires special setting
305            try:
306                idx = int(cmdline.split()[1])
307                if not 'return' in self.__cmd_passthrough('info version', idx):
308                    print 'bad CPU index'
309                    return True
310                self.__cpu_index = idx
311            except ValueError:
312                print 'cpu command takes an integer argument'
313                return True
314        resp = self.__cmd_passthrough(cmdline, self.__cpu_index)
315        if resp is None:
316            print 'Disconnected'
317            return False
318        assert 'return' in resp or 'error' in resp
319        if 'return' in resp:
320            # Success
321            if len(resp['return']) > 0:
322                print resp['return'],
323        else:
324            # Error
325            print '%s: %s' % (resp['error']['class'], resp['error']['desc'])
326        return True
327
328    def show_banner(self):
329        QMPShell.show_banner(self, msg='Welcome to the HMP shell!')
330
331def die(msg):
332    sys.stderr.write('ERROR: %s\n' % msg)
333    sys.exit(1)
334
335def fail_cmdline(option=None):
336    if option:
337        sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option)
338    sys.stderr.write('qemu-shell [ -v ] [ -p ] [ -H ] < UNIX socket path> | < TCP address:port >\n')
339    sys.exit(1)
340
341def main():
342    addr = ''
343    qemu = None
344    hmp = False
345    pp = None
346    verbose = False
347
348    try:
349        for arg in sys.argv[1:]:
350            if arg == "-H":
351                if qemu is not None:
352                    fail_cmdline(arg)
353                hmp = True
354            elif arg == "-p":
355                if pp is not None:
356                    fail_cmdline(arg)
357                pp = pprint.PrettyPrinter(indent=4)
358            elif arg == "-v":
359                verbose = True
360            else:
361                if qemu is not None:
362                    fail_cmdline(arg)
363                if hmp:
364                    qemu = HMPShell(arg)
365                else:
366                    qemu = QMPShell(arg, pp)
367                addr = arg
368
369        if qemu is None:
370            fail_cmdline()
371    except QMPShellBadPort:
372        die('bad port number in command-line')
373
374    try:
375        qemu.connect()
376    except qmp.QMPConnectError:
377        die('Didn\'t get QMP greeting message')
378    except qmp.QMPCapabilitiesError:
379        die('Could not negotiate capabilities')
380    except qemu.error:
381        die('Could not connect to %s' % addr)
382
383    qemu.show_banner()
384    qemu.set_verbosity(verbose)
385    while qemu.read_exec_command(qemu.get_prompt()):
386        pass
387    qemu.close()
388
389if __name__ == '__main__':
390    main()
391