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(0, runtime, *args)
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)
107 def log(self, fault_code, runtime, *args):
112 # Do not log system or Get calls
113 #if self.name.startswith('system') or self.name.startswith('Get'):
117 event = Event(self.api)
118 event['fault_code'] = fault_code
119 event['runtime'] = runtime
121 # Redact passwords and sessions
122 if args and isinstance(args[0], dict):
123 # what type of auth this is
124 if args[0].has_key('AuthMethod'):
125 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
126 auth_method = args[0]['AuthMethod']
127 if auth_method in auth_methods:
128 event['auth_method'] = auth_method
129 for password in 'AuthString', 'session':
130 if args[0].has_key(password):
131 auth = args[0].copy()
132 auth[password] = "Removed by API"
133 args = (auth,) + args[1:]
135 # Log call representation
136 # XXX Truncate to avoid DoS
137 event['call'] = self.name + pprint.saferepr(args)
138 event['call_name'] = self.name
140 # Both users and nodes can call some methods
141 if isinstance(self.caller, Person):
142 event['person_id'] = self.caller['person_id']
143 elif isinstance(self.caller, Node):
144 event['node_id'] = self.caller['node_id']
146 event.sync(commit = False)
148 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
149 for key in self.event_objects.keys():
150 for object_id in self.event_objects[key]:
151 event.add_object(key, object_id, commit = False)
154 # Set the message for this event
155 if hasattr(self, 'message'):
156 event['message'] = self.message
161 def help(self, indent = " "):
163 Text documentation for the method.
166 (min_args, max_args, defaults) = self.args()
168 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
170 text += "Description:\n\n"
171 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
172 text += "\n".join(lines) + "\n\n"
174 text += "Allowed Roles:\n\n"
179 text += indent + ", ".join(roles) + "\n\n"
181 def param_text(name, param, indent, step):
183 Format a method parameter.
188 # Print parameter name
191 text += name.ljust(param_offset - len(indent))
193 param_offset = len(indent)
195 # Print parameter type
196 param_type = python_type(param)
197 text += xmlrpc_type(param_type) + "\n"
199 # Print parameter documentation right below type
200 if isinstance(param, Parameter):
201 wrapper = textwrap.TextWrapper(width = 70,
202 initial_indent = " " * param_offset,
203 subsequent_indent = " " * param_offset)
204 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
209 # Indent struct fields and mixed types
210 if isinstance(param, dict):
211 for name, subparam in param.iteritems():
212 text += param_text(name, subparam, indent + step, step)
213 elif isinstance(param, Mixed):
214 for subparam in param:
215 text += param_text(name, subparam, indent + step, step)
216 elif isinstance(param, (list, tuple, set)):
217 for subparam in param:
218 text += param_text("", subparam, indent + step, step)
222 text += "Parameters:\n\n"
223 for name, param in zip(max_args, self.accepts):
224 text += param_text(name, param, indent, indent)
226 text += "Returns:\n\n"
227 text += param_text("", self.returns, indent, indent)
235 ((arg1_name, arg2_name, ...),
236 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
237 (None, None, ..., optional1_default, optional2_default, ...))
239 That represents the minimum and maximum sets of arguments that
240 this function accepts and the defaults for the optional arguments.
243 # Inspect call. Remove self from the argument list.
244 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
245 defaults = self.call.func_defaults
249 min_args = max_args[0:len(max_args) - len(defaults)]
250 defaults = tuple([None for arg in min_args]) + defaults
252 return (min_args, max_args, defaults)
254 def type_check(self, name, value, expected, args):
256 Checks the type of the named value against the expected type,
257 which may be a Python type, a typed value, a Parameter, a
258 Mixed type, or a list or dictionary of possibly mixed types,
259 values, Parameters, or Mixed types.
261 Extraneous members of lists must be of the same type as the
262 last specified type. For example, if the expected argument
263 type is [int, bool], then [1, False] and [14, True, False,
264 True] are valid, but [1], [False, 1] and [14, True, 1] are
267 Extraneous members of dictionaries are ignored.
270 # If any of a number of types is acceptable
271 if isinstance(expected, Mixed):
272 for item in expected:
274 self.type_check(name, value, item, args)
276 except PLCInvalidArgument, fault:
280 # If an authentication structure is expected, save it and
281 # authenticate after basic type checking is done.
282 if isinstance(expected, Auth):
287 # Get actual expected type from within the Parameter structure
288 if isinstance(expected, Parameter):
291 nullok = expected.nullok
292 expected = expected.type
298 expected_type = python_type(expected)
300 # If value can be NULL
301 if value is None and nullok:
304 # Strings are a special case. Accept either unicode or str
305 # types if a string is expected.
306 if expected_type in StringTypes and isinstance(value, StringTypes):
309 # Integers and long integers are also special types. Accept
310 # either int or long types if an int or long is expected.
311 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
314 elif not isinstance(value, expected_type):
315 raise PLCInvalidArgument("expected %s, got %s" % \
316 (xmlrpc_type(expected_type),
317 xmlrpc_type(type(value))),
320 # If a minimum or maximum (length, value) has been specified
321 if expected_type in StringTypes:
322 if min is not None and \
323 len(value.encode(self.api.encoding)) < min:
324 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
325 if max is not None and \
326 len(value.encode(self.api.encoding)) > max:
327 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
328 elif expected_type in (list, tuple, set):
329 if min is not None and len(value) < min:
330 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
331 if max is not None and len(value) > max:
332 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
334 if min is not None and value < min:
335 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
336 if max is not None and value > max:
337 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
339 # If a list with particular types of items is expected
340 if isinstance(expected, (list, tuple, set)):
341 for i in range(len(value)):
342 if i >= len(expected):
343 j = len(expected) - 1
346 self.type_check(name + "[]", value[i], expected[j], args)
348 # If a struct with particular (or required) types of items is
350 elif isinstance(expected, dict):
351 for key in value.keys():
353 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
354 for key, subparam in expected.iteritems():
355 if isinstance(subparam, Parameter) and \
356 subparam.optional is not None and \
357 not subparam.optional and key not in value.keys():
358 raise PLCInvalidArgument("'%s' not specified" % key, name)
361 auth.check(self, *args)