- fixed logging of calls that fail at or before auth.check
[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.7 2006/10/18 19:42:46 tmack Exp $
8 #
9
10 import xmlrpclib
11 from types import *
12 import textwrap
13 import os
14 import time
15
16 from PLC.Faults import *
17 from PLC.Parameter import Parameter, Mixed
18 from PLC.Auth import Auth
19 from PLC.Debug import profile, log
20
21 class Method:
22     """
23     Base class for all PLCAPI functions. At a minimum, all PLCAPI
24     functions must define:
25
26     roles = [list of roles]
27     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
28     returns = Parameter(return_type, return_doc)
29     call(arg1, arg2, ...): method body
30
31     Argument types may be Python types (e.g., int, bool, etc.), typed
32     values (e.g., 1, True, etc.), a Parameter, or lists or
33     dictionaries of possibly mixed types, values, and/or Parameters
34     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
35
36     Once function decorators in Python 2.4 are fully supported,
37     consider wrapping calls with accepts() and returns() functions
38     instead of performing type checking manually.
39     """
40
41     # Defaults. Could implement authentication and type checking with
42     # decorators, but they are not supported in Python 2.3 and it
43     # would be hard to generate documentation without writing a code
44     # parser.
45
46     roles = []
47     accepts = []
48     returns = bool
49     status = "current"
50
51     def call(self, *args):
52         """
53         Method body for all PLCAPI functions. Must override.
54         """
55
56         return True
57
58     def __init__(self, api):
59         self.name = self.__class__.__name__
60         self.api = api
61
62         # Auth may set this to a Person instance (if an anonymous
63         # method, will remain None).
64         self.caller = None
65
66         # API may set this to a (addr, port) tuple if known
67         self.source = None
68
69         
70     def __call__(self, *args, **kwds):
71         """
72         Main entry point for all PLCAPI functions. Type checks
73         arguments, authenticates, and executes call().
74         """
75
76         try:
77             start = time.time()
78             (min_args, max_args, defaults) = self.args()
79                                 
80             # Check that the right number of arguments were passed in
81             if len(args) < len(min_args) or len(args) > len(max_args):
82                 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
83
84             for name, value, expected in zip(max_args, args, self.accepts):
85                 self.type_check(name, value, expected)
86         
87             # The first argument to all methods that require
88             # authentication, should be an Auth structure. The rest of the
89             # arguments to the call may also be used in the authentication
90             # check. For example, calls made by the Boot Manager are
91             # verified by comparing a hash of the message parameters to
92             # the value in the authentication structure.        
93
94             if len(self.accepts):
95                 auth = None
96                 if isinstance(self.accepts[0], Auth):
97                     auth = self.accepts[0]
98                 elif isinstance(self.accepts[0], Mixed):
99                     for auth in self.accepts[0]:
100                         if isinstance(auth, Auth):
101                             break
102                 if isinstance(auth, Auth):
103                     auth.check(self, *args)
104            
105             result = self.call(*args, **kwds)
106             runtime = time.time() - start
107
108             if self.api.config.PLC_API_DEBUG:
109                 self.log(0, runtime, *args)
110                 
111             return result
112
113         except PLCFault, fault:
114             # Prepend method name to expected faults
115             fault.faultString = self.name + ": " + fault.faultString
116             runtime = time.time() - start
117             self.log(fault.faultCode, runtime, *args)
118             raise fault
119
120
121     def log(self, fault_code, runtime, *args):
122         """
123         Log the transaction 
124         """     
125         # Gather necessary logging variables
126         event_type = 'Unknown'
127         object_type = 'Unknown'
128         person_id = 'Null'
129         object_ids = []
130         call_name = self.name
131         call_args = ", ".join([unicode(arg) for arg in list(args)[1:]])
132         call = "%s(%s)" % (call_name, call_args)
133                 
134         if hasattr(self, 'event_type'):
135                 event_type = self.event_type
136         if hasattr(self, 'object_type'):
137                 object_type = self.object_type
138         if self.caller:
139                 person_id = self.caller['person_id']
140         if hasattr(self, 'object_ids'):
141                 object_ids = self.object_ids 
142
143         # do not log system calls
144         if call_name.startswith('system'):
145                 return False
146         # do not log get calls
147         if call_name.startswith('Get'):
148                 return False
149         
150         sql_event = "INSERT INTO events " \
151               " (person_id, event_type, object_type, fault_code, call, runtime) VALUES" \
152               " (%(person_id)s, '%(event_type)s', '%(object_type)s'," \
153               "  %(fault_code)d, '%(call)s', %(runtime)f)" 
154         self.api.db.do(sql_event % locals())    
155
156         print self.api.db.last_insert_id('events', 'event_id')  
157         # log objects affected
158         for object_id in object_ids:
159                 event_id =  self.api.db.last_insert_id('events', 'event_id')
160                 sql_objects = "INSERT INTO event_object (event_id, object_id) VALUES" \
161                         " (%(event_id)d, %(object_id)d) "  % (locals()) 
162                 self.api.db.do(sql_objects)
163                         
164         self.api.db.commit()            
165         
166
167     def help(self, indent = "  "):
168         """
169         Text documentation for the method.
170         """
171
172         (min_args, max_args, defaults) = self.args()
173
174         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
175
176         text += "Description:\n\n"
177         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
178         text += "\n".join(lines) + "\n\n"
179
180         text += "Allowed Roles:\n\n"
181         if not self.roles:
182             roles = ["any"]
183         else:
184             roles = self.roles
185         text += indent + ", ".join(roles) + "\n\n"
186
187         def param_text(name, param, indent, step):
188             """
189             Format a method parameter.
190             """
191
192             text = indent
193
194             # Print parameter name
195             if name:
196                 param_offset = 32
197                 text += name.ljust(param_offset - len(indent))
198             else:
199                 param_offset = len(indent)
200
201             # Print parameter type
202             param_type = python_type(param)
203             text += xmlrpc_type(param_type) + "\n"
204
205             # Print parameter documentation right below type
206             if isinstance(param, Parameter):
207                 wrapper = textwrap.TextWrapper(width = 70,
208                                                initial_indent = " " * param_offset,
209                                                subsequent_indent = " " * param_offset)
210                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
211                 param = param.type
212
213             text += "\n"
214
215             # Indent struct fields and mixed types
216             if isinstance(param, dict):
217                 for name, subparam in param.iteritems():
218                     text += param_text(name, subparam, indent + step, step)
219             elif isinstance(param, Mixed):
220                 for subparam in param:
221                     text += param_text(name, subparam, indent + step, step)
222             elif isinstance(param, (list, tuple)):
223                 for subparam in param:
224                     text += param_text("", subparam, indent + step, step)
225
226             return text
227
228         text += "Parameters:\n\n"
229         for name, param in zip(max_args, self.accepts):
230             text += param_text(name, param, indent, indent)
231
232         text += "Returns:\n\n"
233         text += param_text("", self.returns, indent, indent)
234
235         return text
236
237     def args(self):
238         """
239         Returns a tuple:
240
241         ((arg1_name, arg2_name, ...),
242          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
243          (None, None, ..., optional1_default, optional2_default, ...))
244
245         That represents the minimum and maximum sets of arguments that
246         this function accepts and the defaults for the optional arguments.
247         """
248
249         # Inspect call. Remove self from the argument list.
250         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
251         defaults = self.call.func_defaults
252         if defaults is None:
253             defaults = ()
254
255         min_args = max_args[0:len(max_args) - len(defaults)]
256         defaults = tuple([None for arg in min_args]) + defaults
257         
258         return (min_args, max_args, defaults)
259
260     def type_check(self, name, value, expected, min = None, max = None):
261         """
262         Checks the type of the named value against the expected type,
263         which may be a Python type, a typed value, a Parameter, a
264         Mixed type, or a list or dictionary of possibly mixed types,
265         values, Parameters, or Mixed types.
266         
267         Extraneous members of lists must be of the same type as the
268         last specified type. For example, if the expected argument
269         type is [int, bool], then [1, False] and [14, True, False,
270         True] are valid, but [1], [False, 1] and [14, True, 1] are
271         not.
272
273         Extraneous members of dictionaries are ignored.
274         """
275
276         # If any of a number of types is acceptable
277         if isinstance(expected, Mixed):
278             for item in expected:
279                 try:
280                     self.type_check(name, value, item)
281                     expected = item
282                     break
283                 except PLCInvalidArgument, fault:
284                     pass
285             if expected != item:
286                 xmlrpc_types = [xmlrpc_type(item) for item in expected]
287                 raise PLCInvalidArgument("expected %s, got %s" % \
288                                          (" or ".join(xmlrpc_types),
289                                           xmlrpc_type(type(value))),
290                                          name)
291
292         # Get actual expected type from within the Parameter structure
293         elif isinstance(expected, Parameter):
294             min = expected.min
295             max = expected.max
296             expected = expected.type
297
298         expected_type = python_type(expected)
299
300         # Strings are a special case. Accept either unicode or str
301         # types if a string is expected.
302         if expected_type in StringTypes and isinstance(value, StringTypes):
303             pass
304
305         # Integers and long integers are also special types. Accept
306         # either int or long types if an int or long is expected.
307         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
308             pass
309
310         elif not isinstance(value, expected_type):
311             raise PLCInvalidArgument("expected %s, got %s" % \
312                                      (xmlrpc_type(expected_type),
313                                       xmlrpc_type(type(value))),
314                                      name)
315
316         # If a minimum or maximum (length, value) has been specified
317         if expected_type in StringTypes:
318             if min is not None and \
319                len(value.encode(self.api.encoding)) < min:
320                 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
321             if max is not None and \
322                len(value.encode(self.api.encoding)) > max:
323                 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
324         else:
325             if min is not None and value < min:
326                 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
327             if max is not None and value > max:
328                 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
329
330         # If a list with particular types of items is expected
331         if isinstance(expected, (list, tuple)):
332             for i in range(len(value)):
333                 if i >= len(expected):
334                     i = len(expected) - 1
335                 self.type_check(name + "[]", value[i], expected[i])
336
337         # If a struct with particular (or required) types of items is
338         # expected.
339         elif isinstance(expected, dict):
340             for key in value.keys():
341                 if key in expected:
342                     self.type_check(name + "['%s']" % key, value[key], expected[key])
343             for key, subparam in expected.iteritems():
344                 if isinstance(subparam, Parameter) and \
345                    not subparam.optional and key not in value.keys():
346                     raise PLCInvalidArgument("'%s' not specified" % key, name)
347
348 def python_type(arg):
349     """
350     Returns the Python type of the specified argument, which may be a
351     Python type, a typed value, or a Parameter.
352     """
353
354     if isinstance(arg, Parameter):
355         arg = arg.type
356
357     if isinstance(arg, type):
358         return arg
359     else:
360         return type(arg)
361
362 def xmlrpc_type(arg):
363     """
364     Returns the XML-RPC type of the specified argument, which may be a
365     Python type, a typed value, or a Parameter.
366     """
367
368     arg_type = python_type(arg)
369
370     if arg_type == NoneType:
371         return "nil"
372     elif arg_type == IntType or arg_type == LongType:
373         return "int"
374     elif arg_type == bool:
375         return "boolean"
376     elif arg_type == FloatType:
377         return "double"
378     elif arg_type in StringTypes:
379         return "string"
380     elif arg_type == ListType or arg_type == TupleType:
381         return "array"
382     elif arg_type == DictType:
383         return "struct"
384     elif arg_type == Mixed:
385         # Not really an XML-RPC type but return "mixed" for
386         # documentation purposes.
387         return "mixed"
388     else:
389         raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type