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