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('ReportRunlevel'):
135 event = Event(self.api)
136 event['fault_code'] = 0
138 event['fault_code'] = fault.faultCode
139 event['runtime'] = runtime
141 # Redact passwords and sessions
142 if args and isinstance(args[0], dict):
143 # what type of auth this is
144 if args[0].has_key('AuthMethod'):
145 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
146 auth_method = args[0]['AuthMethod']
147 if auth_method in auth_methods:
148 event['auth_type'] = auth_method
149 for password in 'AuthString', 'session':
150 if args[0].has_key(password):
151 auth = args[0].copy()
152 auth[password] = "Removed by API"
153 args = (auth,) + args[1:]
155 # Log call representation
156 # XXX Truncate to avoid DoS
157 event['call'] = self.name + pprint.saferepr(args)
158 event['call_name'] = self.name
160 # Both users and nodes can call some methods
161 if isinstance(self.caller, Person):
162 event['person_id'] = self.caller['person_id']
163 elif isinstance(self.caller, Node):
164 event['node_id'] = self.caller['node_id']
166 event.sync(commit = False)
168 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
169 for key in self.event_objects.keys():
170 for object_id in self.event_objects[key]:
171 event.add_object(key, object_id, commit = False)
174 # Set the message for this event
176 event['message'] = fault.faultString
177 elif hasattr(self, 'message'):
178 event['message'] = self.message
183 def help(self, indent = " "):
185 Text documentation for the method.
188 (min_args, max_args, defaults) = self.args()
190 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
192 text += "Description:\n\n"
193 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
194 text += "\n".join(lines) + "\n\n"
196 text += "Allowed Roles:\n\n"
201 text += indent + ", ".join(roles) + "\n\n"
203 def param_text(name, param, indent, step):
205 Format a method parameter.
210 # Print parameter name
213 text += name.ljust(param_offset - len(indent))
215 param_offset = len(indent)
217 # Print parameter type
218 param_type = python_type(param)
219 text += xmlrpc_type(param_type) + "\n"
221 # Print parameter documentation right below type
222 if isinstance(param, Parameter):
223 wrapper = textwrap.TextWrapper(width = 70,
224 initial_indent = " " * param_offset,
225 subsequent_indent = " " * param_offset)
226 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
231 # Indent struct fields and mixed types
232 if isinstance(param, dict):
233 for name, subparam in param.iteritems():
234 text += param_text(name, subparam, indent + step, step)
235 elif isinstance(param, Mixed):
236 for subparam in param:
237 text += param_text(name, subparam, indent + step, step)
238 elif isinstance(param, (list, tuple, set)):
239 for subparam in param:
240 text += param_text("", subparam, indent + step, step)
244 text += "Parameters:\n\n"
245 for name, param in zip(max_args, self.accepts):
246 text += param_text(name, param, indent, indent)
248 text += "Returns:\n\n"
249 text += param_text("", self.returns, indent, indent)
257 ((arg1_name, arg2_name, ...),
258 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
259 (None, None, ..., optional1_default, optional2_default, ...))
261 That represents the minimum and maximum sets of arguments that
262 this function accepts and the defaults for the optional arguments.
265 # Inspect call. Remove self from the argument list.
266 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
267 defaults = self.call.func_defaults
271 min_args = max_args[0:len(max_args) - len(defaults)]
272 defaults = tuple([None for arg in min_args]) + defaults
274 return (min_args, max_args, defaults)
276 def type_check(self, name, value, expected, args):
278 Checks the type of the named value against the expected type,
279 which may be a Python type, a typed value, a Parameter, a
280 Mixed type, or a list or dictionary of possibly mixed types,
281 values, Parameters, or Mixed types.
283 Extraneous members of lists must be of the same type as the
284 last specified type. For example, if the expected argument
285 type is [int, bool], then [1, False] and [14, True, False,
286 True] are valid, but [1], [False, 1] and [14, True, 1] are
289 Extraneous members of dictionaries are ignored.
292 # If any of a number of types is acceptable
293 if isinstance(expected, Mixed):
294 for item in expected:
296 self.type_check(name, value, item, args)
298 except PLCInvalidArgument, fault:
302 # If an authentication structure is expected, save it and
303 # authenticate after basic type checking is done.
304 if isinstance(expected, Auth):
309 # Get actual expected type from within the Parameter structure
310 if isinstance(expected, Parameter):
313 nullok = expected.nullok
314 expected = expected.type
320 expected_type = python_type(expected)
322 # If value can be NULL
323 if value is None and nullok:
326 # Strings are a special case. Accept either unicode or str
327 # types if a string is expected.
328 if expected_type in StringTypes and isinstance(value, StringTypes):
331 # Integers and long integers are also special types. Accept
332 # either int or long types if an int or long is expected.
333 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
336 elif not isinstance(value, expected_type):
337 raise PLCInvalidArgument("expected %s, got %s" % \
338 (xmlrpc_type(expected_type),
339 xmlrpc_type(type(value))),
342 # If a minimum or maximum (length, value) has been specified
343 if expected_type in StringTypes:
344 if min is not None and \
345 len(value.encode(self.api.encoding)) < min:
346 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
347 if max is not None and \
348 len(value.encode(self.api.encoding)) > max:
349 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
350 elif expected_type in (list, tuple, set):
351 if min is not None and len(value) < min:
352 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
353 if max is not None and len(value) > max:
354 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
356 if min is not None and value < min:
357 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
358 if max is not None and value > max:
359 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
361 # If a list with particular types of items is expected
362 if isinstance(expected, (list, tuple, set)):
363 for i in range(len(value)):
364 if i >= len(expected):
365 j = len(expected) - 1
368 self.type_check(name + "[]", value[i], expected[j], args)
370 # If a struct with particular (or required) types of items is
372 elif isinstance(expected, dict):
373 for key in value.keys():
375 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
376 for key, subparam in expected.iteritems():
377 if isinstance(subparam, Parameter) and \
378 subparam.optional is not None and \
379 not subparam.optional and key not in value.keys():
380 raise PLCInvalidArgument("'%s' not specified" % key, name)
383 auth.check(self, *args)