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
27 # we inherit object because we use new-style classes for legacy methods
28 class Method (object):
30 Base class for all PLCAPI functions. At a minimum, all PLCAPI
31 functions must define:
33 roles = [list of roles]
34 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
35 returns = Parameter(return_type, return_doc)
36 call(arg1, arg2, ...): method body
38 Argument types may be Python types (e.g., int, bool, etc.), typed
39 values (e.g., 1, True, etc.), a Parameter, or lists or
40 dictionaries of possibly mixed types, values, and/or Parameters
41 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
43 Once function decorators in Python 2.4 are fully supported,
44 consider wrapping calls with accepts() and returns() functions
45 instead of performing type checking manually.
48 # Defaults. Could implement authentication and type checking with
49 # decorators, but they are not supported in Python 2.3 and it
50 # would be hard to generate documentation without writing a code
58 def call(self, *args):
60 Method body for all PLCAPI functions. Must override.
65 def __init__(self, api):
66 self.name = self.__class__.__name__
69 # Auth may set this to a Person instance (if an anonymous
70 # method, will remain None).
73 # API may set this to a (addr, port) tuple if known
76 def __call__(self, *args, **kwds):
78 Main entry point for all PLCAPI functions. Type checks
79 arguments, authenticates, and executes call().
85 # legacy code cannot be type-checked, due to the way Method.args() works
86 if not hasattr(self,"skip_typecheck"):
87 (min_args, max_args, defaults) = self.args()
89 # Check that the right number of arguments were passed in
90 if len(args) < len(min_args) or len(args) > len(max_args):
91 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
93 for name, value, expected in zip(max_args, args, self.accepts):
94 self.type_check(name, value, expected, args)
96 result = self.call(*args, **kwds)
97 runtime = time.time() - start
99 if self.api.config.PLC_API_DEBUG or hasattr(self, 'message'):
100 self.log(None, runtime, *args)
104 except PLCFault, fault:
107 if isinstance(self.caller, Person):
108 caller = 'person_id %s' % self.caller['person_id']
109 elif isinstance(self.caller, Node):
110 caller = 'node_id %s' % self.caller['node_id']
112 # Prepend caller and method name to expected faults
113 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
114 runtime = time.time() - start
116 if self.api.config.PLC_API_DEBUG:
117 self.log(fault, runtime, *args)
121 def log(self, fault, runtime, *args):
126 # Do not log system or Get calls
127 #if self.name.startswith('system') or self.name.startswith('Get'):
131 event = Event(self.api)
132 event['fault_code'] = 0
134 event['fault_code'] = fault.faultCode
135 event['runtime'] = runtime
137 # Redact passwords and sessions
138 if args and isinstance(args[0], dict):
139 # what type of auth this is
140 if args[0].has_key('AuthMethod'):
141 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
142 auth_method = args[0]['AuthMethod']
143 if auth_method in auth_methods:
144 event['auth_type'] = auth_method
145 for password in 'AuthString', 'session':
146 if args[0].has_key(password):
147 auth = args[0].copy()
148 auth[password] = "Removed by API"
149 args = (auth,) + args[1:]
151 # Log call representation
152 # XXX Truncate to avoid DoS
153 event['call'] = self.name + pprint.saferepr(args)
154 event['call_name'] = self.name
156 # Both users and nodes can call some methods
157 if isinstance(self.caller, Person):
158 event['person_id'] = self.caller['person_id']
159 elif isinstance(self.caller, Node):
160 event['node_id'] = self.caller['node_id']
162 event.sync(commit = False)
164 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
165 for key in self.event_objects.keys():
166 for object_id in self.event_objects[key]:
167 event.add_object(key, object_id, commit = False)
170 # Set the message for this event
172 event['message'] = fault.faultString
173 elif hasattr(self, 'message'):
174 event['message'] = self.message
179 def help(self, indent = " "):
181 Text documentation for the method.
184 (min_args, max_args, defaults) = self.args()
186 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
188 text += "Description:\n\n"
189 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
190 text += "\n".join(lines) + "\n\n"
192 text += "Allowed Roles:\n\n"
197 text += indent + ", ".join(roles) + "\n\n"
199 def param_text(name, param, indent, step):
201 Format a method parameter.
206 # Print parameter name
209 text += name.ljust(param_offset - len(indent))
211 param_offset = len(indent)
213 # Print parameter type
214 param_type = python_type(param)
215 text += xmlrpc_type(param_type) + "\n"
217 # Print parameter documentation right below type
218 if isinstance(param, Parameter):
219 wrapper = textwrap.TextWrapper(width = 70,
220 initial_indent = " " * param_offset,
221 subsequent_indent = " " * param_offset)
222 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
227 # Indent struct fields and mixed types
228 if isinstance(param, dict):
229 for name, subparam in param.iteritems():
230 text += param_text(name, subparam, indent + step, step)
231 elif isinstance(param, Mixed):
232 for subparam in param:
233 text += param_text(name, subparam, indent + step, step)
234 elif isinstance(param, (list, tuple, set)):
235 for subparam in param:
236 text += param_text("", subparam, indent + step, step)
240 text += "Parameters:\n\n"
241 for name, param in zip(max_args, self.accepts):
242 text += param_text(name, param, indent, indent)
244 text += "Returns:\n\n"
245 text += param_text("", self.returns, indent, indent)
253 ((arg1_name, arg2_name, ...),
254 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
255 (None, None, ..., optional1_default, optional2_default, ...))
257 That represents the minimum and maximum sets of arguments that
258 this function accepts and the defaults for the optional arguments.
261 # Inspect call. Remove self from the argument list.
262 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
263 defaults = self.call.func_defaults
267 min_args = max_args[0:len(max_args) - len(defaults)]
268 defaults = tuple([None for arg in min_args]) + defaults
270 return (min_args, max_args, defaults)
272 def type_check(self, name, value, expected, args):
274 Checks the type of the named value against the expected type,
275 which may be a Python type, a typed value, a Parameter, a
276 Mixed type, or a list or dictionary of possibly mixed types,
277 values, Parameters, or Mixed types.
279 Extraneous members of lists must be of the same type as the
280 last specified type. For example, if the expected argument
281 type is [int, bool], then [1, False] and [14, True, False,
282 True] are valid, but [1], [False, 1] and [14, True, 1] are
285 Extraneous members of dictionaries are ignored.
288 # If any of a number of types is acceptable
289 if isinstance(expected, Mixed):
290 for item in expected:
292 self.type_check(name, value, item, args)
294 except PLCInvalidArgument, fault:
298 # If an authentication structure is expected, save it and
299 # authenticate after basic type checking is done.
300 if isinstance(expected, Auth):
305 # Get actual expected type from within the Parameter structure
306 if isinstance(expected, Parameter):
309 nullok = expected.nullok
310 expected = expected.type
316 expected_type = python_type(expected)
318 # If value can be NULL
319 if value is None and nullok:
322 # Strings are a special case. Accept either unicode or str
323 # types if a string is expected.
324 if expected_type in StringTypes and isinstance(value, StringTypes):
327 # Integers and long integers are also special types. Accept
328 # either int or long types if an int or long is expected.
329 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
332 elif not isinstance(value, expected_type):
333 raise PLCInvalidArgument("expected %s, got %s" % \
334 (xmlrpc_type(expected_type),
335 xmlrpc_type(type(value))),
338 # If a minimum or maximum (length, value) has been specified
339 if expected_type in StringTypes:
340 if min is not None and \
341 len(value.encode(self.api.encoding)) < min:
342 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
343 if max is not None and \
344 len(value.encode(self.api.encoding)) > max:
345 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
346 elif expected_type in (list, tuple, set):
347 if min is not None and len(value) < min:
348 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
349 if max is not None and len(value) > max:
350 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
352 if min is not None and value < min:
353 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
354 if max is not None and value > max:
355 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
357 # If a list with particular types of items is expected
358 if isinstance(expected, (list, tuple, set)):
359 for i in range(len(value)):
360 if i >= len(expected):
361 j = len(expected) - 1
364 self.type_check(name + "[]", value[i], expected[j], args)
366 # If a struct with particular (or required) types of items is
368 elif isinstance(expected, dict):
369 for key in value.keys():
371 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
372 for key, subparam in expected.iteritems():
373 if isinstance(subparam, Parameter) and \
374 subparam.optional is not None and \
375 not subparam.optional and key not in value.keys():
376 raise PLCInvalidArgument("'%s' not specified" % key, name)
379 auth.check(self, *args)