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.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.client_shell = None
70 # let a method call another one by propagating its caller
73 # Auth may set this to a Person instance (if an anonymous
74 # method, will remain None).
78 # API may set this to a (addr, port) tuple if known
81 def __call__(self, *args, **kwds):
83 Main entry point for all PLCAPI functions. Type checks
84 arguments, authenticates, and executes call().
90 # legacy code cannot be type-checked, due to the way Method.args() works
91 # as of 5.0-rc16 we don't use skip_type_check anymore
92 if not hasattr(self,"skip_type_check"):
93 (min_args, max_args, defaults) = self.args()
95 # Check that the right number of arguments were passed in
96 if len(args) < len(min_args) or len(args) > len(max_args):
97 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
99 for name, value, expected in zip(max_args, args, self.accepts):
100 self.type_check(name, value, expected, args)
102 result = self.call(*args, **kwds)
103 runtime = time.time() - start
107 except PLCFault, fault:
111 caller = 'user: %s' % self.caller.name
113 # Prepend caller and method name to expected faults
114 fault.faultString = caller + ": " + self.name + ": " + fault.faultString
117 def help(self, indent = " "):
119 Text documentation for the method.
122 (min_args, max_args, defaults) = self.args()
124 text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
126 text += "Description:\n\n"
127 lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
128 text += "\n".join(lines) + "\n\n"
130 text += "Allowed Roles:\n\n"
135 text += indent + ", ".join(roles) + "\n\n"
137 def param_text(name, param, indent, step):
139 Format a method parameter.
144 # Print parameter name
147 text += name.ljust(param_offset - len(indent))
149 param_offset = len(indent)
151 # Print parameter type
152 param_type = python_type(param)
153 text += xmlrpc_type(param_type) + "\n"
155 # Print parameter documentation right below type
156 if isinstance(param, Parameter):
157 wrapper = textwrap.TextWrapper(width = 70,
158 initial_indent = " " * param_offset,
159 subsequent_indent = " " * param_offset)
160 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
165 # Indent struct fields and mixed types
166 if isinstance(param, dict):
167 for name, subparam in param.iteritems():
168 text += param_text(name, subparam, indent + step, step)
169 elif isinstance(param, Mixed):
170 for subparam in param:
171 text += param_text(name, subparam, indent + step, step)
172 elif isinstance(param, (list, tuple, set)):
173 for subparam in param:
174 text += param_text("", subparam, indent + step, step)
178 text += "Parameters:\n\n"
179 for name, param in zip(max_args, self.accepts):
180 text += param_text(name, param, indent, indent)
182 text += "Returns:\n\n"
183 text += param_text("", self.returns, indent, indent)
191 ((arg1_name, arg2_name, ...),
192 (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
193 (None, None, ..., optional1_default, optional2_default, ...))
195 That represents the minimum and maximum sets of arguments that
196 this function accepts and the defaults for the optional arguments.
199 # Inspect call. Remove self from the argument list.
200 max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
201 defaults = self.call.func_defaults
205 min_args = max_args[0:len(max_args) - len(defaults)]
206 defaults = tuple([None for arg in min_args]) + defaults
208 return (min_args, max_args, defaults)
210 def type_check(self, name, value, expected, args):
212 Checks the type of the named value against the expected type,
213 which may be a Python type, a typed value, a Parameter, a
214 Mixed type, or a list or dictionary of possibly mixed types,
215 values, Parameters, or Mixed types.
217 Extraneous members of lists must be of the same type as the
218 last specified type. For example, if the expected argument
219 type is [int, bool], then [1, False] and [14, True, False,
220 True] are valid, but [1], [False, 1] and [14, True, 1] are
223 Extraneous members of dictionaries are ignored.
226 # If any of a number of types is acceptable
227 if isinstance(expected, Mixed):
228 for item in expected:
230 self.type_check(name, value, item, args)
232 except PLCInvalidArgument, fault:
236 # If an authentication structure is expected, save it and
237 # authenticate after basic type checking is done.
238 if isinstance(expected, Auth):
243 # Get actual expected type from within the Parameter structure
244 if isinstance(expected, Parameter):
247 nullok = expected.nullok
248 expected = expected.type
254 expected_type = python_type(expected)
256 # If value can be NULL
257 if value is None and nullok:
260 # Strings are a special case. Accept either unicode or str
261 # types if a string is expected.
262 if expected_type in StringTypes and isinstance(value, StringTypes):
265 # Integers and long integers are also special types. Accept
266 # either int or long types if an int or long is expected.
267 elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
270 elif not isinstance(value, expected_type):
271 raise PLCInvalidArgument("expected %s, got %s" % \
272 (xmlrpc_type(expected_type),
273 xmlrpc_type(type(value))),
276 # If a minimum or maximum (length, value) has been specified
277 if expected_type in StringTypes:
278 if min is not None and \
279 len(value.encode(self.api.encoding)) < min:
280 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
281 if max is not None and \
282 len(value.encode(self.api.encoding)) > max:
283 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
284 elif expected_type in (list, tuple, set):
285 if min is not None and len(value) < min:
286 raise PLCInvalidArgument, "%s must contain at least %d items" % (name, min)
287 if max is not None and len(value) > max:
288 raise PLCInvalidArgument, "%s must contain at most %d items" % (name, max)
290 if min is not None and value < min:
291 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
292 if max is not None and value > max:
293 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
295 # If a list with particular types of items is expected
296 if isinstance(expected, (list, tuple, set)):
297 for i in range(len(value)):
298 if i >= len(expected):
299 j = len(expected) - 1
302 self.type_check(name + "[]", value[i], expected[j], args)
304 # If a struct with particular (or required) types of items is
306 elif isinstance(expected, dict):
307 for key in value.keys():
309 self.type_check(name + "['%s']" % key, value[key], expected[key], args)
310 for key, subparam in expected.iteritems():
311 if isinstance(subparam, Parameter) and \
312 subparam.optional is not None and \
313 not subparam.optional and key not in value.keys():
314 raise PLCInvalidArgument("'%s' not specified" % key, name)
317 # assume auth structure is first argument
318 self.authenticate(args[0])
320 def authenticate(self, auth):
322 # establish nova connection
323 keystone_name = auth['Username'][:auth['Username'].index('@')]
324 self.api.client_shell = NovaShell(username=keystone_name,
325 password=auth['AuthString'],
326 tenant=auth['Tenant'])
327 self.api.client_shell.authenticate()
328 persons = Persons(self.api, {'email': auth['Username']})
330 raise PLCAuthenticationFailure, "Username not found"
331 self.caller = persons[0]
332 keystone_user = self.api.client_shell.keystone.users.find(id=self.caller['keystone_id'])
333 self.caller_tenant = self.api.client_shell.keystone.tenants.find(name=auth['Tenant'])
334 caller_roles = self.api.client_shell.keystone.roles.roles_for_user(self.caller, self.caller_tenant)
335 role_names = [role.name for role in caller_roles]
336 self.caller['roles'] = role_names
337 if not set(role_names).intersection(self.roles):
338 method_message="method %s has roles [%s]"%(self.name,','.join(self.roles))
339 person_message="caller %s has roles [%s]"%(self.caller.name,','.join(role_names))
340 raise PLCAuthenticationFailure, "PasswordAuth: missing role, %s -- %s"%(method_message,person_message)