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
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,caller=None):
63 self.name = self.__class__.__name__
67 # let a method call another one by propagating its caller
70 # Auth may set this to a Person instance (if an anonymous
71 # method, will remain None).
75 # API may set this to a (addr, port) tuple if known
78 def __call__(self, *args, **kwds):
80 Main entry point for all PLCAPI functions. Type checks
81 arguments, authenticates, and executes call().
87 # legacy code cannot be type-checked, due to the way Method.args() works
88 # as of 5.0-rc16 we don't use skip_type_check anymore
89 if not hasattr(self,"skip_type_check"):
90 (min_args, max_args, defaults) = self.args()
92 # Check that the right number of arguments were passed in
93 if len(args) < len(min_args) or len(args) > len(max_args):
94 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
96 for name, value, expected in zip(max_args, args, self.accepts):
97 self.type_check(name, value, expected, args)
99 result = self.call(*args, **kwds)
100 runtime = time.time() - start
102 if self.api.config.PLC_API_DEBUG or hasattr(self, 'message'):
103 self.log(None, runtime, *args)
107 except PLCFault as fault:
110 if isinstance(self.caller, Person):
111 caller = 'person_id %s' % self.caller['person_id']
112 elif isinstance(self.caller, Node):
113 caller = 'node_id %s' % self.caller['node_id']
115 # Prepend caller and method name to expected faults
116 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
117 runtime = time.time() - start
119 if self.api.config.PLC_API_DEBUG:
120 self.log(fault, runtime, *args)
124 def log(self, fault, runtime, *args):
129 # Do not log system or Get calls
130 #if self.name.startswith('system') or self.name.startswith('Get'):
132 # Do not log ReportRunlevel
133 if self.name.startswith('system'):
135 if self.name.startswith('ReportRunlevel'):
139 event = Event(self.api)
140 event['fault_code'] = 0
142 event['fault_code'] = fault.faultCode
143 event['runtime'] = runtime
145 # Redact passwords and sessions
150 if not isinstance(arg, dict):
153 # what type of auth this is
154 if 'AuthMethod' in arg:
155 auth_methods = ['session', 'password', 'capability', 'gpg', 'hmac','anonymous']
156 auth_method = arg['AuthMethod']
157 if auth_method in auth_methods:
158 event['auth_type'] = auth_method
159 for password in 'AuthString', 'session', 'password':
162 arg[password] = "Removed by API"
165 # Log call representation
166 # XXX Truncate to avoid DoS
167 event['call'] = self.name + pprint.saferepr(newargs)
168 event['call_name'] = self.name
170 # Both users and nodes can call some methods
171 if isinstance(self.caller, Person):
172 event['person_id'] = self.caller['person_id']
173 elif isinstance(self.caller, Node):
174 event['node_id'] = self.caller['node_id']
176 event.sync(commit = False)
178 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
179 for key in list(self.event_objects.keys()):
180 for object_id in self.event_objects[key]:
181 event.add_object(key, object_id, commit = False)
184 # Set the message for this event
186 event['message'] = fault.faultString
187 elif hasattr(self, 'message'):
188 event['message'] = self.message
193 def help(self, indent = " "):
195 Text documentation for the method.
198 (min_args, max_args, defaults) = self.args()
200 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
202 text += "Description:\n\n"
203 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
204 text += "\n".join(lines) + "\n\n"
206 text += "Allowed Roles:\n\n"
211 text += indent + ", ".join(roles) + "\n\n"
213 def param_text(name, param, indent, step):
215 Format a method parameter.
220 # Print parameter name
223 text += name.ljust(param_offset - len(indent))
225 param_offset = len(indent)
227 # Print parameter type
228 param_type = python_type(param)
229 text += xmlrpc_type(param_type) + "\n"
231 # Print parameter documentation right below type
232 if isinstance(param, Parameter):
233 wrapper = textwrap.TextWrapper(width = 70,
234 initial_indent = " " * param_offset,
235 subsequent_indent = " " * param_offset)
236 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
241 # Indent struct fields and mixed types
242 if isinstance(param, dict):
243 for name, subparam in param.items():
244 text += param_text(name, subparam, indent + step, step)
245 elif isinstance(param, Mixed):
246 for subparam in param:
247 text += param_text(name, subparam, indent + step, step)
248 elif isinstance(param, (list, tuple, set)):
249 for subparam in param:
250 text += param_text("", subparam, indent + step, step)
254 text += "Parameters:\n\n"
255 for name, param in zip(max_args, self.accepts):
256 text += param_text(name, param, indent, indent)
258 text += "Returns:\n\n"
259 text += param_text("", self.returns, indent, indent)
267 ((arg1_name, arg2_name, ...),
268 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
269 (None, None, ..., optional1_default, optional2_default, ...))
271 That represents the minimum and maximum sets of arguments that
272 this function accepts and the defaults for the optional arguments.
275 # Inspect call. Remove self from the argument list.
276 max_args = self.call.__code__.co_varnames[1:self.call.__code__.co_argcount]
277 defaults = self.call.__defaults__
281 min_args = max_args[0:len(max_args) - len(defaults)]
282 defaults = tuple([None for arg in min_args]) + defaults
284 return (min_args, max_args, defaults)
286 def type_check(self, name, value, expected, args):
288 Checks the type of the named value against the expected type,
289 which may be a Python type, a typed value, a Parameter, a
290 Mixed type, or a list or dictionary of possibly mixed types,
291 values, Parameters, or Mixed types.
293 Extraneous members of lists must be of the same type as the
294 last specified type. For example, if the expected argument
295 type is [int, bool], then [1, False] and [14, True, False,
296 True] are valid, but [1], [False, 1] and [14, True, 1] are
299 Extraneous members of dictionaries are ignored.
302 # If any of a number of types is acceptable
303 if isinstance(expected, Mixed):
304 for item in expected:
306 self.type_check(name, value, item, args)
308 except PLCInvalidArgument as fault:
312 # If an authentication structure is expected, save it and
313 # authenticate after basic type checking is done.
314 if isinstance(expected, Auth):
319 # Get actual expected type from within the Parameter structure
320 if isinstance(expected, Parameter):
323 nullok = expected.nullok
324 expected = expected.type
330 expected_type = python_type(expected)
332 # If value can be NULL
333 if value is None and nullok:
336 # Strings are a special case. Accept either unicode or str
337 # types if a string is expected.
338 if expected_type in StringTypes and isinstance(value, StringTypes):
341 # Integers and long integers are also special types. Accept
342 # either int or long types if an int or long is expected.
343 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
346 elif not isinstance(value, expected_type):
347 raise PLCInvalidArgument("expected %s, got %s" % \
348 (xmlrpc_type(expected_type),
349 xmlrpc_type(type(value))),
352 # If a minimum or maximum (length, value) has been specified
353 if expected_type in StringTypes:
354 if min is not None and \
355 len(value.encode(self.api.encoding)) < min:
356 raise PLCInvalidArgument("%s must be at least %d bytes long" % (name, min))
357 if max is not None and \
358 len(value.encode(self.api.encoding)) > max:
359 raise PLCInvalidArgument("%s must be at most %d bytes long" % (name, max))
360 elif expected_type in (list, tuple, set):
361 if min is not None and len(value) < min:
362 raise PLCInvalidArgument("%s must contain at least %d items" % (name, min))
363 if max is not None and len(value) > max:
364 raise PLCInvalidArgument("%s must contain at most %d items" % (name, max))
366 if min is not None and value < min:
367 raise PLCInvalidArgument("%s must be > %s" % (name, str(min)))
368 if max is not None and value > max:
369 raise PLCInvalidArgument("%s must be < %s" % (name, str(max)))
371 # If a list with particular types of items is expected
372 if isinstance(expected, (list, tuple, set)):
373 for i in range(len(value)):
374 if i >= len(expected):
375 j = len(expected) - 1
378 self.type_check(name + "[]", value[i], expected[j], args)
380 # If a struct with particular (or required) types of items is
382 elif isinstance(expected, dict):
383 for key in list(value.keys()):
385 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
386 for key, subparam in expected.items():
387 if isinstance(subparam, Parameter) and \
388 subparam.optional is not None and \
389 not subparam.optional and key not in list(value.keys()):
390 raise PLCInvalidArgument("'%s' not specified" % key, name)
393 auth.check(self, *args)