Merge remote-tracking branch 'origin/pycurl' into planetlab-4_0-branch
[plcapi.git] / PLC / Method.py
1 #
2 # Base class for all PLCAPI functions
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id: Method.py 5574 2007-10-25 20:33:17Z thierry $
8 #
9
10 import xmlrpclib
11 from types import *
12 import textwrap
13 import os
14 import time
15 import pprint
16
17 from types import StringTypes
18
19 from PLC.Faults import *
20 from PLC.Parameter import Parameter, Mixed, python_type, xmlrpc_type
21 from PLC.Auth import Auth
22 from PLC.Debug import profile, log
23 from PLC.Events import Event, Events
24 from PLC.Nodes import Node, Nodes
25 from PLC.Persons import Person, Persons
26
27 class Method:
28     """
29     Base class for all PLCAPI functions. At a minimum, all PLCAPI
30     functions must define:
31
32     roles = [list of roles]
33     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
34     returns = Parameter(return_type, return_doc)
35     call(arg1, arg2, ...): method body
36
37     Argument types may be Python types (e.g., int, bool, etc.), typed
38     values (e.g., 1, True, etc.), a Parameter, or lists or
39     dictionaries of possibly mixed types, values, and/or Parameters
40     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
41
42     Once function decorators in Python 2.4 are fully supported,
43     consider wrapping calls with accepts() and returns() functions
44     instead of performing type checking manually.
45     """
46
47     # Defaults. Could implement authentication and type checking with
48     # decorators, but they are not supported in Python 2.3 and it
49     # would be hard to generate documentation without writing a code
50     # parser.
51
52     roles = []
53     accepts = []
54     returns = bool
55     status = "current"
56
57     def call(self, *args):
58         """
59         Method body for all PLCAPI functions. Must override.
60         """
61
62         return True
63
64     def __init__(self, api):
65         self.name = self.__class__.__name__
66         self.api = api
67
68         # Auth may set this to a Person instance (if an anonymous
69         # method, will remain None).
70         self.caller = None
71
72         # API may set this to a (addr, port) tuple if known
73         self.source = None
74         
75     def __call__(self, *args, **kwds):
76         """
77         Main entry point for all PLCAPI functions. Type checks
78         arguments, authenticates, and executes call().
79         """
80
81         try:
82             start = time.time()
83             (min_args, max_args, defaults) = self.args()
84                                 
85             # Check that the right number of arguments were passed in
86             if len(args) < len(min_args) or len(args) > len(max_args):
87                 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
88
89             for name, value, expected in zip(max_args, args, self.accepts):
90                 self.type_check(name, value, expected, args)
91         
92             result = self.call(*args, **kwds)
93             runtime = time.time() - start
94
95             if self.api.config.PLC_API_DEBUG or hasattr(self, 'message'):
96                 self.log(None, runtime, *args)
97                 
98             return result
99
100         except PLCFault, fault:
101         
102             caller = ""
103             if isinstance(self.caller, Person):
104                 caller = 'person_id %s'  % self.caller['person_id']
105             elif isinstance(self.caller, Node):
106                 caller = 'node_id %s'  % self.caller['node_id']
107
108             # Prepend caller and method name to expected faults
109             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
110             runtime = time.time() - start
111             self.log(fault, runtime, *args)
112             raise fault
113
114     def log(self, fault, runtime, *args):
115         """
116         Log the transaction 
117         """     
118
119         # Do not log system or Get calls
120         #if self.name.startswith('system') or self.name.startswith('Get'):
121         #    return False
122
123         # Create a new event
124         event = Event(self.api)
125         event['fault_code'] = 0
126         if fault:
127             event['fault_code'] = fault.faultCode
128         event['runtime'] = runtime
129
130         # Redact passwords and sessions
131         if args and isinstance(args[0], dict):
132             # what type of auth this is
133             if args[0].has_key('AuthMethod'):
134                 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
135                 auth_method = args[0]['AuthMethod']
136                 if auth_method in auth_methods:
137                     event['auth_type'] = auth_method
138             for password in 'AuthString', 'session':
139                 if args[0].has_key(password):
140                     auth = args[0].copy()
141                     auth[password] = "Removed by API"
142                     args = (auth,) + args[1:]
143
144         # Log call representation
145         # XXX Truncate to avoid DoS
146         event['call'] = self.name + pprint.saferepr(args)
147         event['call_name'] = self.name
148
149         # Both users and nodes can call some methods
150         if isinstance(self.caller, Person):
151             event['person_id'] = self.caller['person_id']
152         elif isinstance(self.caller, Node):
153             event['node_id'] = self.caller['node_id']
154
155         event.sync(commit = False)
156
157         if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
158             for key in self.event_objects.keys():
159                 for object_id in self.event_objects[key]:
160                     event.add_object(key, object_id, commit = False)
161         
162
163         # Set the message for this event
164         if fault:
165             event['message'] = fault.faultString
166         elif hasattr(self, 'message'):
167             event['message'] = self.message     
168         
169         # Commit
170         event.sync()
171
172     def help(self, indent = "  "):
173         """
174         Text documentation for the method.
175         """
176
177         (min_args, max_args, defaults) = self.args()
178
179         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
180
181         text += "Description:\n\n"
182         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
183         text += "\n".join(lines) + "\n\n"
184
185         text += "Allowed Roles:\n\n"
186         if not self.roles:
187             roles = ["any"]
188         else:
189             roles = self.roles
190         text += indent + ", ".join(roles) + "\n\n"
191
192         def param_text(name, param, indent, step):
193             """
194             Format a method parameter.
195             """
196
197             text = indent
198
199             # Print parameter name
200             if name:
201                 param_offset = 32
202                 text += name.ljust(param_offset - len(indent))
203             else:
204                 param_offset = len(indent)
205
206             # Print parameter type
207             param_type = python_type(param)
208             text += xmlrpc_type(param_type) + "\n"
209
210             # Print parameter documentation right below type
211             if isinstance(param, Parameter):
212                 wrapper = textwrap.TextWrapper(width = 70,
213                                                initial_indent = " " * param_offset,
214                                                subsequent_indent = " " * param_offset)
215                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
216                 param = param.type
217
218             text += "\n"
219
220             # Indent struct fields and mixed types
221             if isinstance(param, dict):
222                 for name, subparam in param.iteritems():
223                     text += param_text(name, subparam, indent + step, step)
224             elif isinstance(param, Mixed):
225                 for subparam in param:
226                     text += param_text(name, subparam, indent + step, step)
227             elif isinstance(param, (list, tuple, set)):
228                 for subparam in param:
229                     text += param_text("", subparam, indent + step, step)
230
231             return text
232
233         text += "Parameters:\n\n"
234         for name, param in zip(max_args, self.accepts):
235             text += param_text(name, param, indent, indent)
236
237         text += "Returns:\n\n"
238         text += param_text("", self.returns, indent, indent)
239
240         return text
241
242     def args(self):
243         """
244         Returns a tuple:
245
246         ((arg1_name, arg2_name, ...),
247          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
248          (None, None, ..., optional1_default, optional2_default, ...))
249
250         That represents the minimum and maximum sets of arguments that
251         this function accepts and the defaults for the optional arguments.
252         """
253
254         # Inspect call. Remove self from the argument list.
255         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
256         defaults = self.call.func_defaults
257         if defaults is None:
258             defaults = ()
259
260         min_args = max_args[0:len(max_args) - len(defaults)]
261         defaults = tuple([None for arg in min_args]) + defaults
262         
263         return (min_args, max_args, defaults)
264
265     def type_check(self, name, value, expected, args):
266         """
267         Checks the type of the named value against the expected type,
268         which may be a Python type, a typed value, a Parameter, a
269         Mixed type, or a list or dictionary of possibly mixed types,
270         values, Parameters, or Mixed types.
271         
272         Extraneous members of lists must be of the same type as the
273         last specified type. For example, if the expected argument
274         type is [int, bool], then [1, False] and [14, True, False,
275         True] are valid, but [1], [False, 1] and [14, True, 1] are
276         not.
277
278         Extraneous members of dictionaries are ignored.
279         """
280
281         # If any of a number of types is acceptable
282         if isinstance(expected, Mixed):
283             for item in expected:
284                 try:
285                     self.type_check(name, value, item, args)
286                     return
287                 except PLCInvalidArgument, fault:
288                     pass
289             raise fault
290
291         # If an authentication structure is expected, save it and
292         # authenticate after basic type checking is done.
293         if isinstance(expected, Auth):
294             auth = expected
295         else:
296             auth = None
297
298         # Get actual expected type from within the Parameter structure
299         if isinstance(expected, Parameter):
300             min = expected.min
301             max = expected.max
302             nullok = expected.nullok
303             expected = expected.type
304         else:
305             min = None
306             max = None
307             nullok = False
308
309         expected_type = python_type(expected)
310
311         # If value can be NULL
312         if value is None and nullok:
313             return
314
315         # Strings are a special case. Accept either unicode or str
316         # types if a string is expected.
317         if expected_type in StringTypes and isinstance(value, StringTypes):
318             pass
319
320         # Integers and long integers are also special types. Accept
321         # either int or long types if an int or long is expected.
322         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
323             pass
324
325         elif not isinstance(value, expected_type):
326             raise PLCInvalidArgument("expected %s, got %s" % \
327                                      (xmlrpc_type(expected_type),
328                                       xmlrpc_type(type(value))),
329                                      name)
330
331         # If a minimum or maximum (length, value) has been specified
332         if expected_type in StringTypes:
333             if min is not None and \
334                len(value.encode(self.api.encoding)) < min:
335                 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
336             if max is not None and \
337                len(value.encode(self.api.encoding)) > max:
338                 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
339         elif expected_type in (list, tuple, set):
340             if min is not None and len(value) < min:
341                 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
342             if max is not None and len(value) > max:
343                 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
344         else:
345             if min is not None and value < min:
346                 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
347             if max is not None and value > max:
348                 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
349
350         # If a list with particular types of items is expected
351         if isinstance(expected, (list, tuple, set)):
352             for i in range(len(value)):
353                 if i >= len(expected):
354                     j = len(expected) - 1
355                 else:
356                     j = i
357                 self.type_check(name + "[]", value[i], expected[j], args)
358
359         # If a struct with particular (or required) types of items is
360         # expected.
361         elif isinstance(expected, dict):
362             for key in value.keys():
363                 if key in expected:
364                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
365             for key, subparam in expected.iteritems():
366                 if isinstance(subparam, Parameter) and \
367                    subparam.optional is not None and \
368                    not subparam.optional and key not in value.keys():
369                     raise PLCInvalidArgument("'%s' not specified" % key, name)
370
371         if auth is not None:
372             auth.check(self, *args)