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') or self.name.startswith('ReportRunlevel') :
132 event = Event(self.api)
133 event['fault_code'] = 0
135 event['fault_code'] = fault.faultCode
136 event['runtime'] = runtime
138 # Redact passwords and sessions
139 if args and isinstance(args[0], dict):
140 # what type of auth this is
141 if args[0].has_key('AuthMethod'):
142 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
143 auth_method = args[0]['AuthMethod']
144 if auth_method in auth_methods:
145 event['auth_type'] = auth_method
146 for password in 'AuthString', 'session':
147 if args[0].has_key(password):
148 auth = args[0].copy()
149 auth[password] = "Removed by API"
150 args = (auth,) + args[1:]
152 # Log call representation
153 # XXX Truncate to avoid DoS
154 event['call'] = self.name + pprint.saferepr(args)
155 event['call_name'] = self.name
157 # Both users and nodes can call some methods
158 if isinstance(self.caller, Person):
159 event['person_id'] = self.caller['person_id']
160 elif isinstance(self.caller, Node):
161 event['node_id'] = self.caller['node_id']
163 event.sync(commit = False)
165 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
166 for key in self.event_objects.keys():
167 for object_id in self.event_objects[key]:
168 event.add_object(key, object_id, commit = False)
171 # Set the message for this event
173 event['message'] = fault.faultString
174 elif hasattr(self, 'message'):
175 event['message'] = self.message
180 def help(self, indent = " "):
182 Text documentation for the method.
185 (min_args, max_args, defaults) = self.args()
187 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
189 text += "Description:\n\n"
190 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
191 text += "\n".join(lines) + "\n\n"
193 text += "Allowed Roles:\n\n"
198 text += indent + ", ".join(roles) + "\n\n"
200 def param_text(name, param, indent, step):
202 Format a method parameter.
207 # Print parameter name
210 text += name.ljust(param_offset - len(indent))
212 param_offset = len(indent)
214 # Print parameter type
215 param_type = python_type(param)
216 text += xmlrpc_type(param_type) + "\n"
218 # Print parameter documentation right below type
219 if isinstance(param, Parameter):
220 wrapper = textwrap.TextWrapper(width = 70,
221 initial_indent = " " * param_offset,
222 subsequent_indent = " " * param_offset)
223 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
228 # Indent struct fields and mixed types
229 if isinstance(param, dict):
230 for name, subparam in param.iteritems():
231 text += param_text(name, subparam, indent + step, step)
232 elif isinstance(param, Mixed):
233 for subparam in param:
234 text += param_text(name, subparam, indent + step, step)
235 elif isinstance(param, (list, tuple, set)):
236 for subparam in param:
237 text += param_text("", subparam, indent + step, step)
241 text += "Parameters:\n\n"
242 for name, param in zip(max_args, self.accepts):
243 text += param_text(name, param, indent, indent)
245 text += "Returns:\n\n"
246 text += param_text("", self.returns, indent, indent)
254 ((arg1_name, arg2_name, ...),
255 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
256 (None, None, ..., optional1_default, optional2_default, ...))
258 That represents the minimum and maximum sets of arguments that
259 this function accepts and the defaults for the optional arguments.
262 # Inspect call. Remove self from the argument list.
263 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
264 defaults = self.call.func_defaults
268 min_args = max_args[0:len(max_args) - len(defaults)]
269 defaults = tuple([None for arg in min_args]) + defaults
271 return (min_args, max_args, defaults)
273 def type_check(self, name, value, expected, args):
275 Checks the type of the named value against the expected type,
276 which may be a Python type, a typed value, a Parameter, a
277 Mixed type, or a list or dictionary of possibly mixed types,
278 values, Parameters, or Mixed types.
280 Extraneous members of lists must be of the same type as the
281 last specified type. For example, if the expected argument
282 type is [int, bool], then [1, False] and [14, True, False,
283 True] are valid, but [1], [False, 1] and [14, True, 1] are
286 Extraneous members of dictionaries are ignored.
289 # If any of a number of types is acceptable
290 if isinstance(expected, Mixed):
291 for item in expected:
293 self.type_check(name, value, item, args)
295 except PLCInvalidArgument, fault:
299 # If an authentication structure is expected, save it and
300 # authenticate after basic type checking is done.
301 if isinstance(expected, Auth):
306 # Get actual expected type from within the Parameter structure
307 if isinstance(expected, Parameter):
310 nullok = expected.nullok
311 expected = expected.type
317 expected_type = python_type(expected)
319 # If value can be NULL
320 if value is None and nullok:
323 # Strings are a special case. Accept either unicode or str
324 # types if a string is expected.
325 if expected_type in StringTypes and isinstance(value, StringTypes):
328 # Integers and long integers are also special types. Accept
329 # either int or long types if an int or long is expected.
330 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
333 elif not isinstance(value, expected_type):
334 raise PLCInvalidArgument("expected %s, got %s" % \
335 (xmlrpc_type(expected_type),
336 xmlrpc_type(type(value))),
339 # If a minimum or maximum (length, value) has been specified
340 if expected_type in StringTypes:
341 if min is not None and \
342 len(value.encode(self.api.encoding)) < min:
343 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
344 if max is not None and \
345 len(value.encode(self.api.encoding)) > max:
346 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
347 elif expected_type in (list, tuple, set):
348 if min is not None and len(value) < min:
349 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
350 if max is not None and len(value) > max:
351 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
353 if min is not None and value < min:
354 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
355 if max is not None and value > max:
356 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
358 # If a list with particular types of items is expected
359 if isinstance(expected, (list, tuple, set)):
360 for i in range(len(value)):
361 if i >= len(expected):
362 j = len(expected) - 1
365 self.type_check(name + "[]", value[i], expected[j], args)
367 # If a struct with particular (or required) types of items is
369 elif isinstance(expected, dict):
370 for key in value.keys():
372 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
373 for key, subparam in expected.iteritems():
374 if isinstance(subparam, Parameter) and \
375 subparam.optional is not None and \
376 not subparam.optional and key not in value.keys():
377 raise PLCInvalidArgument("'%s' not specified" % key, name)
380 auth.check(self, *args)