60f08716dce4840a9ad58411ac25d60c26d28708
[plcapi.git] / PLC / Method.py
1 #
2 # Base class for all PLCAPI functions
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 import xmlrpclib
8 from types import *
9 import textwrap
10 import os
11 import time
12 import pprint
13
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
23
24 # we inherit object because we use new-style classes for legacy methods
25 class Method (object):
26     """
27     Base class for all PLCAPI functions. At a minimum, all PLCAPI
28     functions must define:
29
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
34
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}).
39
40 w
41 w
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.
45     """
46
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
50     # parser.
51
52     roles = []
53     accepts = []
54     returns = bool
55     status = "current"
56
57     def call(self, *args):
58         """
59         Method body for all PLCAPI functions. Must override.
60         """
61
62         return True
63
64     def __init__(self, api,caller=None):
65         self.name = self.__class__.__name__
66         self.api = api
67         self.api.client_shell = None
68
69         if caller: 
70             # let a method call another one by propagating its caller
71             self.caller=caller
72         else:
73             # Auth may set this to a Person instance (if an anonymous
74             # method, will remain None).
75             self.caller = None
76         
77
78         # API may set this to a (addr, port) tuple if known
79         self.source = None
80
81     def __call__(self, *args, **kwds):
82         """
83         Main entry point for all PLCAPI functions. Type checks
84         arguments, authenticates, and executes call().
85         """
86
87         try:
88             start = time.time()
89
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()
94
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))
98
99                 for name, value, expected in zip(max_args, args, self.accepts):
100                     self.type_check(name, value, expected, args)
101
102             result = self.call(*args, **kwds)
103             runtime = time.time() - start
104
105             return result
106
107         except PLCFault, fault:
108
109             caller = ""
110             if self.caller:  
111                 caller = 'user: %s'  % self.caller.name
112
113             # Prepend caller and method name to expected faults
114             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
115             raise fault
116
117     def help(self, indent = "  "):
118         """
119         Text documentation for the method.
120         """
121
122         (min_args, max_args, defaults) = self.args()
123
124         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
125
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"
129
130         text += "Allowed Roles:\n\n"
131         if not self.roles:
132             roles = ["any"]
133         else:
134             roles = self.roles
135         text += indent + ", ".join(roles) + "\n\n"
136
137         def param_text(name, param, indent, step):
138             """
139             Format a method parameter.
140             """
141
142             text = indent
143
144             # Print parameter name
145             if name:
146                 param_offset = 32
147                 text += name.ljust(param_offset - len(indent))
148             else:
149                 param_offset = len(indent)
150
151             # Print parameter type
152             param_type = python_type(param)
153             text += xmlrpc_type(param_type) + "\n"
154
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"
161                 param = param.type
162
163             text += "\n"
164
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)
175
176             return text
177
178         text += "Parameters:\n\n"
179         for name, param in zip(max_args, self.accepts):
180             text += param_text(name, param, indent, indent)
181
182         text += "Returns:\n\n"
183         text += param_text("", self.returns, indent, indent)
184
185         return text
186
187     def args(self):
188         """
189         Returns a tuple:
190
191         ((arg1_name, arg2_name, ...),
192          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
193          (None, None, ..., optional1_default, optional2_default, ...))
194
195         That represents the minimum and maximum sets of arguments that
196         this function accepts and the defaults for the optional arguments.
197         """
198
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
202         if defaults is None:
203             defaults = ()
204
205         min_args = max_args[0:len(max_args) - len(defaults)]
206         defaults = tuple([None for arg in min_args]) + defaults
207
208         return (min_args, max_args, defaults)
209
210     def type_check(self, name, value, expected, args):
211         """
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.
216
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
221         not.
222
223         Extraneous members of dictionaries are ignored.
224         """
225
226         # If any of a number of types is acceptable
227         if isinstance(expected, Mixed):
228             for item in expected:
229                 try:
230                     self.type_check(name, value, item, args)
231                     return
232                 except PLCInvalidArgument, fault:
233                     pass
234             raise fault
235
236         # If an authentication structure is expected, save it and
237         # authenticate after basic type checking is done.
238         if isinstance(expected, Auth):
239             auth = expected
240         else:
241             auth = None
242
243         # Get actual expected type from within the Parameter structure
244         if isinstance(expected, Parameter):
245             min = expected.min
246             max = expected.max
247             nullok = expected.nullok
248             expected = expected.type
249         else:
250             min = None
251             max = None
252             nullok = False
253
254         expected_type = python_type(expected)
255
256         # If value can be NULL
257         if value is None and nullok:
258             return
259
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):
263             pass
264
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)):
268             pass
269
270         elif not isinstance(value, expected_type):
271             raise PLCInvalidArgument("expected %s, got %s" % \
272                                      (xmlrpc_type(expected_type),
273                                       xmlrpc_type(type(value))),
274                                      name)
275
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)
289         else:
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))
294
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
300                 else:
301                     j = i
302                 self.type_check(name + "[]", value[i], expected[j], args)
303
304         # If a struct with particular (or required) types of items is
305         # expected.
306         elif isinstance(expected, dict):
307             for key in value.keys():
308                 if key in expected:
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)
315
316         if auth is not None:
317             # assume auth structure is first argument 
318             self.authenticate(args[0])
319
320     def authenticate(self, auth):
321
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']})
329         if not persons:
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)
341
342