2 # Base class for all PLCAPI functions
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
18 from types import StringTypes
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
28 # we inherit object because we use new-style classes for legacy methods
29 class Method (object):
31 Base class for all PLCAPI functions. At a minimum, all PLCAPI
32 functions must define:
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
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}).
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.
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
59 def call(self, *args):
61 Method body for all PLCAPI functions. Must override.
66 def __init__(self, api):
67 self.name = self.__class__.__name__
70 # Auth may set this to a Person instance (if an anonymous
71 # method, will remain None).
74 # API may set this to a (addr, port) tuple if known
77 def __call__(self, *args, **kwds):
79 Main entry point for all PLCAPI functions. Type checks
80 arguments, authenticates, and executes call().
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()
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))
94 for name, value, expected in zip(max_args, args, self.accepts):
95 self.type_check(name, value, expected, args)
97 result = self.call(*args, **kwds)
98 runtime = time.time() - start
100 if self.api.config.PLC_API_DEBUG or hasattr(self, 'message'):
101 self.log(None, runtime, *args)
105 except PLCFault, fault:
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']
113 # Prepend caller and method name to expected faults
114 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
115 runtime = time.time() - start
117 if self.api.config.PLC_API_DEBUG:
118 self.log(fault, runtime, *args)
122 def log(self, fault, runtime, *args):
127 # Do not log system or Get calls
128 #if self.name.startswith('system') or self.name.startswith('Get'):
130 # Do not log ReportRunlevel
131 if self.name.startswith('system'):
133 if self.name.startswith('ReportRunlevel'):
137 event = Event(self.api)
138 event['fault_code'] = 0
140 event['fault_code'] = fault.faultCode
141 event['runtime'] = runtime
143 # Redact passwords and sessions
144 if args and isinstance(args[0], dict):
145 # what type of auth this is
146 if args[0].has_key('AuthMethod'):
147 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
148 auth_method = args[0]['AuthMethod']
149 if auth_method in auth_methods:
150 event['auth_type'] = auth_method
151 for password in 'AuthString', 'session':
152 if args[0].has_key(password):
153 auth = args[0].copy()
154 auth[password] = "Removed by API"
155 args = (auth,) + args[1:]
157 # Log call representation
158 # XXX Truncate to avoid DoS
159 event['call'] = self.name + pprint.saferepr(args)
160 event['call_name'] = self.name
162 # Both users and nodes can call some methods
163 if isinstance(self.caller, Person):
164 event['person_id'] = self.caller['person_id']
165 elif isinstance(self.caller, Node):
166 event['node_id'] = self.caller['node_id']
168 event.sync(commit = False)
170 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
171 for key in self.event_objects.keys():
172 for object_id in self.event_objects[key]:
173 event.add_object(key, object_id, commit = False)
176 # Set the message for this event
178 event['message'] = fault.faultString
179 elif hasattr(self, 'message'):
180 event['message'] = self.message
185 def help(self, indent = " "):
187 Text documentation for the method.
190 (min_args, max_args, defaults) = self.args()
192 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
194 text += "Description:\n\n"
195 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
196 text += "\n".join(lines) + "\n\n"
198 text += "Allowed Roles:\n\n"
203 text += indent + ", ".join(roles) + "\n\n"
205 def param_text(name, param, indent, step):
207 Format a method parameter.
212 # Print parameter name
215 text += name.ljust(param_offset - len(indent))
217 param_offset = len(indent)
219 # Print parameter type
220 param_type = python_type(param)
221 text += xmlrpc_type(param_type) + "\n"
223 # Print parameter documentation right below type
224 if isinstance(param, Parameter):
225 wrapper = textwrap.TextWrapper(width = 70,
226 initial_indent = " " * param_offset,
227 subsequent_indent = " " * param_offset)
228 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
233 # Indent struct fields and mixed types
234 if isinstance(param, dict):
235 for name, subparam in param.iteritems():
236 text += param_text(name, subparam, indent + step, step)
237 elif isinstance(param, Mixed):
238 for subparam in param:
239 text += param_text(name, subparam, indent + step, step)
240 elif isinstance(param, (list, tuple, set)):
241 for subparam in param:
242 text += param_text("", subparam, indent + step, step)
246 text += "Parameters:\n\n"
247 for name, param in zip(max_args, self.accepts):
248 text += param_text(name, param, indent, indent)
250 text += "Returns:\n\n"
251 text += param_text("", self.returns, indent, indent)
259 ((arg1_name, arg2_name, ...),
260 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
261 (None, None, ..., optional1_default, optional2_default, ...))
263 That represents the minimum and maximum sets of arguments that
264 this function accepts and the defaults for the optional arguments.
267 # Inspect call. Remove self from the argument list.
268 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
269 defaults = self.call.func_defaults
273 min_args = max_args[0:len(max_args) - len(defaults)]
274 defaults = tuple([None for arg in min_args]) + defaults
276 return (min_args, max_args, defaults)
278 def type_check(self, name, value, expected, args):
280 Checks the type of the named value against the expected type,
281 which may be a Python type, a typed value, a Parameter, a
282 Mixed type, or a list or dictionary of possibly mixed types,
283 values, Parameters, or Mixed types.
285 Extraneous members of lists must be of the same type as the
286 last specified type. For example, if the expected argument
287 type is [int, bool], then [1, False] and [14, True, False,
288 True] are valid, but [1], [False, 1] and [14, True, 1] are
291 Extraneous members of dictionaries are ignored.
294 # If any of a number of types is acceptable
295 if isinstance(expected, Mixed):
296 for item in expected:
298 self.type_check(name, value, item, args)
300 except PLCInvalidArgument, fault:
304 # If an authentication structure is expected, save it and
305 # authenticate after basic type checking is done.
306 if isinstance(expected, Auth):
311 # Get actual expected type from within the Parameter structure
312 if isinstance(expected, Parameter):
315 nullok = expected.nullok
316 expected = expected.type
322 expected_type = python_type(expected)
324 # If value can be NULL
325 if value is None and nullok:
328 # Strings are a special case. Accept either unicode or str
329 # types if a string is expected.
330 if expected_type in StringTypes and isinstance(value, StringTypes):
333 # Integers and long integers are also special types. Accept
334 # either int or long types if an int or long is expected.
335 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
338 elif not isinstance(value, expected_type):
339 raise PLCInvalidArgument("expected %s, got %s" % \
340 (xmlrpc_type(expected_type),
341 xmlrpc_type(type(value))),
344 # If a minimum or maximum (length, value) has been specified
345 if expected_type in StringTypes:
346 if min is not None and \
347 len(value.encode(self.api.encoding)) < min:
348 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
349 if max is not None and \
350 len(value.encode(self.api.encoding)) > max:
351 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
352 elif expected_type in (list, tuple, set):
353 if min is not None and len(value) < min:
354 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
355 if max is not None and len(value) > max:
356 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
358 if min is not None and value < min:
359 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
360 if max is not None and value > max:
361 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
363 # If a list with particular types of items is expected
364 if isinstance(expected, (list, tuple, set)):
365 for i in range(len(value)):
366 if i >= len(expected):
367 j = len(expected) - 1
370 self.type_check(name + "[]", value[i], expected[j], args)
372 # If a struct with particular (or required) types of items is
374 elif isinstance(expected, dict):
375 for key in value.keys():
377 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
378 for key, subparam in expected.iteritems():
379 if isinstance(subparam, Parameter) and \
380 subparam.optional is not None and \
381 not subparam.optional and key not in value.keys():
382 raise PLCInvalidArgument("'%s' not specified" % key, name)
385 auth.check(self, *args)