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