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