for federation : Peers and ForeignNodes
[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,v 1.15 2006/11/02 18:32:55 mlhuang Exp $
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
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:
96                 self.log(0, runtime, *args)
97                 
98             return result
99
100         except PLCFault, fault:
101             # Prepend method name to expected faults
102             fault.faultString = self.name + ": " + fault.faultString
103             runtime = time.time() - start
104             self.log(fault.faultCode, runtime, *args)
105             raise fault
106
107     def log(self, fault_code, runtime, *args):
108         """
109         Log the transaction 
110         """     
111
112         # Do not log system or Get calls
113         if self.name.startswith('system') or self.name.startswith('Get'):
114             return False
115
116         # Create a new event
117         event = Event(self.api)
118         event['fault_code'] = fault_code
119         event['runtime'] = runtime
120
121         # Redact passwords and sessions
122         if args and isinstance(args[0], dict):
123             for password in 'AuthString', 'session':
124                 if args[0].has_key(password):
125                     auth = args[0].copy()
126                     auth[password] = "Removed by API"
127                     args = (auth,) + args[1:]
128
129         # Log call representation
130         # XXX Truncate to avoid DoS
131         event['call'] = self.name + pprint.saferepr(args)
132
133         # Both users and nodes can call some methods
134         if isinstance(self.caller, Person):
135             event['person_id'] = self.caller['person_id']
136         elif isinstance(self.caller, Node):
137             event['node_id'] = self.caller['node_id']
138
139         event.sync(commit = False)
140
141         # XXX object_ids is currently defined as a class variable
142         if hasattr(self, 'object_ids'):
143             for object_id in self.object_ids:
144                 event.add_object(object_id, commit = False)
145
146         # Commit
147         event.sync(commit = True)
148
149     def help(self, indent = "  "):
150         """
151         Text documentation for the method.
152         """
153
154         (min_args, max_args, defaults) = self.args()
155
156         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
157
158         text += "Description:\n\n"
159         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
160         text += "\n".join(lines) + "\n\n"
161
162         text += "Allowed Roles:\n\n"
163         if not self.roles:
164             roles = ["any"]
165         else:
166             roles = self.roles
167         text += indent + ", ".join(roles) + "\n\n"
168
169         def param_text(name, param, indent, step):
170             """
171             Format a method parameter.
172             """
173
174             text = indent
175
176             # Print parameter name
177             if name:
178                 param_offset = 32
179                 text += name.ljust(param_offset - len(indent))
180             else:
181                 param_offset = len(indent)
182
183             # Print parameter type
184             param_type = python_type(param)
185             text += xmlrpc_type(param_type) + "\n"
186
187             # Print parameter documentation right below type
188             if isinstance(param, Parameter):
189                 wrapper = textwrap.TextWrapper(width = 70,
190                                                initial_indent = " " * param_offset,
191                                                subsequent_indent = " " * param_offset)
192                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
193                 param = param.type
194
195             text += "\n"
196
197             # Indent struct fields and mixed types
198             if isinstance(param, dict):
199                 for name, subparam in param.iteritems():
200                     text += param_text(name, subparam, indent + step, step)
201             elif isinstance(param, Mixed):
202                 for subparam in param:
203                     text += param_text(name, subparam, indent + step, step)
204             elif isinstance(param, (list, tuple)):
205                 for subparam in param:
206                     text += param_text("", subparam, indent + step, step)
207
208             return text
209
210         text += "Parameters:\n\n"
211         for name, param in zip(max_args, self.accepts):
212             text += param_text(name, param, indent, indent)
213
214         text += "Returns:\n\n"
215         text += param_text("", self.returns, indent, indent)
216
217         return text
218
219     def args(self):
220         """
221         Returns a tuple:
222
223         ((arg1_name, arg2_name, ...),
224          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
225          (None, None, ..., optional1_default, optional2_default, ...))
226
227         That represents the minimum and maximum sets of arguments that
228         this function accepts and the defaults for the optional arguments.
229         """
230
231         # Inspect call. Remove self from the argument list.
232         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
233         defaults = self.call.func_defaults
234         if defaults is None:
235             defaults = ()
236
237         min_args = max_args[0:len(max_args) - len(defaults)]
238         defaults = tuple([None for arg in min_args]) + defaults
239         
240         return (min_args, max_args, defaults)
241
242     def type_check(self, name, value, expected, args):
243         """
244         Checks the type of the named value against the expected type,
245         which may be a Python type, a typed value, a Parameter, a
246         Mixed type, or a list or dictionary of possibly mixed types,
247         values, Parameters, or Mixed types.
248         
249         Extraneous members of lists must be of the same type as the
250         last specified type. For example, if the expected argument
251         type is [int, bool], then [1, False] and [14, True, False,
252         True] are valid, but [1], [False, 1] and [14, True, 1] are
253         not.
254
255         Extraneous members of dictionaries are ignored.
256         """
257
258         # If any of a number of types is acceptable
259         if isinstance(expected, Mixed):
260             for item in expected:
261                 try:
262                     self.type_check(name, value, item, args)
263                     return
264                 except PLCInvalidArgument, fault:
265                     pass
266             xmlrpc_types = [xmlrpc_type(item) for item in expected]
267             raise PLCInvalidArgument("expected %s, got %s" % \
268                                      (" or ".join(xmlrpc_types),
269                                       xmlrpc_type(type(value))),
270                                      name)
271
272         # If an authentication structure is expected, save it and
273         # authenticate after basic type checking is done.
274         if isinstance(expected, Auth):
275             auth = expected
276         else:
277             auth = None
278
279         # Get actual expected type from within the Parameter structure
280         if isinstance(expected, Parameter):
281             min = expected.min
282             max = expected.max
283             nullok = expected.nullok
284             expected = expected.type
285         else:
286             min = None
287             max = None
288             nullok = False
289
290         expected_type = python_type(expected)
291
292         # If value can be NULL
293         if value is None and nullok:
294             return
295
296         # Strings are a special case. Accept either unicode or str
297         # types if a string is expected.
298         if expected_type in StringTypes and isinstance(value, StringTypes):
299             pass
300
301         # Integers and long integers are also special types. Accept
302         # either int or long types if an int or long is expected.
303         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
304             pass
305
306         elif not isinstance(value, expected_type):
307             raise PLCInvalidArgument("expected %s, got %s" % \
308                                      (xmlrpc_type(expected_type),
309                                       xmlrpc_type(type(value))),
310                                      name)
311
312         # If a minimum or maximum (length, value) has been specified
313         if expected_type in StringTypes:
314             if min is not None and \
315                len(value.encode(self.api.encoding)) < min:
316                 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
317             if max is not None and \
318                len(value.encode(self.api.encoding)) > max:
319                 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
320         else:
321             if min is not None and value < min:
322                 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
323             if max is not None and value > max:
324                 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
325
326         # If a list with particular types of items is expected
327         if isinstance(expected, (list, tuple)):
328             for i in range(len(value)):
329                 if i >= len(expected):
330                     j = len(expected) - 1
331                 else:
332                     j = i
333                 self.type_check(name + "[]", value[i], expected[j], args)
334
335         # If a struct with particular (or required) types of items is
336         # expected.
337         elif isinstance(expected, dict):
338             for key in value.keys():
339                 if key in expected:
340                     self.type_check(name + "['%s']" % key, value[key], expected[key], args)
341             for key, subparam in expected.iteritems():
342                 if isinstance(subparam, Parameter) and \
343                    subparam.optional is not None and \
344                    not subparam.optional and key not in value.keys():
345                     raise PLCInvalidArgument("'%s' not specified" % key, name)
346
347         if auth is not None:
348             auth.check(self, *args)
349
350 def python_type(arg):
351     """
352     Returns the Python type of the specified argument, which may be a
353     Python type, a typed value, or a Parameter.
354     """
355
356     if isinstance(arg, Parameter):
357         arg = arg.type
358
359     if isinstance(arg, type):
360         return arg
361     else:
362         return type(arg)
363
364 def xmlrpc_type(arg):
365     """
366     Returns the XML-RPC type of the specified argument, which may be a
367     Python type, a typed value, or a Parameter.
368     """
369
370     arg_type = python_type(arg)
371
372     if arg_type == NoneType:
373         return "nil"
374     elif arg_type == IntType or arg_type == LongType:
375         return "int"
376     elif arg_type == bool:
377         return "boolean"
378     elif arg_type == FloatType:
379         return "double"
380     elif arg_type in StringTypes:
381         return "string"
382     elif arg_type == ListType or arg_type == TupleType:
383         return "array"
384     elif arg_type == DictType:
385         return "struct"
386     elif arg_type == Mixed:
387         # Not really an XML-RPC type but return "mixed" for
388         # documentation purposes.
389         return "mixed"
390     else:
391         raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type