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