updated fields
[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.Events import Event, Events
22 from PLC.Nodes import Node, Nodes
23 from PLC.Persons import Person, Persons
24
25 # we inherit object because we use new-style classes for legacy methods
26 class Method (object):
27     """
28     Base class for all PLCAPI functions. At a minimum, all PLCAPI
29     functions must define:
30
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
35
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}).
40
41 w
42 w
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.
46     """
47
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
51     # parser.
52
53     roles = []
54     accepts = []
55     returns = bool
56     status = "current"
57
58     def call(self, *args):
59         """
60         Method body for all PLCAPI functions. Must override.
61         """
62
63         return True
64
65     def __init__(self, api,caller=None):
66         self.name = self.__class__.__name__
67         self.api = api
68         self.api.admin_shell = NovaShell()
69         self.api.client_shell = None
70
71         if caller: 
72             # let a method call another one by propagating its caller
73             self.caller=caller
74         else:
75             # Auth may set this to a Person instance (if an anonymous
76             # method, will remain None).
77             self.caller = None
78         
79
80         # API may set this to a (addr, port) tuple if known
81         self.source = None
82
83     def __call__(self, *args, **kwds):
84         """
85         Main entry point for all PLCAPI functions. Type checks
86         arguments, authenticates, and executes call().
87         """
88
89         try:
90             start = time.time()
91
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()
96
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))
100
101                 for name, value, expected in zip(max_args, args, self.accepts):
102                     self.type_check(name, value, expected, args)
103
104             result = self.call(*args, **kwds)
105             runtime = time.time() - start
106
107             return result
108
109         except PLCFault, fault:
110
111             caller = ""
112             if self.caller:  
113                 caller = 'user: %s'  % self.caller.name
114
115             # Prepend caller and method name to expected faults
116             fault.faultString = caller + ": " +  self.name + ": " + fault.faultString
117             raise fault
118
119     def help(self, indent = "  "):
120         """
121         Text documentation for the method.
122         """
123
124         (min_args, max_args, defaults) = self.args()
125
126         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
127
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"
131
132         text += "Allowed Roles:\n\n"
133         if not self.roles:
134             roles = ["any"]
135         else:
136             roles = self.roles
137         text += indent + ", ".join(roles) + "\n\n"
138
139         def param_text(name, param, indent, step):
140             """
141             Format a method parameter.
142             """
143
144             text = indent
145
146             # Print parameter name
147             if name:
148                 param_offset = 32
149                 text += name.ljust(param_offset - len(indent))
150             else:
151                 param_offset = len(indent)
152
153             # Print parameter type
154             param_type = python_type(param)
155             text += xmlrpc_type(param_type) + "\n"
156
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"
163                 param = param.type
164
165             text += "\n"
166
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)
177
178             return text
179
180         text += "Parameters:\n\n"
181         for name, param in zip(max_args, self.accepts):
182             text += param_text(name, param, indent, indent)
183
184         text += "Returns:\n\n"
185         text += param_text("", self.returns, indent, indent)
186
187         return text
188
189     def args(self):
190         """
191         Returns a tuple:
192
193         ((arg1_name, arg2_name, ...),
194          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
195          (None, None, ..., optional1_default, optional2_default, ...))
196
197         That represents the minimum and maximum sets of arguments that
198         this function accepts and the defaults for the optional arguments.
199         """
200
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
204         if defaults is None:
205             defaults = ()
206
207         min_args = max_args[0:len(max_args) - len(defaults)]
208         defaults = tuple([None for arg in min_args]) + defaults
209
210         return (min_args, max_args, defaults)
211
212     def type_check(self, name, value, expected, args):
213         """
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.
218
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
223         not.
224
225         Extraneous members of dictionaries are ignored.
226         """
227
228         # If any of a number of types is acceptable
229         if isinstance(expected, Mixed):
230             for item in expected:
231                 try:
232                     self.type_check(name, value, item, args)
233                     return
234                 except PLCInvalidArgument, fault:
235                     pass
236             raise fault
237
238         # If an authentication structure is expected, save it and
239         # authenticate after basic type checking is done.
240         if isinstance(expected, Auth):
241             auth = expected
242         else:
243             auth = None
244
245         # Get actual expected type from within the Parameter structure
246         if isinstance(expected, Parameter):
247             min = expected.min
248             max = expected.max
249             nullok = expected.nullok
250             expected = expected.type
251         else:
252             min = None
253             max = None
254             nullok = False
255
256         expected_type = python_type(expected)
257
258         # If value can be NULL
259         if value is None and nullok:
260             return
261
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):
265             pass
266
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)):
270             pass
271
272         elif not isinstance(value, expected_type):
273             raise PLCInvalidArgument("expected %s, got %s" % \
274                                      (xmlrpc_type(expected_type),
275                                       xmlrpc_type(type(value))),
276                                      name)
277
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)
291         else:
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))
296
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
302                 else:
303                     j = i
304                 self.type_check(name + "[]", value[i], expected[j], args)
305
306         # If a struct with particular (or required) types of items is
307         # expected.
308         elif isinstance(expected, dict):
309             for key in value.keys():
310                 if key in expected:
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)
317
318         if auth is not None:
319             # assume auth structure is first argument 
320             self.authenticate(args[0])
321
322     def authenticate(self, auth):
323
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)
338
339