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