2 # Base class for all PLCAPI functions
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
14 from types import StringTypes
16 from PLC.Faults import *
17 from PLC.Parameter import Parameter, Mixed, python_type, xmlrpc_type
18 from PLC.Auth import Auth
19 from PLC.Debug import profile, log
20 from PLC.Events import Event, Events
21 from PLC.Nodes import Node, Nodes
22 from PLC.Persons import Person, Persons
24 # we inherit object because we use new-style classes for legacy methods
25 class Method (object):
27 Base class for all PLCAPI functions. At a minimum, all PLCAPI
28 functions must define:
30 roles = [list of roles]
31 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
32 returns = Parameter(return_type, return_doc)
33 call(arg1, arg2, ...): method body
35 Argument types may be Python types (e.g., int, bool, etc.), typed
36 values (e.g., 1, True, etc.), a Parameter, or lists or
37 dictionaries of possibly mixed types, values, and/or Parameters
38 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
40 Once function decorators in Python 2.4 are fully supported,
41 consider wrapping calls with accepts() and returns() functions
42 instead of performing type checking manually.
45 # Defaults. Could implement authentication and type checking with
46 # decorators, but they are not supported in Python 2.3 and it
47 # would be hard to generate documentation without writing a code
55 def call(self, *args):
57 Method body for all PLCAPI functions. Must override.
62 def __init__(self, api):
63 self.name = self.__class__.__name__
66 # Auth may set this to a Person instance (if an anonymous
67 # method, will remain None).
70 # API may set this to a (addr, port) tuple if known
73 def __call__(self, *args, **kwds):
75 Main entry point for all PLCAPI functions. Type checks
76 arguments, authenticates, and executes call().
82 # legacy code cannot be type-checked, due to the way Method.args() works
83 # as of 5.0-rc16 we don't use skip_type_check anymore
84 if not hasattr(self,"skip_type_check"):
85 (min_args, max_args, defaults) = self.args()
87 # Check that the right number of arguments were passed in
88 if len(args) < len(min_args) or len(args) > len(max_args):
89 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
91 for name, value, expected in zip(max_args, args, self.accepts):
92 self.type_check(name, value, expected, args)
94 result = self.call(*args, **kwds)
95 runtime = time.time() - start
97 if self.api.config.PLC_API_DEBUG or hasattr(self, 'message'):
98 self.log(None, runtime, *args)
102 except PLCFault, fault:
105 if isinstance(self.caller, Person):
106 caller = 'person_id %s' % self.caller['person_id']
107 elif isinstance(self.caller, Node):
108 caller = 'node_id %s' % self.caller['node_id']
110 # Prepend caller and method name to expected faults
111 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
112 runtime = time.time() - start
114 if self.api.config.PLC_API_DEBUG:
115 self.log(fault, runtime, *args)
119 def log(self, fault, runtime, *args):
124 # Do not log system or Get calls
125 #if self.name.startswith('system') or self.name.startswith('Get'):
127 # Do not log ReportRunlevel
128 if self.name.startswith('system'):
130 if self.name.startswith('ReportRunlevel'):
134 event = Event(self.api)
135 event['fault_code'] = 0
137 event['fault_code'] = fault.faultCode
138 event['runtime'] = runtime
140 # Redact passwords and sessions
145 if not isinstance(arg, dict):
148 # what type of auth this is
149 if arg.has_key('AuthMethod'):
150 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
151 auth_method = arg['AuthMethod']
152 if auth_method in auth_methods:
153 event['auth_type'] = auth_method
154 for password in 'AuthString', 'session', 'password':
155 if arg.has_key(password):
157 arg[password] = "Removed by API"
160 # Log call representation
161 # XXX Truncate to avoid DoS
162 event['call'] = self.name + pprint.saferepr(newargs)
163 event['call_name'] = self.name
165 # Both users and nodes can call some methods
166 if isinstance(self.caller, Person):
167 event['person_id'] = self.caller['person_id']
168 elif isinstance(self.caller, Node):
169 event['node_id'] = self.caller['node_id']
171 event.sync(commit = False)
173 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
174 for key in self.event_objects.keys():
175 for object_id in self.event_objects[key]:
176 event.add_object(key, object_id, commit = False)
179 # Set the message for this event
181 event['message'] = fault.faultString
182 elif hasattr(self, 'message'):
183 event['message'] = self.message
188 def help(self, indent = " "):
190 Text documentation for the method.
193 (min_args, max_args, defaults) = self.args()
195 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
197 text += "Description:\n\n"
198 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
199 text += "\n".join(lines) + "\n\n"
201 text += "Allowed Roles:\n\n"
206 text += indent + ", ".join(roles) + "\n\n"
208 def param_text(name, param, indent, step):
210 Format a method parameter.
215 # Print parameter name
218 text += name.ljust(param_offset - len(indent))
220 param_offset = len(indent)
222 # Print parameter type
223 param_type = python_type(param)
224 text += xmlrpc_type(param_type) + "\n"
226 # Print parameter documentation right below type
227 if isinstance(param, Parameter):
228 wrapper = textwrap.TextWrapper(width = 70,
229 initial_indent = " " * param_offset,
230 subsequent_indent = " " * param_offset)
231 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
236 # Indent struct fields and mixed types
237 if isinstance(param, dict):
238 for name, subparam in param.iteritems():
239 text += param_text(name, subparam, indent + step, step)
240 elif isinstance(param, Mixed):
241 for subparam in param:
242 text += param_text(name, subparam, indent + step, step)
243 elif isinstance(param, (list, tuple, set)):
244 for subparam in param:
245 text += param_text("", subparam, indent + step, step)
249 text += "Parameters:\n\n"
250 for name, param in zip(max_args, self.accepts):
251 text += param_text(name, param, indent, indent)
253 text += "Returns:\n\n"
254 text += param_text("", self.returns, indent, indent)
262 ((arg1_name, arg2_name, ...),
263 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
264 (None, None, ..., optional1_default, optional2_default, ...))
266 That represents the minimum and maximum sets of arguments that
267 this function accepts and the defaults for the optional arguments.
270 # Inspect call. Remove self from the argument list.
271 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
272 defaults = self.call.func_defaults
276 min_args = max_args[0:len(max_args) - len(defaults)]
277 defaults = tuple([None for arg in min_args]) + defaults
279 return (min_args, max_args, defaults)
281 def type_check(self, name, value, expected, args):
283 Checks the type of the named value against the expected type,
284 which may be a Python type, a typed value, a Parameter, a
285 Mixed type, or a list or dictionary of possibly mixed types,
286 values, Parameters, or Mixed types.
288 Extraneous members of lists must be of the same type as the
289 last specified type. For example, if the expected argument
290 type is [int, bool], then [1, False] and [14, True, False,
291 True] are valid, but [1], [False, 1] and [14, True, 1] are
294 Extraneous members of dictionaries are ignored.
297 # If any of a number of types is acceptable
298 if isinstance(expected, Mixed):
299 for item in expected:
301 self.type_check(name, value, item, args)
303 except PLCInvalidArgument, fault:
307 # If an authentication structure is expected, save it and
308 # authenticate after basic type checking is done.
309 if isinstance(expected, Auth):
314 # Get actual expected type from within the Parameter structure
315 if isinstance(expected, Parameter):
318 nullok = expected.nullok
319 expected = expected.type
325 expected_type = python_type(expected)
327 # If value can be NULL
328 if value is None and nullok:
331 # Strings are a special case. Accept either unicode or str
332 # types if a string is expected.
333 if expected_type in StringTypes and isinstance(value, StringTypes):
336 # Integers and long integers are also special types. Accept
337 # either int or long types if an int or long is expected.
338 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
341 elif not isinstance(value, expected_type):
342 raise PLCInvalidArgument("expected %s, got %s" % \
343 (xmlrpc_type(expected_type),
344 xmlrpc_type(type(value))),
347 # If a minimum or maximum (length, value) has been specified
348 if expected_type in StringTypes:
349 if min is not None and \
350 len(value.encode(self.api.encoding)) < min:
351 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
352 if max is not None and \
353 len(value.encode(self.api.encoding)) > max:
354 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
355 elif expected_type in (list, tuple, set):
356 if min is not None and len(value) < min:
357 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
358 if max is not None and len(value) > max:
359 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
361 if min is not None and value < min:
362 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
363 if max is not None and value > max:
364 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
366 # If a list with particular types of items is expected
367 if isinstance(expected, (list, tuple, set)):
368 for i in range(len(value)):
369 if i >= len(expected):
370 j = len(expected) - 1
373 self.type_check(name + "[]", value[i], expected[j], args)
375 # If a struct with particular (or required) types of items is
377 elif isinstance(expected, dict):
378 for key in value.keys():
380 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
381 for key, subparam in expected.iteritems():
382 if isinstance(subparam, Parameter) and \
383 subparam.optional is not None and \
384 not subparam.optional and key not in value.keys():
385 raise PLCInvalidArgument("'%s' not specified" % key, name)
388 auth.check(self, *args)