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