xref: /qemu/scripts/qmp/qmp-shell (revision 6092c3ecc4bdafee5bf07061be78a4a2cc5a5088)
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
77    def __get_address(self, arg):
78        """
79        Figure out if the argument is in the port:host form, if it's not it's
80        probably a file path.
81        """
82        addr = arg.split(':')
83        if len(addr) == 2:
84            try:
85                port = int(addr[1])
86            except ValueError:
87                raise QMPShellBadPort
88            return ( addr[0], port )
89        # socket path
90        return arg
91
92    def _fill_completion(self):
93        for cmd in self.cmd('query-commands')['return']:
94            self._completer.append(cmd['name'])
95
96    def __completer_setup(self):
97        self._completer = QMPCompleter()
98        self._fill_completion()
99        readline.set_completer(self._completer.complete)
100        readline.parse_and_bind("tab: complete")
101        # XXX: default delimiters conflict with some command names (eg. query-),
102        # clearing everything as it doesn't seem to matter
103        readline.set_completer_delims('')
104
105    def __parse_value(self, val):
106        try:
107            return int(val)
108        except ValueError:
109            pass
110
111        if val.lower() == 'true':
112            return True
113        if val.lower() == 'false':
114            return False
115        if val.startswith(('{', '[')):
116            # Try first as pure JSON:
117            try:
118                return json.loads(val)
119            except ValueError:
120                pass
121            # Try once again as FuzzyJSON:
122            try:
123                st = ast.parse(val, mode='eval')
124                return ast.literal_eval(FuzzyJSON().visit(st))
125            except SyntaxError:
126                pass
127            except ValueError:
128                pass
129        return val
130
131    def __cli_expr(self, tokens, parent):
132        for arg in tokens:
133            (key, _, val) = arg.partition('=')
134            if not val:
135                raise QMPShellError("Expected a key=value pair, got '%s'" % arg)
136
137            value = self.__parse_value(val)
138            optpath = key.split('.')
139            curpath = []
140            for p in optpath[:-1]:
141                curpath.append(p)
142                d = parent.get(p, {})
143                if type(d) is not dict:
144                    raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath))
145                parent[p] = d
146                parent = d
147            if optpath[-1] in parent:
148                if type(parent[optpath[-1]]) is dict:
149                    raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath))
150                else:
151                    raise QMPShellError('Cannot set "%s" multiple times' % key)
152            parent[optpath[-1]] = value
153
154    def __build_cmd(self, cmdline):
155        """
156        Build a QMP input object from a user provided command-line in the
157        following format:
158
159            < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
160        """
161        cmdargs = cmdline.split()
162        qmpcmd = { 'execute': cmdargs[0], 'arguments': {} }
163        self.__cli_expr(cmdargs[1:], qmpcmd['arguments'])
164        return qmpcmd
165
166    def _execute_cmd(self, cmdline):
167        try:
168            qmpcmd = self.__build_cmd(cmdline)
169        except Exception, e:
170            print 'Error while parsing command line: %s' % e
171            print 'command format: <command-name> ',
172            print '[arg-name1=arg1] ... [arg-nameN=argN]'
173            return True
174        resp = self.cmd_obj(qmpcmd)
175        if resp is None:
176            print 'Disconnected'
177            return False
178
179        if self._pp is not None:
180            self._pp.pprint(resp)
181        else:
182            print resp
183        return True
184
185    def connect(self):
186        self._greeting = qmp.QEMUMonitorProtocol.connect(self)
187        self.__completer_setup()
188
189    def show_banner(self, msg='Welcome to the QMP low-level shell!'):
190        print msg
191        version = self._greeting['QMP']['version']['qemu']
192        print 'Connected to QEMU %d.%d.%d\n' % (version['major'],version['minor'],version['micro'])
193
194    def read_exec_command(self, prompt):
195        """
196        Read and execute a command.
197
198        @return True if execution was ok, return False if disconnected.
199        """
200        try:
201            cmdline = raw_input(prompt)
202        except EOFError:
203            print
204            return False
205        if cmdline == '':
206            for ev in self.get_events():
207                print ev
208            self.clear_events()
209            return True
210        else:
211            return self._execute_cmd(cmdline)
212
213class HMPShell(QMPShell):
214    def __init__(self, address):
215        QMPShell.__init__(self, address)
216        self.__cpu_index = 0
217
218    def __cmd_completion(self):
219        for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'):
220            if cmd and cmd[0] != '[' and cmd[0] != '\t':
221                name = cmd.split()[0] # drop help text
222                if name == 'info':
223                    continue
224                if name.find('|') != -1:
225                    # Command in the form 'foobar|f' or 'f|foobar', take the
226                    # full name
227                    opt = name.split('|')
228                    if len(opt[0]) == 1:
229                        name = opt[1]
230                    else:
231                        name = opt[0]
232                self._completer.append(name)
233                self._completer.append('help ' + name) # help completion
234
235    def __info_completion(self):
236        for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'):
237            if cmd:
238                self._completer.append('info ' + cmd.split()[1])
239
240    def __other_completion(self):
241        # special cases
242        self._completer.append('help info')
243
244    def _fill_completion(self):
245        self.__cmd_completion()
246        self.__info_completion()
247        self.__other_completion()
248
249    def __cmd_passthrough(self, cmdline, cpu_index = 0):
250        return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments':
251                              { 'command-line': cmdline,
252                                'cpu-index': cpu_index } })
253
254    def _execute_cmd(self, cmdline):
255        if cmdline.split()[0] == "cpu":
256            # trap the cpu command, it requires special setting
257            try:
258                idx = int(cmdline.split()[1])
259                if not 'return' in self.__cmd_passthrough('info version', idx):
260                    print 'bad CPU index'
261                    return True
262                self.__cpu_index = idx
263            except ValueError:
264                print 'cpu command takes an integer argument'
265                return True
266        resp = self.__cmd_passthrough(cmdline, self.__cpu_index)
267        if resp is None:
268            print 'Disconnected'
269            return False
270        assert 'return' in resp or 'error' in resp
271        if 'return' in resp:
272            # Success
273            if len(resp['return']) > 0:
274                print resp['return'],
275        else:
276            # Error
277            print '%s: %s' % (resp['error']['class'], resp['error']['desc'])
278        return True
279
280    def show_banner(self):
281        QMPShell.show_banner(self, msg='Welcome to the HMP shell!')
282
283def die(msg):
284    sys.stderr.write('ERROR: %s\n' % msg)
285    sys.exit(1)
286
287def fail_cmdline(option=None):
288    if option:
289        sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option)
290    sys.stderr.write('qemu-shell [ -p ] [ -H ] < UNIX socket path> | < TCP address:port >\n')
291    sys.exit(1)
292
293def main():
294    addr = ''
295    qemu = None
296    hmp = False
297    pp = None
298
299    try:
300        for arg in sys.argv[1:]:
301            if arg == "-H":
302                if qemu is not None:
303                    fail_cmdline(arg)
304                hmp = True
305            elif arg == "-p":
306                if pp is not None:
307                    fail_cmdline(arg)
308                pp = pprint.PrettyPrinter(indent=4)
309            else:
310                if qemu is not None:
311                    fail_cmdline(arg)
312                if hmp:
313                    qemu = HMPShell(arg)
314                else:
315                    qemu = QMPShell(arg, pp)
316                addr = arg
317
318        if qemu is None:
319            fail_cmdline()
320    except QMPShellBadPort:
321        die('bad port number in command-line')
322
323    try:
324        qemu.connect()
325    except qmp.QMPConnectError:
326        die('Didn\'t get QMP greeting message')
327    except qmp.QMPCapabilitiesError:
328        die('Could not negotiate capabilities')
329    except qemu.error:
330        die('Could not connect to %s' % addr)
331
332    qemu.show_banner()
333    while qemu.read_exec_command('(QEMU) '):
334        pass
335    qemu.close()
336
337if __name__ == '__main__':
338    main()
339