fix bugs
[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.admin_shell = NovaShell()
68         self.api.client_shell = None
69
70         if caller: 
71             # let a method call another one by propagating its caller
72             self.caller=caller
73         else:
74             # Auth may set this to a Person instance (if an anonymous
75             # method, will remain None).
76             self.caller = None
77         
78
79         # API may set this to a (addr, port) tuple if known
80         self.source = None
81
82     def __call__(self, *args, **kwds):
83         """
84         Main entry point for all PLCAPI functions. Type checks
85         arguments, authenticates, and executes call().
86         """
87
88         try:
89             start = time.time()
90
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()
95
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))
99
100                 for name, value, expected in zip(max_args, args, self.accepts):
101                     self.type_check(name, value, expected, args)
102
103             result = self.call(*args, **kwds)
104             runtime = time.time() - start
105
106             return result
107
108         except PLCFault, fault:
109
110             caller = ""
111             if self.caller:  
112                 caller = 'user: %s'  % self.caller.name
113
114             # Prepend caller and method name to expected faults
115             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
116             raise fault
117
118     def help(self, indent = "  "):
119         """
120         Text documentation for the method.
121         """
122
123         (min_args, max_args, defaults) = self.args()
124
125         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
126
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"
130
131         text += "Allowed Roles:\n\n"
132         if not self.roles:
133             roles = ["any"]
134         else:
135             roles = self.roles
136         text += indent + ", ".join(roles) + "\n\n"
137
138         def param_text(name, param, indent, step):
139             """
140             Format a method parameter.
141             """
142
143             text = indent
144
145             # Print parameter name
146             if name:
147                 param_offset = 32
148                 text += name.ljust(param_offset - len(indent))
149             else:
150                 param_offset = len(indent)
151
152             # Print parameter type
153             param_type = python_type(param)
154             text += xmlrpc_type(param_type) + "\n"
155
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"
162                 param = param.type
163
164             text += "\n"
165
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)
176
177             return text
178
179         text += "Parameters:\n\n"
180         for name, param in zip(max_args, self.accepts):
181             text += param_text(name, param, indent, indent)
182
183         text += "Returns:\n\n"
184         text += param_text("", self.returns, indent, indent)
185
186         return text
187
188     def args(self):
189         """
190         Returns a tuple:
191
192         ((arg1_name, arg2_name, ...),
193          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
194          (None, None, ..., optional1_default, optional2_default, ...))
195
196         That represents the minimum and maximum sets of arguments that
197         this function accepts and the defaults for the optional arguments.
198         """
199
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
203         if defaults is None:
204             defaults = ()
205
206         min_args = max_args[0:len(max_args) - len(defaults)]
207         defaults = tuple([None for arg in min_args]) + defaults
208
209         return (min_args, max_args, defaults)
210
211     def type_check(self, name, value, expected, args):
212         """
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.
217
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
222         not.
223
224         Extraneous members of dictionaries are ignored.
225         """
226
227         # If any of a number of types is acceptable
228         if isinstance(expected, Mixed):
229             for item in expected:
230                 try:
231                     self.type_check(name, value, item, args)
232                     return
233                 except PLCInvalidArgument, fault:
234                     pass
235             raise fault
236
237         # If an authentication structure is expected, save it and
238         # authenticate after basic type checking is done.
239         if isinstance(expected, Auth):
240             auth = expected
241         else:
242             auth = None
243
244         # Get actual expected type from within the Parameter structure
245         if isinstance(expected, Parameter):
246             min = expected.min
247             max = expected.max
248             nullok = expected.nullok
249             expected = expected.type
250         else:
251             min = None
252             max = None
253             nullok = False
254
255         expected_type = python_type(expected)
256
257         # If value can be NULL
258         if value is None and nullok:
259             return
260
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):
264             pass
265
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)):
269             pass
270
271         elif not isinstance(value, expected_type):
272             raise PLCInvalidArgument("expected %s, got %s" % \
273                                      (xmlrpc_type(expected_type),
274                                       xmlrpc_type(type(value))),
275                                      name)
276
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)
290         else:
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))
295
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
301                 else:
302                     j = i
303                 self.type_check(name + "[]", value[i], expected[j], args)
304
305         # If a struct with particular (or required) types of items is
306         # expected.
307         elif isinstance(expected, dict):
308             for key in value.keys():
309                 if key in expected:
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)
316
317         if auth is not None:
318             # assume auth structure is first argument 
319             self.authenticate(args[0])
320
321     def authenticate(self, auth):
322
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         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