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