revert accidental (too early) checkin
[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.18 2006/12/15 18:36:16 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.server = None
84         except Exception, err:
85             # Try connecting to the API server via XML-RPC
86             self.api = PLCAPI(None)
87
88             if config is None:
89                 self.config = Config()
90             else:
91                 try:
92                     self.config = Config(config)
93                 except Exception, err:
94                     # Try to continue if no configuration file is available
95                     self.config = None
96                     pass
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.server = xmlrpclib.ServerProxy(url, PyCurlTransport(url, cacert), allow_none = 1)
110
111         # Set up authentication structure
112
113         # Default is to use capability authentication
114         if (method, user, password) == (None, None, None):
115             method = "capability"
116
117         if method == "capability":
118             # Load defaults from configuration file if using capability
119             # authentication.
120             if user is None and self.config is not None:
121                 user = self.config.PLC_API_MAINTENANCE_USER
122             if password is None and self.config is not None:
123                 password = self.config.PLC_API_MAINTENANCE_PASSWORD
124             if role is None:
125                 role = "admin"
126         elif method is None:
127             # Otherwise, default to password authentication
128             method = "password"
129
130         if role == "anonymous" or method == "anonymous":
131             self.auth = {'AuthMethod': "anonymous"}
132         else:
133             if user is None:
134                 raise Exception, "Must specify username"
135
136             if password is None:
137                 raise Exception, "Must specify password"
138
139             self.auth = {'AuthMethod': method,
140                          'Username': user,
141                          'AuthString': password}
142
143             if role is not None:
144                 self.auth['Role'] = role
145
146         for method in PLC.Methods.methods:
147             api_function = self.api.callable(method)
148
149             if self.server is None:
150                 # Can just call it directly
151                 func = api_function
152             else:
153                 func = getattr(self.server, method)
154
155             # If the function requires an authentication structure as
156             # its first argument, automagically add an auth struct to
157             # the call.
158             if api_function.accepts and \
159                (isinstance(api_function.accepts[0], Auth) or \
160                 (isinstance(api_function.accepts[0], Mixed) and \
161                  filter(lambda param: isinstance(param, Auth), api_function.accepts[0]))):
162                 auth = self.auth
163             else:
164                 auth = None
165
166             callable = Callable(self, method, func, auth)
167
168             # Add to ourself and the global environment. Add dummy
169             # subattributes to support tab completion of methods with
170             # dots in their names (e.g., system.listMethods).
171             class Dummy: pass
172             paths = method.split(".")
173             if len(paths) > 1:
174                 first = paths.pop(0)
175
176                 if not hasattr(self, first):
177                     obj = Dummy()
178                     setattr(self, first, obj)
179                     # Also add to global environment if specified
180                     if globals is not None:
181                         globals[first] = obj
182
183                 obj = getattr(self, first)
184
185                 for path in paths:
186                     if not hasattr(obj, path):
187                         if path == paths[-1]:
188                             setattr(obj, path, callable)
189                         else:
190                             setattr(obj, path, Dummy())
191                     obj = getattr(obj, path)
192             else:
193                 setattr(self, method, callable)
194                 # Also add to global environment if specified
195                 if globals is not None:
196                     globals[method] = callable
197
198         # Override help(), begin(), and commit()
199         if globals is not None:
200             globals['help'] = self.help
201             globals['begin'] = self.begin
202             globals['commit'] = self.commit
203
204         # Multicall support
205         self.calls = []
206         self.multi = False
207
208     def help(self, topic = None):
209         if isinstance(topic, Callable):
210             pydoc.pager(self.system.methodHelp(topic.name))
211         else:
212             pydoc.help(topic)
213
214     def begin(self):
215         if self.calls:
216             raise Exception, "multicall already in progress"
217
218         self.multi = True
219
220     def commit(self):
221         if self.calls:
222             ret = []
223             self.multi = False
224             results = self.system.multicall(self.calls)
225             for result in results:
226                 if type(result) == type({}):
227                     raise xmlrpclib.Fault(result['faultCode'], result['faultString'])
228                 elif type(result) == type([]):
229                     ret.append(result[0])
230                 else:
231                     raise ValueError, "unexpected type in multicall result"
232         else:
233             ret = None
234
235         self.calls = []
236         self.multi = False
237
238         return ret