QMP: Revamp the qmp-shell script
authorLuiz Capitulino <lcapitulino@redhat.com>
Wed, 27 Oct 2010 19:57:51 +0000 (17:57 -0200)
committerLuiz Capitulino <lcapitulino@redhat.com>
Wed, 17 Nov 2010 11:51:07 +0000 (09:51 -0200)
This commit updates the qmp-shell script to use the new interface
introduced by the last commit.

Additionally, the following fixes/features are also introduced:

 o TCP sockets support
 o Update/add documentation
 o Simple command-line completion
 o Fix a number of unhandled errors

Signed-off-by: Luiz Capitulino <lcapitulino@redhat.com>
QMP/qmp-shell

index a5b72d15d1808c8764b11cc93c02992626ecad7f..1fb7e767b0bb430e63b9b113d79409a945c85609 100755 (executable)
@@ -1,8 +1,8 @@
 #!/usr/bin/python
 #
-# Simple QEMU shell on top of QMP
+# Low-level QEMU shell on top of QMP.
 #
-# Copyright (C) 2009 Red Hat Inc.
+# Copyright (C) 2009, 2010 Red Hat Inc.
 #
 # Authors:
 #  Luiz Capitulino <lcapitulino@redhat.com>
 #
 # Start QEMU with:
 #
-# $ qemu [...] -monitor control,unix:./qmp,server
+# # qemu [...] -qmp unix:./qmp-sock,server
 #
 # Run the shell:
 #
-# $ qmp-shell ./qmp
+# $ qmp-shell ./qmp-sock
 #
 # Commands have the following format:
 #
-# < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
+#    < command-name > [ arg-name1=arg1 ] ... [ arg-nameN=argN ]
 #
 # For example:
 #
-# (QEMU) info item=network
+# (QEMU) device_add driver=e1000 id=net1
+# {u'return': {}}
+# (QEMU)
 
 import qmp
 import readline
-from sys import argv,exit
+import sys
 
-def shell_help():
-    print 'bye  exit from the shell'
+class QMPCompleter(list):
+    def complete(self, text, state):
+        for cmd in self:
+            if cmd.startswith(text):
+                if not state:
+                    return cmd
+                else:
+                    state -= 1
 
-def main():
-    if len(argv) != 2:
-        print 'qemu-shell <unix-socket>'
-        exit(1)
+class QMPShellError(Exception):
+    pass
+
+class QMPShellBadPort(QMPShellError):
+    pass
+
+# TODO: QMPShell's interface is a bit ugly (eg. _fill_completion() and
+#       _execute_cmd()). Let's design a better one.
+class QMPShell(qmp.QEMUMonitorProtocol):
+    def __init__(self, address):
+        qmp.QEMUMonitorProtocol.__init__(self, self.__get_address(address))
+        self._greeting = None
+        self._completer = None
+
+    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('')
+
+    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()
+        qmpcmd = { 'execute': cmdargs[0], 'arguments': {} }
+        for arg in cmdargs[1:]:
+            opt = arg.split('=')
+            try:
+                value = int(opt[1])
+            except ValueError:
+                value = opt[1]
+            qmpcmd['arguments'][opt[0]] = value
+        return qmpcmd
+
+    def _execute_cmd(self, cmdline):
+        try:
+            qmpcmd = self.__build_cmd(cmdline)
+        except:
+            print 'command format: <command-name> ',
+            print '[arg-name1=arg1] ... [arg-nameN=argN]'
+            return True
+        resp = self.cmd_obj(qmpcmd)
+        if resp is None:
+            print 'Disconnected'
+            return False
+        print resp
+        return True
+
+    def connect(self):
+        self._greeting = qmp.QEMUMonitorProtocol.connect(self)
+        self.__completer_setup()
 
-    qemu = qmp.QEMUMonitorProtocol(argv[1])
-    qemu.connect()
-    qemu.send("qmp_capabilities")
+    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'])
 
-    print 'Connected!'
+    def read_exec_command(self, prompt):
+        """
+        Read and execute a command.
 
-    while True:
+        @return True if execution was ok, return False if disconnected.
+        """
         try:
-            cmd = raw_input('(QEMU) ')
+            cmdline = raw_input(prompt)
         except EOFError:
             print
-            break
-        if cmd == '':
-            continue
-        elif cmd == 'bye':
-            break
-        elif cmd == 'help':
-            shell_help()
+            return False
+        if cmdline == '':
+            for ev in self.get_events():
+                print ev
+            self.clear_events()
+            return True
         else:
-            try:
-                resp = qemu.send(cmd)
-                if resp == None:
-                    print 'Disconnected'
-                    break
-                print resp
-            except IndexError:
-                print '-> command format: <command-name> ',
-                print '[arg-name1=arg1] ... [arg-nameN=argN]'
+            return self._execute_cmd(cmdline)
+
+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)
+    sys.stderr.write('qemu-shell [ -H ] < UNIX socket path> | < TCP address:port >\n')
+    sys.exit(1)
+
+def main():
+    try:
+        if len(sys.argv) == 2:
+            qemu = QMPShell(sys.argv[1])
+        else:
+                fail_cmdline()
+    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:
+        die('Could not connect to %s' % sys.argv[1])
+
+    qemu.show_banner()
+    while qemu.read_exec_command('(QEMU) '):
+        pass
+    qemu.close()
 
 if __name__ == '__main__':
     main()