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