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