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