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