- log the ids of objects affected by api calls
[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 # $Id: Method.py,v 1.5 2006/10/16 20:41:02 tmack Exp $
8 #
9
10 import xmlrpclib
11 from types import *
12 import textwrap
13 import os
14 import time
15
16 from PLC.Faults import *
17 from PLC.Parameter import Parameter, Mixed
18 from PLC.Auth import Auth
19 from PLC.Debug import profile, log
20
21 class Method:
22     """
23     Base class for all PLCAPI functions. At a minimum, all PLCAPI
24     functions must define:
25
26     roles = [list of roles]
27     accepts = [Parameter(arg1_type, arg1_doc), Parameter(arg2_type, arg2_doc), ...]
28     returns = Parameter(return_type, return_doc)
29     call(arg1, arg2, ...): method body
30
31     Argument types may be Python types (e.g., int, bool, etc.), typed
32     values (e.g., 1, True, etc.), a Parameter, or lists or
33     dictionaries of possibly mixed types, values, and/or Parameters
34     (e.g., [int, bool, ...]  or {'arg1': int, 'arg2': bool}).
35
36     Once function decorators in Python 2.4 are fully supported,
37     consider wrapping calls with accepts() and returns() functions
38     instead of performing type checking manually.
39     """
40
41     # Defaults. Could implement authentication and type checking with
42     # decorators, but they are not supported in Python 2.3 and it
43     # would be hard to generate documentation without writing a code
44     # parser.
45
46     roles = []
47     accepts = []
48     returns = bool
49     status = "current"
50
51     def call(self, *args):
52         """
53         Method body for all PLCAPI functions. Must override.
54         """
55
56         return True
57
58     def __init__(self, api):
59         self.name = self.__class__.__name__
60         self.api = api
61
62         # Auth may set this to a Person instance (if an anonymous
63         # method, will remain None).
64         self.caller = None
65
66         # API may set this to a (addr, port) tuple if known
67         self.source = None
68
69         
70     def __call__(self, *args):
71         """
72         Main entry point for all PLCAPI functions. Type checks
73         arguments, authenticates, and executes call().
74         """
75
76         try:
77             (min_args, max_args, defaults) = self.args()
78                                 
79             # Check that the right number of arguments were passed in
80             if len(args) < len(min_args) or len(args) > len(max_args):
81                 raise PLCInvalidArgumentCount(len(args), len(min_args), len(max_args))
82
83             for name, value, expected in zip(max_args, args, self.accepts):
84                 self.type_check(name, value, expected)
85         
86             # The first argument to all methods that require
87             # authentication, should be an Auth structure. The rest of the
88             # arguments to the call may also be used in the authentication
89             # check. For example, calls made by the Boot Manager are
90             # verified by comparing a hash of the message parameters to
91             # the value in the authentication structure.        
92
93             if len(self.accepts):
94                 auth = None
95                 if isinstance(self.accepts[0], Auth):
96                     auth = self.accepts[0]
97                 elif isinstance(self.accepts[0], Mixed):
98                     for auth in self.accepts[0]:
99                         if isinstance(auth, Auth):
100                             break
101                 if isinstance(auth, Auth):
102                     auth.check(self, *args)
103     
104             result = self.call(*args)
105
106             if self.api.config.PLC_API_DEBUG:
107                 self.log(0, *args)
108                 
109             return result
110
111         except PLCFault, fault:
112             # Prepend method name to expected faults
113             fault.faultString = self.name + ": " + fault.faultString
114             self.log(fault.faultCode, *args)
115             raise fault
116
117
118     def log(self, fault_code, *args):
119         """
120         Log the transaction 
121         """     
122         # Gather necessary logging variables
123         event_type = 'Unknown'
124         object_type = 'Unknown'
125         person_id = 0
126         object_ids = []
127         call_name = self.name
128         call_args = ", ".join([unicode(arg) for arg in list(args)[1:]]).replace('\'', '\\\'')
129         call = "%s(%s)" % (call_name, call_args)
130                 
131         if hasattr(self, 'event_type'):
132                 event_type = self.event_type
133         if hasattr(self, 'object_type'):
134                 object_type = self.object_type
135         if self.caller:
136                 person_id = self.caller['person_id']
137         if hasattr(self, 'object_ids'):
138                 object_ids = self.object_ids 
139
140         # make sure this is an api call
141         if call_name in ['system.listMethods', 'system.methodHelp', 'system.multicall', 'system.methodSignature']:
142                 return False
143
144         # get next event_id
145         # XX get real event id
146              
147         rows = self.api.db.selectall("SELECT nextval('events_event_id_seq')", hashref = False)
148         event_id =  rows[0][0]
149         
150         sql_event = "INSERT INTO events " \
151               " (event_id, person_id, event_type, object_type, fault_code, call) VALUES" \
152               " (%(event_id)d, %(person_id)d, '%(event_type)s', '%(object_type)s', %(fault_code)d, '%(call)s')" %  \
153               (locals())
154         self.api.db.do(sql_event)
155                         
156         # log objects affected
157         for object_id in object_ids:
158                 sql_objects = "INSERT INTO event_objects (event_id, object_id) VALUES" \
159                         " (%(event_id)d, %(object_id)d) "  % (locals()) 
160                 self.api.db.do(sql_objects)
161                         
162         self.api.db.commit()            
163         
164
165     def help(self, indent = "  "):
166         """
167         Text documentation for the method.
168         """
169
170         (min_args, max_args, defaults) = self.args()
171
172         text = "%s(%s) -> %s\n\n" % (self.name, ", ".join(max_args), xmlrpc_type(self.returns))
173
174         text += "Description:\n\n"
175         lines = [indent + line.strip() for line in self.__doc__.strip().split("\n")]
176         text += "\n".join(lines) + "\n\n"
177
178         text += "Allowed Roles:\n\n"
179         if not self.roles:
180             roles = ["any"]
181         else:
182             roles = self.roles
183         text += indent + ", ".join(roles) + "\n\n"
184
185         def param_text(name, param, indent, step):
186             """
187             Format a method parameter.
188             """
189
190             text = indent
191
192             # Print parameter name
193             if name:
194                 param_offset = 32
195                 text += name.ljust(param_offset - len(indent))
196             else:
197                 param_offset = len(indent)
198
199             # Print parameter type
200             param_type = python_type(param)
201             text += xmlrpc_type(param_type) + "\n"
202
203             # Print parameter documentation right below type
204             if isinstance(param, Parameter):
205                 wrapper = textwrap.TextWrapper(width = 70,
206                                                initial_indent = " " * param_offset,
207                                                subsequent_indent = " " * param_offset)
208                 text += "\n".join(wrapper.wrap(param.doc)) + "\n"
209                 param = param.type
210
211             text += "\n"
212
213             # Indent struct fields and mixed types
214             if isinstance(param, dict):
215                 for name, subparam in param.iteritems():
216                     text += param_text(name, subparam, indent + step, step)
217             elif isinstance(param, Mixed):
218                 for subparam in param:
219                     text += param_text(name, subparam, indent + step, step)
220             elif isinstance(param, (list, tuple)):
221                 for subparam in param:
222                     text += param_text("", subparam, indent + step, step)
223
224             return text
225
226         text += "Parameters:\n\n"
227         for name, param in zip(max_args, self.accepts):
228             text += param_text(name, param, indent, indent)
229
230         text += "Returns:\n\n"
231         text += param_text("", self.returns, indent, indent)
232
233         return text
234
235     def args(self):
236         """
237         Returns a tuple:
238
239         ((arg1_name, arg2_name, ...),
240          (arg1_name, arg2_name, ..., optional1_name, optional2_name, ...),
241          (None, None, ..., optional1_default, optional2_default, ...))
242
243         That represents the minimum and maximum sets of arguments that
244         this function accepts and the defaults for the optional arguments.
245         """
246
247         # Inspect call. Remove self from the argument list.
248         max_args = self.call.func_code.co_varnames[1:self.call.func_code.co_argcount]
249         defaults = self.call.func_defaults
250         if defaults is None:
251             defaults = ()
252
253         min_args = max_args[0:len(max_args) - len(defaults)]
254         defaults = tuple([None for arg in min_args]) + defaults
255         
256         return (min_args, max_args, defaults)
257
258     def type_check(self, name, value, expected, min = None, max = None):
259         """
260         Checks the type of the named value against the expected type,
261         which may be a Python type, a typed value, a Parameter, a
262         Mixed type, or a list or dictionary of possibly mixed types,
263         values, Parameters, or Mixed types.
264         
265         Extraneous members of lists must be of the same type as the
266         last specified type. For example, if the expected argument
267         type is [int, bool], then [1, False] and [14, True, False,
268         True] are valid, but [1], [False, 1] and [14, True, 1] are
269         not.
270
271         Extraneous members of dictionaries are ignored.
272         """
273
274         # If any of a number of types is acceptable
275         if isinstance(expected, Mixed):
276             for item in expected:
277                 try:
278                     self.type_check(name, value, item)
279                     expected = item
280                     break
281                 except PLCInvalidArgument, fault:
282                     pass
283             if expected != item:
284                 xmlrpc_types = [xmlrpc_type(item) for item in expected]
285                 raise PLCInvalidArgument("expected %s, got %s" % \
286                                          (" or ".join(xmlrpc_types),
287                                           xmlrpc_type(type(value))),
288                                          name)
289
290         # Get actual expected type from within the Parameter structure
291         elif isinstance(expected, Parameter):
292             min = expected.min
293             max = expected.max
294             expected = expected.type
295
296         expected_type = python_type(expected)
297
298         # Strings are a special case. Accept either unicode or str
299         # types if a string is expected.
300         if expected_type in StringTypes and isinstance(value, StringTypes):
301             pass
302
303         # Integers and long integers are also special types. Accept
304         # either int or long types if an int or long is expected.
305         elif expected_type in (IntType, LongType) and isinstance(value, (IntType, LongType)):
306             pass
307
308         elif not isinstance(value, expected_type):
309             raise PLCInvalidArgument("expected %s, got %s" % \
310                                      (xmlrpc_type(expected_type),
311                                       xmlrpc_type(type(value))),
312                                      name)
313
314         # If a minimum or maximum (length, value) has been specified
315         if expected_type in StringTypes:
316             if min is not None and \
317                len(value.encode(self.api.encoding)) < min:
318                 raise PLCInvalidArgument, "%s must be at least %d bytes long" % (name, min)
319             if max is not None and \
320                len(value.encode(self.api.encoding)) > max:
321                 raise PLCInvalidArgument, "%s must be at most %d bytes long" % (name, max)
322         else:
323             if min is not None and value < min:
324                 raise PLCInvalidArgument, "%s must be > %s" % (name, str(min))
325             if max is not None and value > max:
326                 raise PLCInvalidArgument, "%s must be < %s" % (name, str(max))
327
328         # If a list with particular types of items is expected
329         if isinstance(expected, (list, tuple)):
330             for i in range(len(value)):
331                 if i >= len(expected):
332                     i = len(expected) - 1
333                 self.type_check(name + "[]", value[i], expected[i])
334
335         # If a struct with particular (or required) types of items is
336         # expected.
337         elif isinstance(expected, dict):
338             for key in value.keys():
339                 if key in expected:
340                     self.type_check(name + "['%s']" % key, value[key], expected[key])
341             for key, subparam in expected.iteritems():
342                 if isinstance(subparam, Parameter) and \
343                    not subparam.optional and key not in value.keys():
344                     raise PLCInvalidArgument("'%s' not specified" % key, name)
345
346 def python_type(arg):
347     """
348     Returns the Python type of the specified argument, which may be a
349     Python type, a typed value, or a Parameter.
350     """
351
352     if isinstance(arg, Parameter):
353         arg = arg.type
354
355     if isinstance(arg, type):
356         return arg
357     else:
358         return type(arg)
359
360 def xmlrpc_type(arg):
361     """
362     Returns the XML-RPC type of the specified argument, which may be a
363     Python type, a typed value, or a Parameter.
364     """
365
366     arg_type = python_type(arg)
367
368     if arg_type == NoneType:
369         return "nil"
370     elif arg_type == IntType or arg_type == LongType:
371         return "int"
372     elif arg_type == bool:
373         return "boolean"
374     elif arg_type == FloatType:
375         return "double"
376     elif arg_type in StringTypes:
377         return "string"
378     elif arg_type == ListType or arg_type == TupleType:
379         return "array"
380     elif arg_type == DictType:
381         return "struct"
382     elif arg_type == Mixed:
383         # Not really an XML-RPC type but return "mixed" for
384         # documentation purposes.
385         return "mixed"
386     else:
387         raise PLCAPIError, "XML-RPC cannot marshal %s objects" % arg_type