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
148 if not isinstance(arg, dict):
151 # what type of auth this is
152 if arg.has_key('AuthMethod'):
153 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
154 auth_method = arg['AuthMethod']
155 if auth_method in auth_methods:
156 event['auth_type'] = auth_method
157 for password in 'AuthString', 'session', 'password':
158 if arg.has_key(password):
160 arg[password] = "Removed by API"
163 # Log call representation
164 # XXX Truncate to avoid DoS
165 event['call'] = self.name + pprint.saferepr(newargs)
166 event['call_name'] = self.name
168 # Both users and nodes can call some methods
169 if isinstance(self.caller, Person):
170 event['person_id'] = self.caller['person_id']
171 elif isinstance(self.caller, Node):
172 event['node_id'] = self.caller['node_id']
174 event.sync(commit = False)
176 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
177 for key in self.event_objects.keys():
178 for object_id in self.event_objects[key]:
179 event.add_object(key, object_id, commit = False)
182 # Set the message for this event
184 event['message'] = fault.faultString
185 elif hasattr(self, 'message'):
186 event['message'] = self.message
191 def help(self, indent = " "):
193 Text documentation for the method.
196 (min_args, max_args, defaults) = self.args()
198 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
200 text += "Description:\n\n"
201 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
202 text += "\n".join(lines) + "\n\n"
204 text += "Allowed Roles:\n\n"
209 text += indent + ", ".join(roles) + "\n\n"
211 def param_text(name, param, indent, step):
213 Format a method parameter.
218 # Print parameter name
221 text += name.ljust(param_offset - len(indent))
223 param_offset = len(indent)
225 # Print parameter type
226 param_type = python_type(param)
227 text += xmlrpc_type(param_type) + "\n"
229 # Print parameter documentation right below type
230 if isinstance(param, Parameter):
231 wrapper = textwrap.TextWrapper(width = 70,
232 initial_indent = " " * param_offset,
233 subsequent_indent = " " * param_offset)
234 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
239 # Indent struct fields and mixed types
240 if isinstance(param, dict):
241 for name, subparam in param.iteritems():
242 text += param_text(name, subparam, indent + step, step)
243 elif isinstance(param, Mixed):
244 for subparam in param:
245 text += param_text(name, subparam, indent + step, step)
246 elif isinstance(param, (list, tuple, set)):
247 for subparam in param:
248 text += param_text("", subparam, indent + step, step)
252 text += "Parameters:\n\n"
253 for name, param in zip(max_args, self.accepts):
254 text += param_text(name, param, indent, indent)
256 text += "Returns:\n\n"
257 text += param_text("", self.returns, indent, indent)
265 ((arg1_name, arg2_name, ...),
266 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
267 (None, None, ..., optional1_default, optional2_default, ...))
269 That represents the minimum and maximum sets of arguments that
270 this function accepts and the defaults for the optional arguments.
273 # Inspect call. Remove self from the argument list.
274 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
275 defaults = self.call.func_defaults
279 min_args = max_args[0:len(max_args) - len(defaults)]
280 defaults = tuple([None for arg in min_args]) + defaults
282 return (min_args, max_args, defaults)
284 def type_check(self, name, value, expected, args):
286 Checks the type of the named value against the expected type,
287 which may be a Python type, a typed value, a Parameter, a
288 Mixed type, or a list or dictionary of possibly mixed types,
289 values, Parameters, or Mixed types.
291 Extraneous members of lists must be of the same type as the
292 last specified type. For example, if the expected argument
293 type is [int, bool], then [1, False] and [14, True, False,
294 True] are valid, but [1], [False, 1] and [14, True, 1] are
297 Extraneous members of dictionaries are ignored.
300 # If any of a number of types is acceptable
301 if isinstance(expected, Mixed):
302 for item in expected:
304 self.type_check(name, value, item, args)
306 except PLCInvalidArgument, fault:
310 # If an authentication structure is expected, save it and
311 # authenticate after basic type checking is done.
312 if isinstance(expected, Auth):
317 # Get actual expected type from within the Parameter structure
318 if isinstance(expected, Parameter):
321 nullok = expected.nullok
322 expected = expected.type
328 expected_type = python_type(expected)
330 # If value can be NULL
331 if value is None and nullok:
334 # Strings are a special case. Accept either unicode or str
335 # types if a string is expected.
336 if expected_type in StringTypes and isinstance(value, StringTypes):
339 # Integers and long integers are also special types. Accept
340 # either int or long types if an int or long is expected.
341 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
344 elif not isinstance(value, expected_type):
345 raise PLCInvalidArgument("expected %s, got %s" % \
346 (xmlrpc_type(expected_type),
347 xmlrpc_type(type(value))),
350 # If a minimum or maximum (length, value) has been specified
351 if expected_type in StringTypes:
352 if min is not None and \
353 len(value.encode(self.api.encoding)) < min:
354 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
355 if max is not None and \
356 len(value.encode(self.api.encoding)) > max:
357 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
358 elif expected_type in (list, tuple, set):
359 if min is not None and len(value) < min:
360 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
361 if max is not None and len(value) > max:
362 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
364 if min is not None and value < min:
365 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
366 if max is not None and value > max:
367 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
369 # If a list with particular types of items is expected
370 if isinstance(expected, (list, tuple, set)):
371 for i in range(len(value)):
372 if i >= len(expected):
373 j = len(expected) - 1
376 self.type_check(name + "[]", value[i], expected[j], args)
378 # If a struct with particular (or required) types of items is
380 elif isinstance(expected, dict):
381 for key in value.keys():
383 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
384 for key, subparam in expected.iteritems():
385 if isinstance(subparam, Parameter) and \
386 subparam.optional is not None and \
387 not subparam.optional and key not in value.keys():
388 raise PLCInvalidArgument("'%s' not specified" % key, name)
391 auth.check(self, *args)