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