2 # Base class for all PLCAPI functions
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
17 from types import StringTypes
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
29 Base class for all PLCAPI functions. At a minimum, all PLCAPI
30 functions must define:
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
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}).
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.
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
57 def call(self, *args):
59 Method body for all PLCAPI functions. Must override.
64 def __init__(self, api):
65 self.name = self.__class__.__name__
68 # Auth may set this to a Person instance (if an anonymous
69 # method, will remain None).
72 # API may set this to a (addr, port) tuple if known
75 def __call__(self, *args, **kwds):
77 Main entry point for all PLCAPI functions. Type checks
78 arguments, authenticates, and executes call().
83 (min_args, max_args, defaults) = self.args()
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))
89 for name, value, expected in zip(max_args, args, self.accepts):
90 self.type_check(name, value, expected, args)
92 result = self.call(*args, **kwds)
93 runtime = time.time() - start
95 if self.api.config.PLC_API_DEBUG or hasattr(self, 'message'):
96 self.log(None, runtime, *args)
100 except PLCFault, fault:
103 if isinstance(self.caller, Person):
104 caller = 'person_id %s' % self.caller['person_id']
105 elif isinstance(self.caller, Node):
106 caller = 'node_id %s' % self.caller['node_id']
108 # Prepend caller and method name to expected faults
109 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
110 runtime = time.time() - start
112 if self.api.config.PLC_API_DEBUG:
113 self.log(fault, runtime, *args)
117 def log(self, fault, runtime, *args):
122 # Do not log system or Get calls
123 #if self.name.startswith('system') or self.name.startswith('Get'):
127 event = Event(self.api)
128 event['fault_code'] = 0
130 event['fault_code'] = fault.faultCode
131 event['runtime'] = runtime
133 # Redact passwords and sessions
134 if args and isinstance(args[0], dict):
135 # what type of auth this is
136 if args[0].has_key('AuthMethod'):
137 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
138 auth_method = args[0]['AuthMethod']
139 if auth_method in auth_methods:
140 event['auth_type'] = auth_method
141 for password in 'AuthString', 'session':
142 if args[0].has_key(password):
143 auth = args[0].copy()
144 auth[password] = "Removed by API"
145 args = (auth,) + args[1:]
147 # Log call representation
148 # XXX Truncate to avoid DoS
149 event['call'] = self.name + pprint.saferepr(args)
150 event['call_name'] = self.name
152 # Both users and nodes can call some methods
153 if isinstance(self.caller, Person):
154 event['person_id'] = self.caller['person_id']
155 elif isinstance(self.caller, Node):
156 event['node_id'] = self.caller['node_id']
158 event.sync(commit = False)
160 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
161 for key in self.event_objects.keys():
162 for object_id in self.event_objects[key]:
163 event.add_object(key, object_id, commit = False)
166 # Set the message for this event
168 event['message'] = fault.faultString
169 elif hasattr(self, 'message'):
170 event['message'] = self.message
175 def help(self, indent = " "):
177 Text documentation for the method.
180 (min_args, max_args, defaults) = self.args()
182 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
184 text += "Description:\n\n"
185 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
186 text += "\n".join(lines) + "\n\n"
188 text += "Allowed Roles:\n\n"
193 text += indent + ", ".join(roles) + "\n\n"
195 def param_text(name, param, indent, step):
197 Format a method parameter.
202 # Print parameter name
205 text += name.ljust(param_offset - len(indent))
207 param_offset = len(indent)
209 # Print parameter type
210 param_type = python_type(param)
211 text += xmlrpc_type(param_type) + "\n"
213 # Print parameter documentation right below type
214 if isinstance(param, Parameter):
215 wrapper = textwrap.TextWrapper(width = 70,
216 initial_indent = " " * param_offset,
217 subsequent_indent = " " * param_offset)
218 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
223 # Indent struct fields and mixed types
224 if isinstance(param, dict):
225 for name, subparam in param.iteritems():
226 text += param_text(name, subparam, indent + step, step)
227 elif isinstance(param, Mixed):
228 for subparam in param:
229 text += param_text(name, subparam, indent + step, step)
230 elif isinstance(param, (list, tuple, set)):
231 for subparam in param:
232 text += param_text("", subparam, indent + step, step)
236 text += "Parameters:\n\n"
237 for name, param in zip(max_args, self.accepts):
238 text += param_text(name, param, indent, indent)
240 text += "Returns:\n\n"
241 text += param_text("", self.returns, indent, indent)
249 ((arg1_name, arg2_name, ...),
250 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
251 (None, None, ..., optional1_default, optional2_default, ...))
253 That represents the minimum and maximum sets of arguments that
254 this function accepts and the defaults for the optional arguments.
257 # Inspect call. Remove self from the argument list.
258 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
259 defaults = self.call.func_defaults
263 min_args = max_args[0:len(max_args) - len(defaults)]
264 defaults = tuple([None for arg in min_args]) + defaults
266 return (min_args, max_args, defaults)
268 def type_check(self, name, value, expected, args):
270 Checks the type of the named value against the expected type,
271 which may be a Python type, a typed value, a Parameter, a
272 Mixed type, or a list or dictionary of possibly mixed types,
273 values, Parameters, or Mixed types.
275 Extraneous members of lists must be of the same type as the
276 last specified type. For example, if the expected argument
277 type is [int, bool], then [1, False] and [14, True, False,
278 True] are valid, but [1], [False, 1] and [14, True, 1] are
281 Extraneous members of dictionaries are ignored.
284 # If any of a number of types is acceptable
285 if isinstance(expected, Mixed):
286 for item in expected:
288 self.type_check(name, value, item, args)
290 except PLCInvalidArgument, fault:
294 # If an authentication structure is expected, save it and
295 # authenticate after basic type checking is done.
296 if isinstance(expected, Auth):
301 # Get actual expected type from within the Parameter structure
302 if isinstance(expected, Parameter):
305 nullok = expected.nullok
306 expected = expected.type
312 expected_type = python_type(expected)
314 # If value can be NULL
315 if value is None and nullok:
318 # Strings are a special case. Accept either unicode or str
319 # types if a string is expected.
320 if expected_type in StringTypes and isinstance(value, StringTypes):
323 # Integers and long integers are also special types. Accept
324 # either int or long types if an int or long is expected.
325 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
328 elif not isinstance(value, expected_type):
329 raise PLCInvalidArgument("expected %s, got %s" % \
330 (xmlrpc_type(expected_type),
331 xmlrpc_type(type(value))),
334 # If a minimum or maximum (length, value) has been specified
335 if expected_type in StringTypes:
336 if min is not None and \
337 len(value.encode(self.api.encoding)) < min:
338 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
339 if max is not None and \
340 len(value.encode(self.api.encoding)) > max:
341 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
342 elif expected_type in (list, tuple, set):
343 if min is not None and len(value) < min:
344 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
345 if max is not None and len(value) > max:
346 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
348 if min is not None and value < min:
349 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
350 if max is not None and value > max:
351 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
353 # If a list with particular types of items is expected
354 if isinstance(expected, (list, tuple, set)):
355 for i in range(len(value)):
356 if i >= len(expected):
357 j = len(expected) - 1
360 self.type_check(name + "[]", value[i], expected[j], args)
362 # If a struct with particular (or required) types of items is
364 elif isinstance(expected, dict):
365 for key in value.keys():
367 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
368 for key, subparam in expected.iteritems():
369 if isinstance(subparam, Parameter) and \
370 subparam.optional is not None and \
371 not subparam.optional and key not in value.keys():
372 raise PLCInvalidArgument("'%s' not specified" % key, name)
375 auth.check(self, *args)