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