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