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