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.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}).
42 Once function decorators in Python 2.4 are fully supported,
43 consider wrapping calls with accepts() and returns() functions
44 instead of performing type checking manually.
47 # Defaults. Could implement authentication and type checking with
48 # decorators, but they are not supported in Python 2.3 and it
49 # would be hard to generate documentation without writing a code
57 def call(self, *args):
59 Method body for all PLCAPI functions. Must override.
64 def __init__(self, api,caller=None):
65 self.name = self.__class__.__name__
67 self.api.admin_shell = NovaShell()
68 self.api.client_shell = None
71 # let a method call another one by propagating its caller
74 # Auth may set this to a Person instance (if an anonymous
75 # method, will remain None).
79 # API may set this to a (addr, port) tuple if known
82 def __call__(self, *args, **kwds):
84 Main entry point for all PLCAPI functions. Type checks
85 arguments, authenticates, and executes call().
91 # legacy code cannot be type-checked, due to the way Method.args() works
92 # as of 5.0-rc16 we don't use skip_type_check anymore
93 if not hasattr(self,"skip_type_check"):
94 (min_args, max_args, defaults) = self.args()
96 # Check that the right number of arguments were passed in
97 if len(args) < len(min_args) or len(args) > len(max_args):
98 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
100 for name, value, expected in zip(max_args, args, self.accepts):
101 self.type_check(name, value, expected, args)
103 result = self.call(*args, **kwds)
104 runtime = time.time() - start
108 except PLCFault, fault:
112 caller = 'user: %s' % self.caller.name
114 # Prepend caller and method name to expected faults
115 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
118 def help(self, indent = " "):
120 Text documentation for the method.
123 (min_args, max_args, defaults) = self.args()
125 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
127 text += "Description:\n\n"
128 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
129 text += "\n".join(lines) + "\n\n"
131 text += "Allowed Roles:\n\n"
136 text += indent + ", ".join(roles) + "\n\n"
138 def param_text(name, param, indent, step):
140 Format a method parameter.
145 # Print parameter name
148 text += name.ljust(param_offset - len(indent))
150 param_offset = len(indent)
152 # Print parameter type
153 param_type = python_type(param)
154 text += xmlrpc_type(param_type) + "\n"
156 # Print parameter documentation right below type
157 if isinstance(param, Parameter):
158 wrapper = textwrap.TextWrapper(width = 70,
159 initial_indent = " " * param_offset,
160 subsequent_indent = " " * param_offset)
161 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
166 # Indent struct fields and mixed types
167 if isinstance(param, dict):
168 for name, subparam in param.iteritems():
169 text += param_text(name, subparam, indent + step, step)
170 elif isinstance(param, Mixed):
171 for subparam in param:
172 text += param_text(name, subparam, indent + step, step)
173 elif isinstance(param, (list, tuple, set)):
174 for subparam in param:
175 text += param_text("", subparam, indent + step, step)
179 text += "Parameters:\n\n"
180 for name, param in zip(max_args, self.accepts):
181 text += param_text(name, param, indent, indent)
183 text += "Returns:\n\n"
184 text += param_text("", self.returns, indent, indent)
192 ((arg1_name, arg2_name, ...),
193 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
194 (None, None, ..., optional1_default, optional2_default, ...))
196 That represents the minimum and maximum sets of arguments that
197 this function accepts and the defaults for the optional arguments.
200 # Inspect call. Remove self from the argument list.
201 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
202 defaults = self.call.func_defaults
206 min_args = max_args[0:len(max_args) - len(defaults)]
207 defaults = tuple([None for arg in min_args]) + defaults
209 return (min_args, max_args, defaults)
211 def type_check(self, name, value, expected, args):
213 Checks the type of the named value against the expected type,
214 which may be a Python type, a typed value, a Parameter, a
215 Mixed type, or a list or dictionary of possibly mixed types,
216 values, Parameters, or Mixed types.
218 Extraneous members of lists must be of the same type as the
219 last specified type. For example, if the expected argument
220 type is [int, bool], then [1, False] and [14, True, False,
221 True] are valid, but [1], [False, 1] and [14, True, 1] are
224 Extraneous members of dictionaries are ignored.
227 # If any of a number of types is acceptable
228 if isinstance(expected, Mixed):
229 for item in expected:
231 self.type_check(name, value, item, args)
233 except PLCInvalidArgument, fault:
237 # If an authentication structure is expected, save it and
238 # authenticate after basic type checking is done.
239 if isinstance(expected, Auth):
244 # Get actual expected type from within the Parameter structure
245 if isinstance(expected, Parameter):
248 nullok = expected.nullok
249 expected = expected.type
255 expected_type = python_type(expected)
257 # If value can be NULL
258 if value is None and nullok:
261 # Strings are a special case. Accept either unicode or str
262 # types if a string is expected.
263 if expected_type in StringTypes and isinstance(value, StringTypes):
266 # Integers and long integers are also special types. Accept
267 # either int or long types if an int or long is expected.
268 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
271 elif not isinstance(value, expected_type):
272 raise PLCInvalidArgument("expected %s, got %s" % \
273 (xmlrpc_type(expected_type),
274 xmlrpc_type(type(value))),
277 # If a minimum or maximum (length, value) has been specified
278 if expected_type in StringTypes:
279 if min is not None and \
280 len(value.encode(self.api.encoding)) < min:
281 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
282 if max is not None and \
283 len(value.encode(self.api.encoding)) > max:
284 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
285 elif expected_type in (list, tuple, set):
286 if min is not None and len(value) < min:
287 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
288 if max is not None and len(value) > max:
289 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
291 if min is not None and value < min:
292 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
293 if max is not None and value > max:
294 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
296 # If a list with particular types of items is expected
297 if isinstance(expected, (list, tuple, set)):
298 for i in range(len(value)):
299 if i >= len(expected):
300 j = len(expected) - 1
303 self.type_check(name + "[]", value[i], expected[j], args)
305 # If a struct with particular (or required) types of items is
307 elif isinstance(expected, dict):
308 for key in value.keys():
310 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
311 for key, subparam in expected.iteritems():
312 if isinstance(subparam, Parameter) and \
313 subparam.optional is not None and \
314 not subparam.optional and key not in value.keys():
315 raise PLCInvalidArgument("'%s' not specified" % key, name)
318 # assume auth structure is first argument
319 self.authenticate(args[0])
321 def authenticate(self, auth):
323 # establish nova connection
324 self.api.client_shell = NovaShell(user=auth['Username'],
325 password=auth['AuthString'],
326 tenant=auth['Tenant'])
327 self.api.client_shell.authenticate()
328 self.caller = self.api.client_shell.keystone.users.find(name=auth['Username'])
329 self.caller_tenant = self.api.client_shell.keystone.tenants.find(name=auth['Tenant'])
330 caller_roles = self.api.client_shell.keystone.roles.roles_for_user(self.caller, self.caller_tenant)
331 role_names = [role.name for role in caller_roles]
332 if not set(role_names).intersection(self.roles):
333 method_message="method %s has roles [%s]"%(self.name,','.join(self.roles))
334 person_message="caller %s has roles [%s]"%(self.caller.name,','.join(role_names))
335 raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)