oops - usage was broken, still referred to global
[plcapi.git] / 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.14 2006/12/05 16:25:26 thierry Exp $
9 #
10
11 import os, sys
12 import traceback
13 import getopt
14 import pydoc
15 import pg
16 import xmlrpclib
17 import getpass
18
19 from PLC.API import PLCAPI
20 from PLC.Parameter import Mixed
21 from PLC.Auth import Auth
22 from PLC.Config import Config
23 from PLC.Method import Method
24 import PLC.Methods
25
26 # the list of globals formerly defined by Shell.py before it was made a class
27 former_globals = ['api','auth','config','begin','commit','calls']
28
29 pyhelp = help
30 def help(thing):
31     """
32     Override builtin help() function to support calling help(method).
33     """
34
35     # help(method)
36     if isinstance(thing, Shell.Callable) and thing.name is not None:
37         pydoc.pager(system.methodHelp(thing.name))
38         return
39
40     # help(help)
41     if thing == help:
42         thing = pyhelp
43
44     # help(...)
45     pyhelp(thing)
46
47 ####################
48 class Shell:
49
50     def __init__ (self,argv,config=None):
51
52         # Defaults
53         if config is not None:
54             self.config=config
55         else:
56             self.config = "/etc/planetlab/plc_config"
57         self.url = None
58         self.method = None
59         self.user = None
60         self.password = None
61         self.role = None
62         self.xmlrpc = False
63         self.server = None
64
65         # More convenient multicall support
66         self.multi = False
67         self.calls = []
68         self.argv = argv
69
70     def init_from_argv (self):
71
72         try:
73             (opts, argv) = getopt.getopt(self.argv[1:],
74                                          "f:h:m:u:p:r:x",
75                                          ["config=", "cfg=", "file=",
76                                           "host=","url=",
77                                           "method=",
78                                           "username=", "user=",
79                                           "password=", "pass=", "authstring=",
80                                           "role=",
81                                           "xmlrpc",
82                                           "help"])
83         except getopt.GetoptError, err:
84             print "Error: ", err.msg
85             self.usage(self.argv)
86                 
87         for (opt, optval) in opts:
88             if opt == "-f" or opt == "--config" or opt == "--cfg" or opt == "--file":
89                 self.config = optval
90             elif opt == "-h" or opt == "--host" or opt == "--url":
91                 self.url = optval
92             elif opt == "-m" or opt == "--method":
93                 self.method = optval
94             elif opt == "-u" or opt == "--username" or opt == "--user":
95                 self.user = optval
96             elif opt == "-p" or opt == "--password" or opt == "--pass" or opt == "--authstring":
97                 self.password = optval
98             elif opt == "-r" or opt == "--role":
99                 self.role = optval
100             elif opt == "-x" or opt == "--xmlrpc":
101                 self.xmlrpc = True
102             elif opt == "--help":
103                 self.usage(self.argv)
104
105     def usage(self,argv):
106         print "Usage: %s [OPTION]..." % argv[0]
107         print "Options:"
108         print "     -f, --config=FILE       PLC configuration file"
109         print "     -h, --url=URL           API URL"
110         print "     -m, --method=METHOD     API authentication method"
111         print "     -u, --user=EMAIL        API user name"
112         print "     -p, --password=STRING   API password"
113         print "     -r, --role=ROLE         API role"
114         print "     -x, --xmlrpc            Use XML-RPC interface"
115         print "     --help                  This message"
116         sys.exit(1)
117
118     def init_connection(self):
119
120         # Append PLC to the system path
121         sys.path.append(os.path.dirname(os.path.realpath(self.argv[0])))
122
123         try:
124             # If any XML-RPC options have been specified, do not try
125             # connecting directly to the DB.
126             if (self.url, self.method, self.user, self.password, self.role, self.xmlrpc) != \
127                    (None, None, None, None, None, False):
128                 raise Exception
129         
130             # Otherwise, first try connecting directly to the DB. If this
131             # fails, try connecting to the API server via XML-RPC.
132             self.api = PLCAPI(self.config)
133             self.config = self.api.config
134             self.server = None
135         except:
136             # Try connecting to the API server via XML-RPC
137             self.api = PLCAPI(None)
138             self.config = Config(self.config)
139
140             if self.url is None:
141                 if int(self.config.PLC_API_PORT) == 443:
142                     self.url = "https://"
143                 else:
144                     self.url = "http://"
145                     self.url += config.PLC_API_HOST + \
146                                 ":" + str(config.PLC_API_PORT) + \
147                                 "/" + config.PLC_API_PATH + "/"
148                     
149             self.server = xmlrpclib.ServerProxy(self.url, allow_none = 1)
150
151         # Default is to use capability authentication
152         if (self.method, self.user, self.password) == (None, None, None):
153             self.method = "capability"
154
155         if self.method == "capability":
156             if self.user is None:
157                 self.user = self.config.PLC_API_MAINTENANCE_USER
158             if self.password is None:
159                 self.password = self.config.PLC_API_MAINTENANCE_PASSWORD
160             if self.role is None:
161                 self.role = "admin"
162         elif self.method is None:
163             self.method = "password"
164
165         if self.role == "anonymous" or self.method == "anonymous":
166             self.auth = {'AuthMethod': "anonymous"}
167         else:
168             if self.user is None:
169                 print "Error: must specify a username with -u"
170                 self.usage()
171
172             if self.password is None:
173                 try:
174                     self.password = getpass.getpass()
175                 except (EOFError, KeyboardInterrupt):
176                     print
177                     sys.exit(0)
178
179             self.auth = {'AuthMethod': self.method,
180                     'Username': self.user,
181                     'AuthString': self.password}
182
183             if self.role is not None:
184                 self.auth['Role'] = self.role
185
186     def begin(self):
187         if self.calls:
188             raise Exception, "multicall already in progress"
189
190         self.multi = True
191
192     def commit(self):
193
194         if calls:
195             ret = []
196             self.multi = False
197             results = system.multicall(calls)
198             for result in results:
199                 if type(result) == type({}):
200                     raise xmlrpclib.Fault(item['faultCode'], item['faultString'])
201                 elif type(result) == type([]):
202                     ret.append(result[0])
203                 else:
204                     raise ValueError, "unexpected type in multicall result"
205         else:
206             ret = None
207
208         self.calls = []
209         self.multi = False
210
211         return ret
212
213     class Callable:
214         """
215         Wrapper to call a method either directly or remotely. Initialize
216         with no arguments to use as a dummy class to support tab
217         completion of API methods with dots in their names (e.g.,
218         system.listMethods).
219         """
220
221         def __init__(self, shell, method = None):
222             self.shell=shell
223             self.name = method
224
225             if method is not None:
226                 # Figure out if the function requires an authentication
227                 # structure as its first argument.
228                 self.auth = False
229             
230                 try:
231                     func = shell.api.callable(method)
232                     if func.accepts and \
233                        (isinstance(func.accepts[0], Auth) or \
234                         (isinstance(func.accepts[0], Mixed) and \
235                          filter(lambda param: isinstance(param, Auth), func.accepts[0]))):
236                         self.auth = True
237                 except:
238                     traceback.print_exc()
239                     # XXX Ignore undefined methods for now
240                     pass
241
242                 if shell.server is not None:
243                     self.func = getattr(shell.server, method)
244                 else:
245                     self.func = func
246
247         def __call__(self, *args, **kwds):
248             """
249             Automagically add the authentication structure if the function
250             requires it and it has not been specified.
251             """
252
253             if self.auth and \
254                (not args or not isinstance(args[0], dict) or \
255                 (not args[0].has_key('AuthMethod') and \
256                  not args[0].has_key('session'))):
257                 args = (self.shell.auth,) + args
258
259             if self.shell.multi:
260                 self.shell.calls.append({'methodName': self.name, 'params': list(args)})
261                 return None
262             else:
263                 return self.func(*args, **kwds)
264
265     def init_methods (self):
266         # makes methods defined on self
267         for method in PLC.Methods.methods:
268             # ignore path-defined methods for now
269             if "." not in method:
270                 setattr(self,method,Shell.Callable(self,method))
271
272     def init_globals (self):
273         # Define all methods in the global namespace to support tab completion
274         for method in PLC.Methods.methods:
275             paths = method.split(".")
276             if len(paths) > 1:
277                 first = paths.pop(0)
278                 if first not in globals():
279                     globals()[first] = Shell.Callable(self)
280                 obj = globals()[first]
281                 for path in paths:
282                     if not hasattr(obj, path):
283                         if path == paths[-1]:
284                             setattr(obj, path, Shell.Callable(self,method))
285                         else:
286                             setattr(obj, path, Shell.Callable(self))
287                     obj = getattr(obj, path)
288             else:
289                 globals()[method] = Shell.Callable(self,method)
290         # Other stuff to be made visible in globals()
291         for slot in former_globals:
292             #print 'Inserting global',slot
293             globals()[slot] = getattr(self,slot)
294
295     def run_script (self):
296         # Pop us off the argument stack
297         self.argv.pop(0)
298         execfile(self.argv[0],globals(),globals())
299         sys.exit(0)
300         
301     def show_config (self, verbose=False):
302         if self.server is None:
303             print "PlanetLab Central Direct API Access"
304             self.prompt = ""
305         elif self.auth['AuthMethod'] == "anonymous":
306             self.prompt = "[anonymous]"
307             print "Connected anonymously"
308         else:
309             self.prompt = "[%s]" % self.auth['Username']
310             print "%s connected using %s authentication" % \
311                   (self.auth['Username'], self.auth['AuthMethod'])
312
313         if verbose:
314             print 'url',self.url
315             print 'server',self.server
316             print 'method',self.method
317             print 'user',self.user,
318             print 'password',self.password
319             print 'role',self.role,
320             print 'xmlrpc',self.xmlrpc,
321             print 'multi',self.multi,
322             print 'calls',self.calls
323         
324     def run_interactive (self):
325         # Readline and tab completion support
326         import atexit
327         import readline
328         import rlcompleter
329         
330         print 'Type "system.listMethods()" or "help(method)" for more information.'
331         # Load command history
332         history_path = os.path.join(os.environ["HOME"], ".plcapi_history")
333         try:
334             file(history_path, 'a').close()
335             readline.read_history_file(history_path)
336             atexit.register(readline.write_history_file, history_path)
337         except IOError:
338             pass
339         
340         # Enable tab completion
341         readline.parse_and_bind("tab: complete")
342         
343         try:
344             while True:
345                 command = ""
346                 while True:
347                     # Get line
348                     try:
349                         if command == "":
350                             sep = ">>> "
351                         else:
352                             sep = "... "
353                         line = raw_input(self.prompt + sep)
354                     # Ctrl-C
355                     except KeyboardInterrupt:
356                         command = ""
357                         print
358                         break
359         
360                     # Build up multi-line command
361                     command += line
362         
363                     # Blank line or first line does not end in :
364                     if line == "" or (command == line and line[-1] != ':'):
365                         break
366         
367                     command += os.linesep
368         
369                 # Blank line
370                 if command == "":
371                     continue
372                 # Quit
373                 elif command in ["q", "quit", "exit"]:
374                     break
375         
376                 try:
377                     try:
378                         # Try evaluating as an expression and printing the result
379                         result = eval(command)
380                         if result is not None:
381                             print result
382                     except SyntaxError:
383                         # Fall back to executing as a statement
384                         exec command
385                 except Exception, err:
386                     traceback.print_exc()
387         
388         except EOFError:
389             print
390             pass
391
392     # support former behaviour
393     def run (self):
394         if len(self.argv) < 2 or not os.path.exists(self.argv[1]):
395             # Parse options if called interactively
396             self.init_from_argv()
397         self.init_connection()
398         self.init_globals()
399         # If called by a script
400         if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
401 #            self.show_config()
402             self.run_script()
403         else:
404             self.show_config()
405             self.run_interactive()
406
407     # does not run anything, support for multi-plc, see e.g. TestPeers.py 
408     def init(self):
409         self.init_from_argv()
410         self.init_connection()
411         self.init_methods()
412
413 if __name__ == '__main__':
414     Shell(sys.argv).run()