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
111 self.log(fault, runtime, *args)
114 def log(self, fault, runtime, *args):
119 # Do not log system or Get calls
120 #if self.name.startswith('system') or self.name.startswith('Get'):
124 event = Event(self.api)
125 event['fault_code'] = 0
127 event['fault_code'] = fault.faultCode
128 event['runtime'] = runtime
130 # Redact passwords and sessions
131 if args and isinstance(args[0], dict):
132 # what type of auth this is
133 if args[0].has_key('AuthMethod'):
134 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
135 auth_method = args[0]['AuthMethod']
136 if auth_method in auth_methods:
137 event['auth_type'] = auth_method
138 for password in 'AuthString', 'session':
139 if args[0].has_key(password):
140 auth = args[0].copy()
141 auth[password] = "Removed by API"
142 args = (auth,) + args[1:]
144 # Log call representation
145 # XXX Truncate to avoid DoS
146 event['call'] = self.name + pprint.saferepr(args)
147 event['call_name'] = self.name
149 # Both users and nodes can call some methods
150 if isinstance(self.caller, Person):
151 event['person_id'] = self.caller['person_id']
152 elif isinstance(self.caller, Node):
153 event['node_id'] = self.caller['node_id']
155 event.sync(commit = False)
157 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
158 for key in self.event_objects.keys():
159 for object_id in self.event_objects[key]:
160 event.add_object(key, object_id, commit = False)
163 # Set the message for this event
165 event['message'] = fault.faultString
166 elif hasattr(self, 'message'):
167 event['message'] = self.message
172 def help(self, indent = " "):
174 Text documentation for the method.
177 (min_args, max_args, defaults) = self.args()
179 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
181 text += "Description:\n\n"
182 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
183 text += "\n".join(lines) + "\n\n"
185 text += "Allowed Roles:\n\n"
190 text += indent + ", ".join(roles) + "\n\n"
192 def param_text(name, param, indent, step):
194 Format a method parameter.
199 # Print parameter name
202 text += name.ljust(param_offset - len(indent))
204 param_offset = len(indent)
206 # Print parameter type
207 param_type = python_type(param)
208 text += xmlrpc_type(param_type) + "\n"
210 # Print parameter documentation right below type
211 if isinstance(param, Parameter):
212 wrapper = textwrap.TextWrapper(width = 70,
213 initial_indent = " " * param_offset,
214 subsequent_indent = " " * param_offset)
215 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
220 # Indent struct fields and mixed types
221 if isinstance(param, dict):
222 for name, subparam in param.iteritems():
223 text += param_text(name, subparam, indent + step, step)
224 elif isinstance(param, Mixed):
225 for subparam in param:
226 text += param_text(name, subparam, indent + step, step)
227 elif isinstance(param, (list, tuple, set)):
228 for subparam in param:
229 text += param_text("", subparam, indent + step, step)
233 text += "Parameters:\n\n"
234 for name, param in zip(max_args, self.accepts):
235 text += param_text(name, param, indent, indent)
237 text += "Returns:\n\n"
238 text += param_text("", self.returns, indent, indent)
246 ((arg1_name, arg2_name, ...),
247 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
248 (None, None, ..., optional1_default, optional2_default, ...))
250 That represents the minimum and maximum sets of arguments that
251 this function accepts and the defaults for the optional arguments.
254 # Inspect call. Remove self from the argument list.
255 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
256 defaults = self.call.func_defaults
260 min_args = max_args[0:len(max_args) - len(defaults)]
261 defaults = tuple([None for arg in min_args]) + defaults
263 return (min_args, max_args, defaults)
265 def type_check(self, name, value, expected, args):
267 Checks the type of the named value against the expected type,
268 which may be a Python type, a typed value, a Parameter, a
269 Mixed type, or a list or dictionary of possibly mixed types,
270 values, Parameters, or Mixed types.
272 Extraneous members of lists must be of the same type as the
273 last specified type. For example, if the expected argument
274 type is [int, bool], then [1, False] and [14, True, False,
275 True] are valid, but [1], [False, 1] and [14, True, 1] are
278 Extraneous members of dictionaries are ignored.
281 # If any of a number of types is acceptable
282 if isinstance(expected, Mixed):
283 for item in expected:
285 self.type_check(name, value, item, args)
287 except PLCInvalidArgument, fault:
291 # If an authentication structure is expected, save it and
292 # authenticate after basic type checking is done.
293 if isinstance(expected, Auth):
298 # Get actual expected type from within the Parameter structure
299 if isinstance(expected, Parameter):
302 nullok = expected.nullok
303 expected = expected.type
309 expected_type = python_type(expected)
311 # If value can be NULL
312 if value is None and nullok:
315 # Strings are a special case. Accept either unicode or str
316 # types if a string is expected.
317 if expected_type in StringTypes and isinstance(value, StringTypes):
320 # Integers and long integers are also special types. Accept
321 # either int or long types if an int or long is expected.
322 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
325 elif not isinstance(value, expected_type):
326 raise PLCInvalidArgument("expected %s, got %s" % \
327 (xmlrpc_type(expected_type),
328 xmlrpc_type(type(value))),
331 # If a minimum or maximum (length, value) has been specified
332 if expected_type in StringTypes:
333 if min is not None and \
334 len(value.encode(self.api.encoding)) < min:
335 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
336 if max is not None and \
337 len(value.encode(self.api.encoding)) > max:
338 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
339 elif expected_type in (list, tuple, set):
340 if min is not None and len(value) < min:
341 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
342 if max is not None and len(value) > max:
343 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
345 if min is not None and value < min:
346 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
347 if max is not None and value > max:
348 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
350 # If a list with particular types of items is expected
351 if isinstance(expected, (list, tuple, set)):
352 for i in range(len(value)):
353 if i >= len(expected):
354 j = len(expected) - 1
357 self.type_check(name + "[]", value[i], expected[j], args)
359 # If a struct with particular (or required) types of items is
361 elif isinstance(expected, dict):
362 for key in value.keys():
364 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
365 for key, subparam in expected.iteritems():
366 if isinstance(subparam, Parameter) and \
367 subparam.optional is not None and \
368 not subparam.optional and key not in value.keys():
369 raise PLCInvalidArgument("'%s' not specified" % key, name)
372 auth.check(self, *args)