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