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