2 # Base class for all PLCAPI functions
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
12 from PLC.Faults import *
13 from PLC.Parameter import Parameter, Mixed, python_type, xmlrpc_type
14 from PLC.Auth import Auth
15 #from PLC.Debug import profile
16 from PLC.Events import Event, Events
17 from PLC.Nodes import Node, Nodes
18 from PLC.Persons import Person, Persons
20 # we inherit object because we use new-style classes for legacy methods
23 Base class for all PLCAPI functions. At a minimum, all PLCAPI
24 functions must define:
26 roles = [list of roles]
27 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
28 returns = Parameter(return_type, return_doc)
29 call(arg1, arg2, ...): method body
31 Argument types may be Python types (e.g., int, bool, etc.), typed
32 values (e.g., 1, True, etc.), a Parameter, or lists or
33 dictionaries of possibly mixed types, values, and/or Parameters
34 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
36 Once function decorators in Python 2.4 are fully supported,
37 consider wrapping calls with accepts() and returns() functions
38 instead of performing type checking manually.
41 # Defaults. Could implement authentication and type checking with
42 # decorators, but they are not supported in Python 2.3 and it
43 # would be hard to generate documentation without writing a code
51 def call(self, *args):
53 Method body for all PLCAPI functions. Must override.
58 def __init__(self, api, caller=None):
59 self.name = self.__class__.__name__
63 # let a method call another one by propagating its caller
66 # Auth may set this to a Person instance (if an anonymous
67 # method, will remain None).
71 # API may set this to a (addr, port) tuple if known
74 def __call__(self, *args, **kwds):
76 Main entry point for all PLCAPI functions. Type checks
77 arguments, authenticates, and executes call().
83 # legacy code cannot be type-checked, due to the way Method.args() works
84 # as of 5.0-rc16 we don't use skip_type_check anymore
85 if not hasattr(self, "skip_type_check"):
86 (min_args, max_args, defaults) = self.args()
88 # Check that the right number of arguments were passed in
89 if len(args) < len(min_args) or len(args) > len(max_args):
90 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
92 for name, value, expected in zip(max_args, args, self.accepts):
93 self.type_check(name, value, expected, args)
95 result = self.call(*args, **kwds)
96 runtime = time.time() - start
98 if self.api.config.PLC_API_DEBUG or hasattr(self, 'message'):
99 self.log(None, runtime, *args)
103 except PLCFault as fault:
106 if isinstance(self.caller, Person):
107 caller = 'person_id %s' % self.caller['person_id']
108 elif isinstance(self.caller, Node):
109 caller = 'node_id %s' % self.caller['node_id']
111 # Prepend caller and method name to expected faults
112 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
113 runtime = time.time() - start
115 if self.api.config.PLC_API_DEBUG:
116 self.log(fault, runtime, *args)
120 def log(self, fault, runtime, *args):
125 # Do not log system or Get calls
126 #if self.name.startswith('system') or self.name.startswith('Get'):
128 # Do not log ReportRunlevel
129 if self.name.startswith('system'):
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
146 if not isinstance(arg, dict):
149 # what type of auth this is
150 if 'AuthMethod' in arg:
151 auth_methods = ['session', 'password', 'capability',
152 'gpg', 'hmac', 'anonymous']
153 auth_method = arg['AuthMethod']
154 if auth_method in auth_methods:
155 event['auth_type'] = auth_method
156 for password in 'AuthString', 'session', 'password':
159 arg[password] = "Removed by API"
162 # Log call representation
163 # XXX Truncate to avoid DoS
164 event['call'] = self.name + pprint.saferepr(newargs)
165 event['call_name'] = self.name
167 # Both users and nodes can call some methods
168 if isinstance(self.caller, Person):
169 event['person_id'] = self.caller['person_id']
170 elif isinstance(self.caller, Node):
171 event['node_id'] = self.caller['node_id']
173 event.sync(commit=False)
175 if hasattr(self, 'event_objects') and isinstance(self.event_objects, dict):
176 for key in self.event_objects.keys():
177 for object_id in self.event_objects[key]:
178 event.add_object(key, object_id, commit=False)
181 # Set the message for this event
183 event['message'] = fault.faultString
184 elif hasattr(self, 'message'):
185 event['message'] = self.message
190 def help(self, indent=" "):
192 Text documentation for the method.
195 (min_args, max_args, defaults) = self.args()
197 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
199 text += "Description:\n\n"
200 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
201 text += "\n".join(lines) + "\n\n"
203 text += "Allowed Roles:\n\n"
208 text += indent + ", ".join(roles) + "\n\n"
210 def param_text(name, param, indent, step):
212 Format a method parameter.
217 # Print parameter name
220 text += name.ljust(param_offset - len(indent))
222 param_offset = len(indent)
224 # Print parameter type
225 param_type = python_type(param)
226 text += xmlrpc_type(param_type) + "\n"
228 # Print parameter documentation right below type
229 if isinstance(param, Parameter):
230 wrapper = textwrap.TextWrapper(
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.items():
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.__code__.co_varnames[1:self.call.__code__.co_argcount]
275 defaults = self.call.__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 # try them one by one, if one succeeds then it's fine
302 if expected and 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 is str and isinstance(value, str):
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 is int and isinstance(value, int):
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 is str:
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)