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