- use temporary home directory since apache user does not have write
[plcapi.git] / Shell.py
index 0c49f92..b755648 100755 (executable)
--- a/Shell.py
+++ b/Shell.py
@@ -5,7 +5,7 @@
 # Mark Huang <mlhuang@cs.princeton.edu>
 # Copyright (C) 2005 The Trustees of Princeton University
 #
-# $Id: Shell.py,v 1.5 2006/10/03 19:34:05 mlhuang Exp $
+# $Id: Shell.py,v 1.17 2006/12/13 22:29:28 mlhuang Exp $
 #
 
 import os, sys
@@ -21,192 +21,11 @@ from PLC.Parameter import Mixed
 from PLC.Auth import Auth
 from PLC.Config import Config
 from PLC.Method import Method
+from PLC.PyCurl import PyCurlTransport
 import PLC.Methods
 
-# Defaults
-config = "/etc/planetlab/plc_config"
-url = None
-method = None
-user = None
-password = None
-role = None
-
-def usage():
-    print "Usage: %s [OPTION]..." % sys.argv[0]
-    print "Options:"
-    print "     -f, --config=FILE       PLC configuration file"
-    print "     -h, --url=URL           API URL"
-    print "     -m, --method=METHOD     API authentication method"
-    print "     -u, --user=EMAIL        API user name"
-    print "     -p, --password=STRING   API password"
-    print "     -r, --role=ROLE         API role"
-    print "     -x, --xmlrpc            Use XML-RPC interface"
-    print "     --help                  This message"
-    sys.exit(1)
-
-# Get options
-try:
-    (opts, argv) = getopt.getopt(sys.argv[1:],
-                                 "f:h:m:u:p:r:x",
-                                 ["config=", "cfg=", "file=",
-                                  "host=",
-                                  "method=",
-                                  "username=", "user=",
-                                  "password=", "pass=", "authstring=",
-                                  "role=",
-                                  "xmlrpc",
-                                  "help"])
-except getopt.GetoptError, err:
-    print "Error: " + err.msg
-    usage()
-
-for (opt, optval) in opts:
-    if opt == "-f" or opt == "--config" or opt == "--cfg" or opt == "--file":
-        config = optval
-    elif opt == "-h" or opt == "--host" or opt == "--url":
-        url = optval
-    elif opt == "-m" or opt == "--method":
-        method = optval
-    elif opt == "-u" or opt == "--username" or opt == "--user":
-        user = optval
-    elif opt == "-p" or opt == "--password" or opt == "--pass" or opt == "--authstring":
-        password = optval
-    elif opt == "-r" or opt == "--role":
-        role = optval
-    elif opt == "--help":
-        usage()
-
-try:
-    # If any XML-RPC options have been specified, do not try
-    # connecting directly to the DB.
-    if opts:
-        raise Exception
-        
-    # Otherwise, first try connecting directly to the DB. If this
-    # fails, try connecting to the API server via XML-RPC.
-    api = PLCAPI(config)
-    config = api.config
-    server = None
-except:
-    # Try connecting to the API server via XML-RPC
-    api = PLCAPI(None)
-    config = Config(config)
-
-    if url is None:
-        if int(config.PLC_API_PORT) == 443:
-            url = "https://"
-        else:
-            url = "http://"
-        url += config.PLC_API_HOST + \
-               ":" + str(config.PLC_API_PORT) + \
-               "/" + config.PLC_API_PATH + "/"
-
-    server = xmlrpclib.ServerProxy(url, allow_none = 1)
-
-# Default is to use capability authentication
-if (method, user, password) == (None, None, None):
-    method = "capability"
-
-if method == "capability":
-    if user is None:
-        user = config.PLC_API_MAINTENANCE_USER
-    if password is None:
-        password = config.PLC_API_MAINTENANCE_PASSWORD
-    if role is None:
-        role = "admin"
-elif method is None:
-    method = "password"
-
-if role == "anonymous" or method == "anonymous":
-    auth = {'AuthMethod': "anonymous"}
-else:
-    if role is None:
-        print "Error: must specify a role with -r"
-        usage()
-
-    if user is None:
-        print "Error: must specify a username with -u"
-        usage()
-
-    if password is None:
-        try:
-            password = getpass.getpass()
-        except (EOFError, KeyboardInterrupt):
-            print
-            sys.exit(0)
-
-    auth = {'AuthMethod': method,
-            'Username': user,
-            'AuthString': password,
-            'Role': role}
-
-class Callable:
-    """
-    Wrapper to call a method either directly or remotely. Initialize
-    with no arguments to use as a dummy class to support tab
-    completion of API methods with dots in their names (e.g.,
-    system.listMethods).
-    """
-
-    def __init__(self, method = None):
-        self.name = method
-
-        if method is not None:
-            # Figure out if the function requires an authentication
-            # structure as its first argument.
-            self.auth = False
-           func = api.callable(method)
-            
-           try:
-                #func = api.callable(method)
-                if func.accepts and \
-                   (isinstance(func.accepts[0], Auth) or \
-                    (isinstance(func.accepts[0], Mixed) and \
-                     filter(lambda param: isinstance(param, Auth), func.accepts[0]))):
-                    self.auth = True
-            except:
-                # XXX Ignore undefined methods for now
-                pass
-
-            if server is not None:
-                self.func = getattr(server, method)
-            else:
-                self.func = func
-
-    def __call__(self, *args, **kwds):
-        """
-        Automagically add the authentication structure if the function
-        requires it and it has not been specified.
-        """
-
-        if self.auth and \
-           (not args or not isinstance(args[0], dict) or not args[0].has_key('AuthMethod')):
-            return self.func(auth, *args, **kwds)
-        else:
-            return self.func(*args)
-
-if server is not None:
-    methods = server.system.listMethods()
-else:
-    methods = api.call(None, "system.listMethods")
-
-# Define all methods in the global namespace to support tab completion
-for method in methods:
-    paths = method.split(".")
-    if len(paths) > 1:
-        first = paths.pop(0)
-        if first not in globals():
-            globals()[first] = Callable()
-        obj = globals()[first]
-        for path in paths:
-            if not hasattr(obj, path):
-                if path == paths[-1]:
-                    setattr(obj, path, Callable(method))
-                else:
-                    setattr(obj, path, Callable())
-            obj = getattr(obj, path)
-    else:
-        globals()[method] = Callable(method)
+# the list of globals formerly defined by Shell.py before it was made a class
+former_globals = ['api','auth','config','begin','commit','calls']
 
 pyhelp = help
 def help(thing):
@@ -215,7 +34,7 @@ def help(thing):
     """
 
     # help(method)
-    if isinstance(thing, Callable) and thing.name is not None:
+    if isinstance(thing, Shell.Callable) and thing.name is not None:
         pydoc.pager(system.methodHelp(thing.name))
         return
 
@@ -226,87 +45,385 @@ def help(thing):
     # help(...)
     pyhelp(thing)
 
-# If a file is specified
-if argv:
-    execfile(argv[0])
-    sys.exit(0)
-
-# Otherwise, create an interactive shell environment
-
-if server is None:
-    print "PlanetLab Central Direct API Access"
-    prompt = ""
-elif auth['AuthMethod'] == "anonymous":
-    prompt = "[anonymous]"
-    print "Connected anonymously"
-else:
-    prompt = "[%s %s]" % (auth['Username'], auth['Role'])
-    print "%s connected as %s using %s authentication" % \
-          (auth['Username'], auth['Role'], auth['AuthMethod'])
-print 'Type "system.listMethods()" or "help(method)" for more information.'
-
-# Readline and tab completion support
-import atexit
-import readline
-import rlcompleter
-
-# Load command history
-history_path = os.path.join(os.environ["HOME"], ".plcapi_history")
-try:
-    file(history_path, 'a').close()
-    readline.read_history_file(history_path)
-    atexit.register(readline.write_history_file, history_path)
-except IOError:
-    pass
-
-# Enable tab completion
-readline.parse_and_bind("tab: complete")
-
-try:
-    while True:
-        command = ""
-        while True:
-            # Get line
+####################
+class Shell:
+
+    def __init__ (self,argv,config=None):
+
+        # Defaults
+        if config is not None:
+            self.config=config
+        else:
+            # support running on non-myplc boxes
+            default_config_file = "/etc/planetlab/plc_config"
             try:
-                if command == "":
-                    sep = ">>> "
+                open (default_config_file).close()
+            except:
+                default_config_file="/dev/null"
+            self.config = default_config_file
+        self.url = None
+        self.method = None
+        self.user = None
+        self.password = None
+        self.role = None
+        self.xmlrpc = False
+        self.server = None
+        self.cacert = None
+
+        # More convenient multicall support
+        self.multi = False
+        self.calls = []
+        self.argv = argv
+
+    def init_from_argv (self):
+
+        try:
+            (opts, argv) = getopt.getopt(self.argv[1:],
+                                         "f:h:m:u:p:r:x",
+                                         ["config=", "cfg=", "file=",
+                                          "host=","url=",
+                                          "method=",
+                                          "username=", "user=",
+                                          "password=", "pass=", "authstring=",
+                                          "role=",
+                                          "xmlrpc",
+                                          "cacert=",
+                                          "help"])
+        except getopt.GetoptError, err:
+            print "Error: ", err.msg
+            self.usage(self.argv)
+                
+        for (opt, optval) in opts:
+            if opt == "-f" or opt == "--config" or opt == "--cfg" or opt == "--file":
+                self.config = optval
+            elif opt == "-h" or opt == "--host" or opt == "--url":
+                self.url = optval
+            elif opt == "-m" or opt == "--method":
+                self.method = optval
+            elif opt == "-u" or opt == "--username" or opt == "--user":
+                self.user = optval
+            elif opt == "-p" or opt == "--password" or opt == "--pass" or opt == "--authstring":
+                self.password = optval
+            elif opt == "-r" or opt == "--role":
+                self.role = optval
+            elif opt == "-x" or opt == "--xmlrpc":
+                self.xmlrpc = True
+            elif opt == "--cacert":
+                self.cacert = optval
+            elif opt == "--help":
+                self.usage(self.argv)
+
+    def usage(self,argv):
+        print "Usage: %s [OPTION]..." % argv[0]
+        print "Options:"
+        print "     -f, --config=FILE       PLC configuration file"
+        print "     -h, --url=URL           API URL"
+        print "     -m, --method=METHOD     API authentication method"
+        print "     -u, --user=EMAIL        API user name"
+        print "     -p, --password=STRING   API password"
+        print "     -r, --role=ROLE         API role"
+        print "     -x, --xmlrpc            Use XML-RPC interface"
+        print "     --cacert=CACERT         API SSL certificate"
+        print "     --help                  This message"
+        sys.exit(1)
+
+    def init_connection(self):
+
+        # Append PLC to the system path
+        sys.path.append(os.path.dirname(os.path.realpath(self.argv[0])))
+
+        try:
+            # If any XML-RPC options have been specified, do not try
+            # connecting directly to the DB.
+            if (self.url, self.method, self.user, self.password, self.role, self.cacert, self.xmlrpc) != \
+                   (None, None, None, None, None, None, False):
+                raise Exception
+        
+            # Otherwise, first try connecting directly to the DB. If this
+            # fails, try connecting to the API server via XML-RPC.
+            self.api = PLCAPI(self.config)
+            self.config = self.api.config
+            self.server = None
+        except:
+            # Try connecting to the API server via XML-RPC
+            self.api = PLCAPI(None)
+            self.config = Config(self.config)
+
+            if self.url is None:
+                if int(self.config.PLC_API_PORT) == 443:
+                    self.url = "https://"
                 else:
-                    sep = "... "
-                line = raw_input(prompt + sep)
-            # Ctrl-C
-            except KeyboardInterrupt:
-                command = ""
-                print
-                break
+                    self.url = "http://"
+                self.url += self.config.PLC_API_HOST + \
+                            ":" + str(self.config.PLC_API_PORT) + \
+                            "/" + self.config.PLC_API_PATH + "/"
+
+            if self.cacert is None:
+                self.cacert = self.config.PLC_API_CA_SSL_CRT
+
+            self.server = xmlrpclib.ServerProxy(self.url, PyCurlTransport(self.url, self.cacert), allow_none = 1)
+
+        # Default is to use capability authentication
+        if (self.method, self.user, self.password) == (None, None, None):
+            self.method = "capability"
+
+        if self.method == "capability":
+            if self.user is None:
+                self.user = self.config.PLC_API_MAINTENANCE_USER
+            if self.password is None:
+                self.password = self.config.PLC_API_MAINTENANCE_PASSWORD
+            if self.role is None:
+                self.role = "admin"
+        elif self.method is None:
+            self.method = "password"
+
+        if self.role == "anonymous" or self.method == "anonymous":
+            self.auth = {'AuthMethod': "anonymous"}
+        else:
+            if self.user is None:
+                print "Error: must specify a username with -u"
+                self.usage()
+
+            if self.password is None:
+                try:
+                    self.password = getpass.getpass()
+                except (EOFError, KeyboardInterrupt):
+                    print
+                    sys.exit(0)
+
+            self.auth = {'AuthMethod': self.method,
+                    'Username': self.user,
+                    'AuthString': self.password}
+
+            if self.role is not None:
+                self.auth['Role'] = self.role
+
+    def begin(self):
+        if self.calls:
+            raise Exception, "multicall already in progress"
+
+        self.multi = True
+
+    def commit(self):
+
+        if calls:
+            ret = []
+            self.multi = False
+            results = system.multicall(calls)
+            for result in results:
+                if type(result) == type({}):
+                    raise xmlrpclib.Fault(item['faultCode'], item['faultString'])
+                elif type(result) == type([]):
+                    ret.append(result[0])
+                else:
+                    raise ValueError, "unexpected type in multicall result"
+        else:
+            ret = None
 
-            # Build up multi-line command
-            command += line
+        self.calls = []
+        self.multi = False
 
-            # Blank line or first line does not end in :
-            if line == "" or (command == line and line[-1] != ':'):
-                break
+        return ret
 
-            command += os.linesep
+    class Callable:
+        """
+        Wrapper to call a method either directly or remotely. Initialize
+        with no arguments to use as a dummy class to support tab
+        completion of API methods with dots in their names (e.g.,
+        system.listMethods).
+        """
 
-        # Blank line
-        if command == "":
-            continue
-        # Quit
-        elif command in ["q", "quit", "exit"]:
-            break
+        def __init__(self, shell, method = None):
+            self.shell=shell
+            self.name = method
 
+            if method is not None:
+                # Figure out if the function requires an authentication
+                # structure as its first argument.
+                self.auth = False
+            
+                try:
+                    func = shell.api.callable(method)
+                    if func.accepts and \
+                       (isinstance(func.accepts[0], Auth) or \
+                        (isinstance(func.accepts[0], Mixed) and \
+                         filter(lambda param: isinstance(param, Auth), func.accepts[0]))):
+                        self.auth = True
+                except:
+                    traceback.print_exc()
+                    # XXX Ignore undefined methods for now
+                    pass
+
+                if shell.server is not None:
+                    self.func = getattr(shell.server, method)
+                else:
+                    self.func = func
+
+        def __call__(self, *args, **kwds):
+            """
+            Automagically add the authentication structure if the function
+            requires it and it has not been specified.
+            """
+
+            if self.auth and \
+               (not args or not isinstance(args[0], dict) or \
+                (not args[0].has_key('AuthMethod') and \
+                 not args[0].has_key('session'))):
+                args = (self.shell.auth,) + args
+
+            if self.shell.multi:
+                self.shell.calls.append({'methodName': self.name, 'params': list(args)})
+                return None
+            else:
+                return self.func(*args, **kwds)
+
+    def init_methods (self):
+        # makes methods defined on self
+        for method in PLC.Methods.methods:
+            # ignore path-defined methods for now
+            if "." not in method:
+                setattr(self,method,Shell.Callable(self,method))
+
+    def init_globals (self):
+        # Define all methods in the global namespace to support tab completion
+        for method in PLC.Methods.methods:
+            paths = method.split(".")
+            if len(paths) > 1:
+                first = paths.pop(0)
+                if first not in globals():
+                    globals()[first] = Shell.Callable(self)
+                obj = globals()[first]
+                for path in paths:
+                    if not hasattr(obj, path):
+                        if path == paths[-1]:
+                            setattr(obj, path, Shell.Callable(self,method))
+                        else:
+                            setattr(obj, path, Shell.Callable(self))
+                    obj = getattr(obj, path)
+            else:
+                globals()[method] = Shell.Callable(self,method)
+        # Other stuff to be made visible in globals()
+        for slot in former_globals:
+            #print 'Inserting global',slot
+            globals()[slot] = getattr(self,slot)
+
+    def run_script (self):
+        # Pop us off the argument stack
+        self.argv.pop(0)
+        execfile(self.argv[0],globals(),globals())
+        sys.exit(0)
+        
+    def show_config (self, verbose=False):
+        if self.server is None:
+            print "PlanetLab Central Direct API Access"
+            self.prompt = ""
+        elif self.auth['AuthMethod'] == "anonymous":
+            self.prompt = "[anonymous]"
+            print "Connected anonymously"
+        else:
+            self.prompt = "[%s]" % self.auth['Username']
+            print "%s connected using %s authentication" % \
+                  (self.auth['Username'], self.auth['AuthMethod'])
+
+        if verbose:
+            print 'url',self.url
+            print 'server',self.server
+            print 'method',self.method
+            print 'user',self.user,
+            print 'password',self.password
+            print 'role',self.role,
+            print 'xmlrpc',self.xmlrpc,
+            print 'multi',self.multi,
+            print 'calls',self.calls
+        
+    def run_interactive (self):
+        # Readline and tab completion support
+        import atexit
+        import readline
+        import rlcompleter
+        
+        print 'Type "system.listMethods()" or "help(method)" for more information.'
+        # Load command history
+        history_path = os.path.join(os.environ["HOME"], ".plcapi_history")
         try:
-            try:
-                # Try evaluating as an expression and printing the result
-                result = eval(command)
-                if result is not None:
-                    print result
-            except:
-                # Fall back to executing as a statement
-                exec command
-        except Exception, err:
-            traceback.print_exc()
-
-except EOFError:
-    print
-    pass
+            file(history_path, 'a').close()
+            readline.read_history_file(history_path)
+            atexit.register(readline.write_history_file, history_path)
+        except IOError:
+            pass
+        
+        # Enable tab completion
+        readline.parse_and_bind("tab: complete")
+        
+        try:
+            while True:
+                command = ""
+                while True:
+                    # Get line
+                    try:
+                        if command == "":
+                            sep = ">>> "
+                        else:
+                            sep = "... "
+                        line = raw_input(self.prompt + sep)
+                    # Ctrl-C
+                    except KeyboardInterrupt:
+                        command = ""
+                        print
+                        break
+        
+                    # Build up multi-line command
+                    command += line
+        
+                    # Blank line or first line does not end in :
+                    if line == "" or (command == line and line[-1] != ':'):
+                        break
+        
+                    command += os.linesep
+        
+                # Blank line
+                if command == "":
+                    continue
+                # Quit
+                elif command in ["q", "quit", "exit"]:
+                    break
+        
+                try:
+                    try:
+                        # Try evaluating as an expression and printing the result
+                        result = eval(command)
+                        if result is not None:
+                            print result
+                    except SyntaxError:
+                        # Fall back to executing as a statement
+                        exec command
+                except Exception, err:
+                    traceback.print_exc()
+        
+        except EOFError:
+            print
+            pass
+
+    # support former behaviour
+    def run (self):
+        if len(self.argv) < 2 or not os.path.exists(self.argv[1]):
+            # Parse options if called interactively
+            self.init_from_argv()
+        self.init_connection()
+        self.init_globals()
+        # If called by a script
+        if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
+#            self.show_config()
+            self.run_script()
+        else:
+            self.show_config()
+            self.run_interactive()
+
+    # does not run anything, support for multi-plc, see e.g. TestPeers.py 
+    def init(self):
+        self.init_from_argv()
+        self.init_connection()
+        self.init_methods()
+
+if __name__ == '__main__':
+    Shell(sys.argv).run()