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