import repository from arizona
[raven.git] / tools / testnodes / plcapilib.py
1 #!/usr/bin/python
2 #
3 # Wrapper around xmlrpclib for interfacing with PLCAPI servers
4 #
5 # Mark Huang <mlhuang@cs.princeton.edu>
6 # Copyright (C) 2005 The Trustees of Princeton University
7 #
8 # $Id: plcapilib.py,v 1.1 2007-09-26 15:40:48 bakers Exp $
9 #
10
11 import getpass
12 import getopt
13 import os, sys
14 import xmlrpclib
15 import re
16 import inspect
17 import pydoc
18
19 class PLCAPI:
20     def usage(self, moreusage = ""):
21         print moreusage + """
22 API options:
23         -h host         API URL (default: %s)
24         -f file         API constants file (default: %s)
25         -m method       API authentication method (default: %s)
26         -p password     API password
27         -u username     API user name
28         -r role         API role
29         -v              Be verbose
30 """ % (self.server_url,
31        self.constants_path,
32        self.constants['PL_API_CAPABILITY_AUTH_METHOD'])
33
34     def __init__(self):
35         # Debug
36         self.verbose = 0
37         
38         # API default constants file
39         self.constants_path = "/etc/planetlab/plc_api"
40
41         # API default constants (if file does not exist)
42         self.constants = {
43             'PL_API_SERVER': "www.planet-lab.org",
44             'PL_API_PATH': "/PLCAPI/",
45             'PL_API_PORT': 443,
46             'PL_API_CAPABILITY_AUTH_METHOD': "password",
47             'PL_API_CAPABILITY_PASS': "",
48             'PL_API_CAPABILITY_USERNAME': ""
49         }
50         self.server_url = "https://www.planet-lab.org/PLCAPI/"
51         self.role = None
52
53         # Multicall
54         self.calls = []
55         self.multi = False
56
57     def getopt(self, args, shortopts = "", longopts = [], moreusage = ""):
58         # (Re)parse constants file if available
59         self.parse_constants_file(self.constants_path)
60
61         # Standard API options
62         shortopts += "h:f:m:p:u:r:v"
63         longopts += ["host=", "constants=", "method=", "password=", "username=", "role=", "verbose", "help"]
64
65         try:
66             (opts, argv) = getopt.getopt(args[1:], shortopts, longopts)
67         except getopt.GetoptError, err:
68             print "Error: " + err.msg
69             self.usage(moreusage)
70             sys.exit(1)
71             
72         moreopts = {}
73         for (opt, optval) in opts:
74             if opt == "-h" or opt == "--host":
75                 self.server_url = optval
76             elif opt == "-f" or opt == "--constants":
77                 self.parse_constants_file(optval)
78             elif opt == "-m" or opt == "--method":
79                 self.constants['PL_API_CAPABILITY_AUTH_METHOD'] = optval
80             elif opt == "-p" or opt == "--password":
81                 self.constants['PL_API_CAPABILITY_PASS'] = optval
82             elif opt == "-u" or opt == "--username":
83                 self.constants['PL_API_CAPABILITY_USERNAME'] = optval
84             elif opt == "-r" or opt == "--role":
85                 self.role = optval
86             elif opt == "-v" or opt == "--verbose":
87                 self.verbose += 1
88             elif opt == "--help":
89                 self.usage(moreusage)
90                 sys.exit(0)
91             else:
92                 moreopts[opt] = optval
93
94         # Capability authentication only available to admins
95         if self.role is None and \
96            self.constants['PL_API_CAPABILITY_AUTH_METHOD'] == "capability":
97             self.role = "admin"
98
99         # Both a role and a username must be specified if not anonymous
100         if self.role is None or \
101            self.role != "anonymous" and not self.constants['PL_API_CAPABILITY_USERNAME']:
102             if self.role is None:
103                 print "Error: must specify a role with -r"
104             else:
105                 print "Error: must specify a username with -u"
106             self.usage(moreusage)
107             sys.exit(1)
108
109         # Password must be specified if not anonymous
110         if self.role != "anonymous" and not self.constants['PL_API_CAPABILITY_PASS']:
111             try:
112                 self.constants['PL_API_CAPABILITY_PASS'] = getpass.getpass()
113             except (EOFError, KeyboardInterrupt):
114                 print
115                 sys.exit(0)
116
117         # Setup authentication structs
118         self.anon = {
119             'AuthMethod': "anonymous"
120         }
121         if self.role == "anonymous":
122             self.auth = self.anon
123         else:
124             self.auth = {
125                 'Username': self.constants['PL_API_CAPABILITY_USERNAME'],
126                 'AuthMethod': self.constants['PL_API_CAPABILITY_AUTH_METHOD'],
127                 'AuthString': self.constants['PL_API_CAPABILITY_PASS'],
128                 'Role': self.role
129             }
130
131         # Connect to API server
132         self.server = xmlrpclib.Server(self.server_url, verbose = self.verbose)
133         if self.role != "anonymous":
134             try:
135                 self.server.AdmAuthCheck(self.auth)
136             except xmlrpclib.Fault, fault:
137                 print fault
138                 sys.exit(fault.faultCode)
139
140         # Save pointer to built-in Python help function
141         self.python_help = help
142
143         return (moreopts, argv)
144
145     def parse_constants_file(self, path):
146         """Parses the given API constants file (must be a valid Python
147         script)."""
148
149         try:
150             for line in file(path, 'r'):
151                 exec line in self.constants
152             if self.constants['PL_API_PORT'] == 443:
153                 self.server_url = "https://"
154             else:
155                 self.server_url = "http://"
156             self.server_url += self.constants['PL_API_SERVER'] + \
157                                ":" + str(self.constants['PL_API_PORT']) + \
158                                "/" + self.constants['PL_API_PATH'] + "/"
159             return True
160         except Exception, err:
161             return False
162
163     def begin(self):
164         self.multi = True
165
166     def commit(self):
167         if self.calls:
168             ret = []
169             results = self.server.system.multicall(self.calls)
170             for result in results:
171                 if type(result) == type({}):
172                     raise Fault(item['faultCode'], item['faultString'])
173                 elif type(result) == type([]):
174                     ret.append(result[0])
175                 else:
176                     raise ValueError, "unexpected type in multicall result"
177         else:
178             ret = None
179
180         self.calls = []
181         self.multi = False
182
183         return ret
184
185     def call(self, method, *params):
186         if self.multi:
187             self.calls.append({'methodName': method, 'params': list(params)})
188             return None
189         else:
190             return eval("self.server.%s(*params)" % method)
191
192     def make_definitions(self, prefix):
193         """Returns a list of code objects that can be executed by the
194         exec statement or eval()."""
195
196         # Get a list of available methods from the server
197         self.methods = self.server.system.listMethods()
198
199         calls = []
200         for method in self.methods:
201             calls.append({ 'methodName': "system.methodSignature", 'params': [method] })
202         signatures_list = [result[0] for result in self.server.system.multicall(calls)]
203
204         definitions = []
205
206         # Hack for system.* calls
207         definitions.append(compile("system = %s" % prefix, prefix, "single"))
208
209         for method, signatures in zip(self.methods, signatures_list):
210             if not signatures or min(map(len, signatures)) < 1:
211                 continue
212
213             for signature in signatures:
214                 # Pop the return value from the signature
215                 signature.pop(0)
216                 # Pop the authentication parameter, too
217                 if not re.match("system.", method):
218                     signature.pop(0)
219             
220             # Sort signatures by number of arguments
221             signatures.sort(lambda x, y: len(x) - len(y))
222             
223             # Build up function parameters and arguments to call()
224             min_args = len(signatures[0])
225             max_args = len(signatures[-1])
226             params = [arg + str(i) for i, arg in enumerate(signatures[0])]
227             params += [arg + str(min_args + i) + "=None" for i, arg in enumerate(signatures[-1][min_args:max_args])]
228             args = [arg + str(i) for i, arg in enumerate(signatures[-1])]
229             
230             # Hack for system.* calls
231             if re.match("system.", method):
232                 function = re.sub("system.", "system_", method)
233                 auth = ""
234             elif re.match("Anon", method):
235                 function = method
236                 auth = "%s.anon," % prefix
237             else:
238                 function = method
239                 auth = "%s.auth," % prefix
240             
241             definition  = "def %s(%s):" % (function, ",".join(params)) + os.linesep
242             for i in range(min_args, max_args):
243                 definition += "    if %s is None:" % args[i] + os.linesep
244                 definition += "        return %s.call('%s', %s%s)" % (prefix, method, auth, ",".join(args[0:i])) + os.linesep
245             definition += "    return %s.call('%s', %s%s)" % (prefix, method, auth, ",".join(args)) + os.linesep
246
247             if self.verbose:
248                 print definition
249
250             definitions.append(compile(definition, prefix, "single"))
251
252             # Hack for system.* calls
253             if re.match("system.", method):
254                 definitions.append(compile("%s = %s" % (method, function), prefix, "single"))
255
256         # Also override built-in Python help function
257         definitions.append(compile("help = %s.help" % prefix, prefix, "single"))
258
259         return definitions
260
261     def help(self, func):
262         """Custom help function for PLCAPI functions."""
263
264         # help(Adm...) or help(Slice...)
265         if inspect.isfunction(func) and func.__name__ in self.methods:
266             pydoc.pager(self.server.system.methodHelp(func.__name__))
267             return
268
269         # help(help)
270         if func == self.help:
271             func = self.python_help
272
273         # help(...)
274         self.python_help(func)
275
276 def plcapi(globals, args = sys.argv, shortopts = "", longopts = [], moreusage = ""):
277     """Instantiates a connection to a PLCAPI server. Attempts to parse
278     command line options and/or an API configuration file. Defines
279     functions for all known methods in the specified
280     namespace. Returns a tuple of (plcapi, moreopts, argv):
281
282     plcapi - handle to the instantiated connection
283     moreopts - dictionary of additional parsed options and their values
284     argv - non-option arguments
285
286     globals - namespace in which to define PLCAPI methods
287     args - command line argument list
288     shortopts - additional short options suitable for passing to getopt.getopt()
289     longopts - additional long options suitable for passing to getopt.getopt()
290     moreusage - additional usage information to be printed if --help is seen
291     """
292
293     __PLCAPI = PLCAPI()
294
295     # Parse command line options and API configuration file
296     (moreopts, argv) = __PLCAPI.getopt(args, shortopts, longopts, moreusage)
297
298     # Define functions for all known methods in specified namespace
299     globals['__PLCAPI'] = __PLCAPI
300     for definition in __PLCAPI.make_definitions('__PLCAPI'):
301         exec definition in globals
302
303     return (__PLCAPI, moreopts, argv)
304
305 if __name__ == '__main__':
306     if len(sys.argv) > 1 and sys.argv[1] in ['build', 'install']:
307         from distutils.core import setup
308         setup(py_modules=["plcapilib"])