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