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