- fix typo
[plcapi.git] / PLC / Shell.py
1 #!/usr/bin/python
2 #
3 # Interactive shell for testing PLCAPI
4 #
5 # Mark Huang <mlhuang@cs.princeton.edu>
6 # Copyright (C) 2005 The Trustees of Princeton University
7 #
8 # $Id: Shell.py,v 1.2 2007/01/10 21:04:40 mlhuang Exp $
9 #
10
11 import pydoc
12 import xmlrpclib
13
14 from PLC.API import PLCAPI
15 from PLC.Parameter import Mixed
16 from PLC.Auth import Auth
17 from PLC.Config import Config
18 from PLC.Method import Method
19 from PLC.PyCurl import PyCurlTransport
20 import PLC.Methods
21
22 class Callable:
23     """
24     Wrapper to call a method either directly or remotely and
25     automagically add the authentication structure if necessary.
26     """
27
28     def __init__(self, shell, name, func, auth = None):
29         self.shell = shell
30         self.name = name
31         self.func = func
32         self.auth = auth
33
34     def __call__(self, *args, **kwds):
35         """
36         Automagically add the authentication structure if the function
37         requires it and it has not been specified.
38         """
39
40         if self.auth and \
41            (not args or not isinstance(args[0], dict) or \
42             (not args[0].has_key('AuthMethod') and \
43              not args[0].has_key('session'))):
44             args = (self.auth,) + args
45
46         if self.shell.multi:
47             self.shell.calls.append({'methodName': self.name, 'params': list(args)})
48             return None
49         else:
50             return self.func(*args, **kwds)
51
52 class Shell:
53     def __init__(self,
54                  # Add API functions to global scope
55                  globals = None,
56                  # Configuration file
57                  config = None,
58                  # XML-RPC server
59                  url = None, xmlrpc = False, cacert = None,
60                  # API authentication
61                  method = None, role = None, user = None, password = None):
62         """
63         Initialize a new shell instance. Re-initializes globals.
64         """
65
66         try:
67             # If any XML-RPC options have been specified, do not try
68             # connecting directly to the DB.
69             if (url, method, user, password, role, cacert, xmlrpc) != \
70                    (None, None, None, None, None, None, False):
71                 raise Exception
72
73             # Otherwise, first try connecting directly to the DB. This
74             # absolutely requires a configuration file; the API
75             # instance looks for one in a default location if one is
76             # not specified. If this fails, try connecting to the API
77             # server via XML-RPC.
78             if config is None:
79                 self.api = PLCAPI()
80             else:
81                 self.api = PLCAPI(config)
82             self.config = self.api.config
83             self.url = None
84             self.server = None
85         except Exception, err:
86             # Try connecting to the API server via XML-RPC
87             self.api = PLCAPI(None)
88
89             try:
90                 if config is None:
91                     self.config = Config()
92                 else:
93                     self.config = Config(config)
94             except Exception, err:
95                 # Try to continue if no configuration file is available
96                 self.config = None
97
98             if url is None:
99                 if self.config is None:
100                     raise Exception, "Must specify API URL"
101
102                 url = "https://" + self.config.PLC_API_HOST + \
103                       ":" + str(self.config.PLC_API_PORT) + \
104                       "/" + self.config.PLC_API_PATH + "/"
105
106             if cacert is None and self.config is not None:
107                 cacert = self.config.PLC_API_CA_SSL_CRT
108
109             self.url = url
110             if cacert is not None:
111                 self.server = xmlrpclib.ServerProxy(url, PyCurlTransport(url, cacert), allow_none = 1)
112             else:
113                 self.server = xmlrpclib.ServerProxy(url, allow_none = 1)
114
115         # Set up authentication structure
116
117         # Default is to use capability authentication
118         if (method, user, password) == (None, None, None):
119             method = "capability"
120
121         if method == "capability":
122             # Load defaults from configuration file if using capability
123             # authentication.
124             if user is None and self.config is not None:
125                 user = self.config.PLC_API_MAINTENANCE_USER
126             if password is None and self.config is not None:
127                 password = self.config.PLC_API_MAINTENANCE_PASSWORD
128             if role is None:
129                 role = "admin"
130         elif method is None:
131             # Otherwise, default to password authentication
132             method = "password"
133
134         if role == "anonymous" or method == "anonymous":
135             self.auth = {'AuthMethod': "anonymous"}
136         else:
137             if user is None:
138                 raise Exception, "Must specify username"
139
140             if password is None:
141                 raise Exception, "Must specify password"
142
143             self.auth = {'AuthMethod': method,
144                          'Username': user,
145                          'AuthString': password}
146
147             if role is not None:
148                 self.auth['Role'] = role
149
150         for method in PLC.Methods.methods:
151             api_function = self.api.callable(method)
152
153             if self.server is None:
154                 # Can just call it directly
155                 func = api_function
156             else:
157                 func = getattr(self.server, method)
158
159             # If the function requires an authentication structure as
160             # its first argument, automagically add an auth struct to
161             # the call.
162             if api_function.accepts and \
163                (isinstance(api_function.accepts[0], Auth) or \
164                 (isinstance(api_function.accepts[0], Mixed) and \
165                  filter(lambda param: isinstance(param, Auth), api_function.accepts[0]))):
166                 auth = self.auth
167             else:
168                 auth = None
169
170             callable = Callable(self, method, func, auth)
171
172             # Add to ourself and the global environment. Add dummy
173             # subattributes to support tab completion of methods with
174             # dots in their names (e.g., system.listMethods).
175             class Dummy: pass
176             paths = method.split(".")
177             if len(paths) > 1:
178                 first = paths.pop(0)
179
180                 if not hasattr(self, first):
181                     obj = Dummy()
182                     setattr(self, first, obj)
183                     # Also add to global environment if specified
184                     if globals is not None:
185                         globals[first] = obj
186
187                 obj = getattr(self, first)
188
189                 for path in paths:
190                     if not hasattr(obj, path):
191                         if path == paths[-1]:
192                             setattr(obj, path, callable)
193                         else:
194                             setattr(obj, path, Dummy())
195                     obj = getattr(obj, path)
196             else:
197                 setattr(self, method, callable)
198                 # Also add to global environment if specified
199                 if globals is not None:
200                     globals[method] = callable
201
202         # Override help(), begin(), and commit()
203         if globals is not None:
204             globals['help'] = self.help
205             globals['begin'] = self.begin
206             globals['commit'] = self.commit
207
208         # Multicall support
209         self.calls = []
210         self.multi = False
211
212     def help(self, topic = None):
213         if isinstance(topic, Callable):
214             pydoc.pager(self.system.methodHelp(topic.name))
215         else:
216             pydoc.help(topic)
217
218     def begin(self):
219         if self.calls:
220             raise Exception, "multicall already in progress"
221
222         self.multi = True
223
224     def commit(self):
225         if self.calls:
226             ret = []
227             self.multi = False
228             results = self.system.multicall(self.calls)
229             for result in results:
230                 if type(result) == type({}):
231                     raise xmlrpclib.Fault(result['faultCode'], result['faultString'])
232                 elif type(result) == type([]):
233                     ret.append(result[0])
234                 else:
235                     raise ValueError, "unexpected type in multicall result"
236         else:
237             ret = None
238
239         self.calls = []
240         self.multi = False
241
242         return ret