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
15 from PLC.NovaShell import NovaShell
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.Timestamp import Timestamp
20 from PLC.Debug import profile
21 from PLC.Events import Event, Events
22 from PLC.Nodes import Node, Nodes
23 from PLC.Persons import Person, Persons
25 # we inherit object because we use new-style classes for legacy methods
26 class Method (object):
28 Base class for all PLCAPI functions. At a minimum, all PLCAPI
29 functions must define:
31 roles = [list of roles]
32 accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
33 returns = Parameter(return_type, return_doc)
34 call(arg1, arg2, ...): method body
36 Argument types may be Python types (e.g., int, bool, etc.), typed
37 values (e.g., 1, True, etc.), a Parameter, or lists or
38 dictionaries of possibly mixed types, values, and/or Parameters
39 (e.g., [int, bool, ...] or {'arg1': int, 'arg2': bool}).
43 Once function decorators in Python 2.4 are fully supported,
44 consider wrapping calls with accepts() and returns() functions
45 instead of performing type checking manually.
48 # Defaults. Could implement authentication and type checking with
49 # decorators, but they are not supported in Python 2.3 and it
50 # would be hard to generate documentation without writing a code
58 def call(self, *args):
60 Method body for all PLCAPI functions. Must override.
65 def __init__(self, api,caller=None):
66 self.name = self.__class__.__name__
68 self.api.admin_shell = NovaShell()
69 self.api.client_shell = None
72 # let a method call another one by propagating its caller
75 # Auth may set this to a Person instance (if an anonymous
76 # method, will remain None).
80 # API may set this to a (addr, port) tuple if known
83 def __call__(self, *args, **kwds):
85 Main entry point for all PLCAPI functions. Type checks
86 arguments, authenticates, and executes call().
92 # legacy code cannot be type-checked, due to the way Method.args() works
93 # as of 5.0-rc16 we don't use skip_type_check anymore
94 if not hasattr(self,"skip_type_check"):
95 (min_args, max_args, defaults) = self.args()
97 # Check that the right number of arguments were passed in
98 if len(args) < len(min_args) or len(args) > len(max_args):
99 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
101 for name, value, expected in zip(max_args, args, self.accepts):
102 self.type_check(name, value, expected, args)
104 result = self.call(*args, **kwds)
105 runtime = time.time() - start
109 except PLCFault, fault:
113 caller = 'user: %s' % self.caller.name
115 # Prepend caller and method name to expected faults
116 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
119 def help(self, indent = " "):
121 Text documentation for the method.
124 (min_args, max_args, defaults) = self.args()
126 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
128 text += "Description:\n\n"
129 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
130 text += "\n".join(lines) + "\n\n"
132 text += "Allowed Roles:\n\n"
137 text += indent + ", ".join(roles) + "\n\n"
139 def param_text(name, param, indent, step):
141 Format a method parameter.
146 # Print parameter name
149 text += name.ljust(param_offset - len(indent))
151 param_offset = len(indent)
153 # Print parameter type
154 param_type = python_type(param)
155 text += xmlrpc_type(param_type) + "\n"
157 # Print parameter documentation right below type
158 if isinstance(param, Parameter):
159 wrapper = textwrap.TextWrapper(width = 70,
160 initial_indent = " " * param_offset,
161 subsequent_indent = " " * param_offset)
162 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
167 # Indent struct fields and mixed types
168 if isinstance(param, dict):
169 for name, subparam in param.iteritems():
170 text += param_text(name, subparam, indent + step, step)
171 elif isinstance(param, Mixed):
172 for subparam in param:
173 text += param_text(name, subparam, indent + step, step)
174 elif isinstance(param, (list, tuple, set)):
175 for subparam in param:
176 text += param_text("", subparam, indent + step, step)
180 text += "Parameters:\n\n"
181 for name, param in zip(max_args, self.accepts):
182 text += param_text(name, param, indent, indent)
184 text += "Returns:\n\n"
185 text += param_text("", self.returns, indent, indent)
193 ((arg1_name, arg2_name, ...),
194 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
195 (None, None, ..., optional1_default, optional2_default, ...))
197 That represents the minimum and maximum sets of arguments that
198 this function accepts and the defaults for the optional arguments.
201 # Inspect call. Remove self from the argument list.
202 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
203 defaults = self.call.func_defaults
207 min_args = max_args[0:len(max_args) - len(defaults)]
208 defaults = tuple([None for arg in min_args]) + defaults
210 return (min_args, max_args, defaults)
212 def type_check(self, name, value, expected, args):
214 Checks the type of the named value against the expected type,
215 which may be a Python type, a typed value, a Parameter, a
216 Mixed type, or a list or dictionary of possibly mixed types,
217 values, Parameters, or Mixed types.
219 Extraneous members of lists must be of the same type as the
220 last specified type. For example, if the expected argument
221 type is [int, bool], then [1, False] and [14, True, False,
222 True] are valid, but [1], [False, 1] and [14, True, 1] are
225 Extraneous members of dictionaries are ignored.
228 # If any of a number of types is acceptable
229 if isinstance(expected, Mixed):
230 for item in expected:
232 self.type_check(name, value, item, args)
234 except PLCInvalidArgument, fault:
238 # If an authentication structure is expected, save it and
239 # authenticate after basic type checking is done.
240 if isinstance(expected, Auth):
245 # Get actual expected type from within the Parameter structure
246 if isinstance(expected, Parameter):
249 nullok = expected.nullok
250 expected = expected.type
256 expected_type = python_type(expected)
258 # If value can be NULL
259 if value is None and nullok:
262 # Strings are a special case. Accept either unicode or str
263 # types if a string is expected.
264 if expected_type in StringTypes and isinstance(value, StringTypes):
267 # Integers and long integers are also special types. Accept
268 # either int or long types if an int or long is expected.
269 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
272 elif not isinstance(value, expected_type):
273 raise PLCInvalidArgument("expected %s, got %s" % \
274 (xmlrpc_type(expected_type),
275 xmlrpc_type(type(value))),
278 # If a minimum or maximum (length, value) has been specified
279 if expected_type in StringTypes:
280 if min is not None and \
281 len(value.encode(self.api.encoding)) < min:
282 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
283 if max is not None and \
284 len(value.encode(self.api.encoding)) > max:
285 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
286 elif expected_type in (list, tuple, set):
287 if min is not None and len(value) < min:
288 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
289 if max is not None and len(value) > max:
290 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
292 if min is not None and value < min:
293 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
294 if max is not None and value > max:
295 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
297 # If a list with particular types of items is expected
298 if isinstance(expected, (list, tuple, set)):
299 for i in range(len(value)):
300 if i >= len(expected):
301 j = len(expected) - 1
304 self.type_check(name + "[]", value[i], expected[j], args)
306 # If a struct with particular (or required) types of items is
308 elif isinstance(expected, dict):
309 for key in value.keys():
311 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
312 for key, subparam in expected.iteritems():
313 if isinstance(subparam, Parameter) and \
314 subparam.optional is not None and \
315 not subparam.optional and key not in value.keys():
316 raise PLCInvalidArgument("'%s' not specified" % key, name)
319 # assume auth structure is first argument
320 self.authenticate(args[0])
322 def authenticate(self, auth):
324 # establish nova connection
325 self.api.client_shell = NovaShell(user=auth['Username'],
326 password=auth['AuthString'],
327 tenant=auth['Tenant'])
328 self.api.client_shell.authenticate()
329 self.caller = Person(self.api, object=self.api.client_shell.keystone.users.find(name=auth['Username']))
330 self.caller_tenant = self.api.client_shell.keystone.tenants.find(name=auth['Tenant'])
331 caller_roles = self.api.client_shell.keystone.roles.roles_for_user(self.caller, self.caller_tenant)
332 role_names = [role.name for role in caller_roles]
333 self.caller['roles'] = role_names
334 if not set(role_names).intersection(self.roles):
335 method_message="method %s has roles [%s]"%(self.name,','.join(self.roles))
336 person_message="caller %s has roles [%s]"%(self.caller.name,','.join(role_names))
337 raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)