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