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