qmp-shell 13.1 KB
Newer Older
L
Luiz Capitulino 已提交
1 2
#!/usr/bin/python
#
L
Luiz Capitulino 已提交
3
# Low-level QEMU shell on top of QMP.
L
Luiz Capitulino 已提交
4
#
L
Luiz Capitulino 已提交
5
# Copyright (C) 2009, 2010 Red Hat Inc.
L
Luiz Capitulino 已提交
6 7 8 9 10 11 12 13 14 15 16
#
# Authors:
#  Luiz Capitulino <lcapitulino@redhat.com>
#
# This work is licensed under the terms of the GNU GPL, version 2.  See
# the COPYING file in the top-level directory.
#
# Usage:
#
# Start QEMU with:
#
L
Luiz Capitulino 已提交
17
# # qemu [...] -qmp unix:./qmp-sock,server
L
Luiz Capitulino 已提交
18 19 20
#
# Run the shell:
#
L
Luiz Capitulino 已提交
21
# $ qmp-shell ./qmp-sock
L
Luiz Capitulino 已提交
22 23 24
#
# Commands have the following format:
#
L
Luiz Capitulino 已提交
25
#    < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
L
Luiz Capitulino 已提交
26 27 28
#
# For example:
#
L
Luiz Capitulino 已提交
29 30 31
# (QEMU) device_add driver=e1000 id=net1
# {u'return': {}}
# (QEMU)
J
John Snow 已提交
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
#
# key=value pairs also support Python or JSON object literal subset notations,
# without spaces. Dictionaries/objects {} are supported as are arrays [].
#
#    example-command arg-name1={'key':'value','obj'={'prop':"value"}}
#
# Both JSON and Python formatting should work, including both styles of
# string literal quotes. Both paradigms of literal values should work,
# including null/true/false for JSON and None/True/False for Python.
#
#
# Transactions have the following multi-line format:
#
#    transaction(
#    action-name1 [ arg-name1=arg1 ] ... [arg-nameN=argN ]
#    ...
#    action-nameN [ arg-name1=arg1 ] ... [arg-nameN=argN ]
#    )
#
# One line transactions are also supported:
#
#    transaction( action-name1 ... )
#
# For example:
#
#     (QEMU) transaction(
#     TRANS> block-dirty-bitmap-add node=drive0 name=bitmap1
#     TRANS> block-dirty-bitmap-clear node=drive0 name=bitmap0
#     TRANS> )
#     {"return": {}}
#     (QEMU)
#
# Use the -v and -p options to activate the verbose and pretty-print options,
# which will echo back the properly formatted JSON-compliant QMP that is being
# sent to QEMU, which is useful for debugging and documentation generation.
L
Luiz Capitulino 已提交
67 68

import qmp
69
import json
70
import ast
L
Luiz Capitulino 已提交
71
import readline
L
Luiz Capitulino 已提交
72
import sys
73
import pprint
L
Luiz Capitulino 已提交
74

L
Luiz Capitulino 已提交
75 76 77 78 79 80 81 82
class QMPCompleter(list):
    def complete(self, text, state):
        for cmd in self:
            if cmd.startswith(text):
                if not state:
                    return cmd
                else:
                    state -= 1
L
Luiz Capitulino 已提交
83

L
Luiz Capitulino 已提交
84 85 86 87 88 89
class QMPShellError(Exception):
    pass

class QMPShellBadPort(QMPShellError):
    pass

90 91 92 93 94 95 96 97 98 99 100 101 102
class FuzzyJSON(ast.NodeTransformer):
    '''This extension of ast.NodeTransformer filters literal "true/false/null"
    values in an AST and replaces them by proper "True/False/None" values that
    Python can properly evaluate.'''
    def visit_Name(self, node):
        if node.id == 'true':
            node.id = 'True'
        if node.id == 'false':
            node.id = 'False'
        if node.id == 'null':
            node.id = 'None'
        return node

L
Luiz Capitulino 已提交
103 104 105
# TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and
#       _execute_cmd()). Let's design a better one.
class QMPShell(qmp.QEMUMonitorProtocol):
106
    def __init__(self, address, pp=None):
L
Luiz Capitulino 已提交
107 108 109
        qmp.QEMUMonitorProtocol.__init__(self, self.__get_address(address))
        self._greeting = None
        self._completer = None
110
        self._pp = pp
111 112
        self._transmode = False
        self._actions = list()
L
Luiz Capitulino 已提交
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141

    def __get_address(self, arg):
        """
        Figure out if the argument is in the port:host form, if it's not it's
        probably a file path.
        """
        addr = arg.split(':')
        if len(addr) == 2:
            try:
                port = int(addr[1])
            except ValueError:
                raise QMPShellBadPort
            return ( addr[0], port )
        # socket path
        return arg

    def _fill_completion(self):
        for cmd in self.cmd('query-commands')['return']:
            self._completer.append(cmd['name'])

    def __completer_setup(self):
        self._completer = QMPCompleter()
        self._fill_completion()
        readline.set_completer(self._completer.complete)
        readline.parse_and_bind("tab: complete")
        # XXX: default delimiters conflict with some command names (eg. query-),
        # clearing everything as it doesn't seem to matter
        readline.set_completer_delims('')

142 143 144 145 146 147 148 149 150 151 152 153
    def __parse_value(self, val):
        try:
            return int(val)
        except ValueError:
            pass

        if val.lower() == 'true':
            return True
        if val.lower() == 'false':
            return False
        if val.startswith(('{', '[')):
            # Try first as pure JSON:
L
Luiz Capitulino 已提交
154
            try:
155
                return json.loads(val)
L
Luiz Capitulino 已提交
156
            except ValueError:
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
                pass
            # Try once again as FuzzyJSON:
            try:
                st = ast.parse(val, mode='eval')
                return ast.literal_eval(FuzzyJSON().visit(st))
            except SyntaxError:
                pass
            except ValueError:
                pass
        return val

    def __cli_expr(self, tokens, parent):
        for arg in tokens:
            (key, _, val) = arg.partition('=')
            if not val:
                raise QMPShellError("Expected a key=value pair, got '%s'" % arg)

            value = self.__parse_value(val)
            optpath = key.split('.')
176 177 178 179 180 181 182 183 184 185 186 187
            curpath = []
            for p in optpath[:-1]:
                curpath.append(p)
                d = parent.get(p, {})
                if type(d) is not dict:
                    raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath))
                parent[p] = d
                parent = d
            if optpath[-1] in parent:
                if type(parent[optpath[-1]]) is dict:
                    raise QMPShellError('Cannot use "%s" as both leaf and non-leaf key' % '.'.join(curpath))
                else:
188
                    raise QMPShellError('Cannot set "%s" multiple times' % key)
189
            parent[optpath[-1]] = value
J
John Snow 已提交
190 191 192 193 194 195 196 197 198

    def __build_cmd(self, cmdline):
        """
        Build a QMP input object from a user provided command-line in the
        following format:

            < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
        """
        cmdargs = cmdline.split()
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228

        # Transactional CLI entry/exit:
        if cmdargs[0] == 'transaction(':
            self._transmode = True
            cmdargs.pop(0)
        elif cmdargs[0] == ')' and self._transmode:
            self._transmode = False
            if len(cmdargs) > 1:
                raise QMPShellError("Unexpected input after close of Transaction sub-shell")
            qmpcmd = { 'execute': 'transaction',
                       'arguments': { 'actions': self._actions } }
            self._actions = list()
            return qmpcmd

        # Nothing to process?
        if not cmdargs:
            return None

        # Parse and then cache this Transactional Action
        if self._transmode:
            finalize = False
            action = { 'type': cmdargs[0], 'data': {} }
            if cmdargs[-1] == ')':
                cmdargs.pop(-1)
                finalize = True
            self.__cli_expr(cmdargs[1:], action['data'])
            self._actions.append(action)
            return self.__build_cmd(')') if finalize else None

        # Standard command: parse and return it to be executed.
J
John Snow 已提交
229 230
        qmpcmd = { 'execute': cmdargs[0], 'arguments': {} }
        self.__cli_expr(cmdargs[1:], qmpcmd['arguments'])
L
Luiz Capitulino 已提交
231 232
        return qmpcmd

J
John Snow 已提交
233 234 235 236 237 238 239
    def _print(self, qmp):
        jsobj = json.dumps(qmp)
        if self._pp is not None:
            self._pp.pprint(jsobj)
        else:
            print str(jsobj)

L
Luiz Capitulino 已提交
240 241 242
    def _execute_cmd(self, cmdline):
        try:
            qmpcmd = self.__build_cmd(cmdline)
243 244
        except Exception, e:
            print 'Error while parsing command line: %s' % e
L
Luiz Capitulino 已提交
245 246 247
            print 'command format: <command-name> ',
            print '[arg-name1=arg1] ... [arg-nameN=argN]'
            return True
248 249 250
        # For transaction mode, we may have just cached the action:
        if qmpcmd is None:
            return True
J
John Snow 已提交
251 252
        if self._verbose:
            self._print(qmpcmd)
L
Luiz Capitulino 已提交
253 254 255 256
        resp = self.cmd_obj(qmpcmd)
        if resp is None:
            print 'Disconnected'
            return False
J
John Snow 已提交
257
        self._print(resp)
L
Luiz Capitulino 已提交
258 259 260 261 262
        return True

    def connect(self):
        self._greeting = qmp.QEMUMonitorProtocol.connect(self)
        self.__completer_setup()
L
Luiz Capitulino 已提交
263

L
Luiz Capitulino 已提交
264 265 266 267
    def show_banner(self, msg='Welcome to the QMP low-level shell!'):
        print msg
        version = self._greeting['QMP']['version']['qemu']
        print 'Connected to QEMU %d.%d.%d\n' % (version['major'],version['minor'],version['micro'])
L
Luiz Capitulino 已提交
268

269 270 271 272 273
    def get_prompt(self):
        if self._transmode:
            return "TRANS> "
        return "(QEMU) "

L
Luiz Capitulino 已提交
274 275 276
    def read_exec_command(self, prompt):
        """
        Read and execute a command.
L
Luiz Capitulino 已提交
277

L
Luiz Capitulino 已提交
278 279
        @return True if execution was ok, return False if disconnected.
        """
L
Luiz Capitulino 已提交
280
        try:
L
Luiz Capitulino 已提交
281
            cmdline = raw_input(prompt)
L
Luiz Capitulino 已提交
282 283
        except EOFError:
            print
L
Luiz Capitulino 已提交
284 285 286 287 288 289
            return False
        if cmdline == '':
            for ev in self.get_events():
                print ev
            self.clear_events()
            return True
L
Luiz Capitulino 已提交
290
        else:
L
Luiz Capitulino 已提交
291 292
            return self._execute_cmd(cmdline)

J
John Snow 已提交
293 294 295
    def set_verbosity(self, verbose):
        self._verbose = verbose

296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
class HMPShell(QMPShell):
    def __init__(self, address):
        QMPShell.__init__(self, address)
        self.__cpu_index = 0

    def __cmd_completion(self):
        for cmd in self.__cmd_passthrough('help')['return'].split('\r\n'):
            if cmd and cmd[0] != '[' and cmd[0] != '\t':
                name = cmd.split()[0] # drop help text
                if name == 'info':
                    continue
                if name.find('|') != -1:
                    # Command in the form 'foobar|f' or 'f|foobar', take the
                    # full name
                    opt = name.split('|')
                    if len(opt[0]) == 1:
                        name = opt[1]
                    else:
                        name = opt[0]
                self._completer.append(name)
                self._completer.append('help ' + name) # help completion

    def __info_completion(self):
        for cmd in self.__cmd_passthrough('info')['return'].split('\r\n'):
            if cmd:
                self._completer.append('info ' + cmd.split()[1])

    def __other_completion(self):
        # special cases
        self._completer.append('help info')

    def _fill_completion(self):
        self.__cmd_completion()
        self.__info_completion()
        self.__other_completion()

    def __cmd_passthrough(self, cmdline, cpu_index = 0):
        return self.cmd_obj({ 'execute': 'human-monitor-command', 'arguments':
                              { 'command-line': cmdline,
                                'cpu-index': cpu_index } })

    def _execute_cmd(self, cmdline):
        if cmdline.split()[0] == "cpu":
            # trap the cpu command, it requires special setting
            try:
                idx = int(cmdline.split()[1])
                if not 'return' in self.__cmd_passthrough('info version', idx):
                    print 'bad CPU index'
                    return True
                self.__cpu_index = idx
            except ValueError:
                print 'cpu command takes an integer argument'
                return True
        resp = self.__cmd_passthrough(cmdline, self.__cpu_index)
        if resp is None:
            print 'Disconnected'
            return False
        assert 'return' in resp or 'error' in resp
        if 'return' in resp:
            # Success
            if len(resp['return']) > 0:
                print resp['return'],
        else:
            # Error
            print '%s: %s' % (resp['error']['class'], resp['error']['desc'])
        return True

    def show_banner(self):
        QMPShell.show_banner(self, msg='Welcome to the HMP shell!')

L
Luiz Capitulino 已提交
366 367 368 369 370 371 372
def die(msg):
    sys.stderr.write('ERROR: %s\n' % msg)
    sys.exit(1)

def fail_cmdline(option=None):
    if option:
        sys.stderr.write('ERROR: bad command-line option \'%s\'\n' % option)
J
John Snow 已提交
373
    sys.stderr.write('qemu-shell [ -v ] [ -p ] [ -H ] < UNIX socket path> | < TCP address:port >\n')
L
Luiz Capitulino 已提交
374 375 376
    sys.exit(1)

def main():
377
    addr = ''
378 379 380
    qemu = None
    hmp = False
    pp = None
J
John Snow 已提交
381
    verbose = False
382

L
Luiz Capitulino 已提交
383
    try:
384 385 386 387 388 389 390 391 392
        for arg in sys.argv[1:]:
            if arg == "-H":
                if qemu is not None:
                    fail_cmdline(arg)
                hmp = True
            elif arg == "-p":
                if pp is not None:
                    fail_cmdline(arg)
                pp = pprint.PrettyPrinter(indent=4)
J
John Snow 已提交
393 394
            elif arg == "-v":
                verbose = True
395 396 397 398 399 400 401 402 403 404 405
            else:
                if qemu is not None:
                    fail_cmdline(arg)
                if hmp:
                    qemu = HMPShell(arg)
                else:
                    qemu = QMPShell(arg, pp)
                addr = arg

        if qemu is None:
            fail_cmdline()
L
Luiz Capitulino 已提交
406 407 408 409 410 411 412 413 414 415
    except QMPShellBadPort:
        die('bad port number in command-line')

    try:
        qemu.connect()
    except qmp.QMPConnectError:
        die('Didn\'t get QMP greeting message')
    except qmp.QMPCapabilitiesError:
        die('Could not negotiate capabilities')
    except qemu.error:
416
        die('Could not connect to %s' % addr)
L
Luiz Capitulino 已提交
417 418

    qemu.show_banner()
J
John Snow 已提交
419
    qemu.set_verbosity(verbose)
420
    while qemu.read_exec_command(qemu.get_prompt()):
L
Luiz Capitulino 已提交
421 422
        pass
    qemu.close()
L
Luiz Capitulino 已提交
423 424 425

if __name__ == '__main__':
    main()