use PLC_RATELIMIT_ENABLED to enable ratelimit aspect
[plcapi.git] / PLC / API.py
1 #
2 # PLCAPI XML-RPC and SOAP interfaces
3 #
4 # Aaron Klingaman <alk@absarokasoft.com>
5 # Mark Huang <mlhuang@cs.princeton.edu>
6 #
7 # Copyright (C) 2004-2006 The Trustees of Princeton University
8 #
9
10 import sys
11 import traceback
12 import string
13
14 import xmlrpclib
15 import simplejson
16 try:
17     # Try to use jsonlib before using simpljson. This is a hack to get around
18     # the fact that the version of simplejson avaialble for f8 is slightly 
19     # faster than xmlrpc but not as fast as jsonlib. There is no jsonlib 
20     # pacakge available for f8, so this has to be installed manually and
21     # is not expected to always be available. Remove this once we move away
22     # from f8 based MyPLC's         
23     import jsonlib
24     json = jsonlib
25 except:
26     json = simplejson 
27
28 # See "2.2 Characters" in the XML specification:
29 #
30 # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
31 # avoiding
32 # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
33
34 invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
35 xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
36
37 def xmlrpclib_escape(s, replace = string.replace):
38     """
39     xmlrpclib does not handle invalid 7-bit control characters. This
40     function augments xmlrpclib.escape, which by default only replaces
41     '&', '<', and '>' with entities.
42     """
43
44     # This is the standard xmlrpclib.escape function
45     s = replace(s, "&", "&amp;")
46     s = replace(s, "<", "&lt;")
47     s = replace(s, ">", "&gt;",)
48
49     # Replace invalid 7-bit control characters with '?'
50     return s.translate(xml_escape_table)
51
52 def xmlrpclib_dump(self, value, write):
53     """
54     xmlrpclib cannot marshal instances of subclasses of built-in
55     types. This function overrides xmlrpclib.Marshaller.__dump so that
56     any value that is an instance of one of its acceptable types is
57     marshalled as that type.
58
59     xmlrpclib also cannot handle invalid 7-bit control characters. See
60     above.
61     """
62
63     # Use our escape function
64     args = [self, value, write]
65     if isinstance(value, (str, unicode)):
66         args.append(xmlrpclib_escape)
67
68     try:
69         # Try for an exact match first
70         f = self.dispatch[type(value)]
71     except KeyError:
72         # Try for an isinstance() match
73         for Type, f in self.dispatch.iteritems():
74             if isinstance(value, Type):
75                 f(*args)
76                 return
77         raise TypeError, "cannot marshal %s objects" % type(value)
78     else:
79         f(*args)
80
81 # You can't hide from me!
82 xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
83
84 # SOAP support is optional
85 try:
86     import SOAPpy
87     from SOAPpy.Parser import parseSOAPRPC
88     from SOAPpy.Types import faultType
89     from SOAPpy.NS import NS
90     from SOAPpy.SOAPBuilder import buildSOAP
91 except ImportError:
92     SOAPpy = None
93
94 from PLC.Config import Config
95 from PLC.Faults import *
96 import PLC.Methods
97 import PLC.Accessors
98
99 def import_deep(name):
100     mod = __import__(name)
101     components = name.split('.')
102     for comp in components[1:]:
103         mod = getattr(mod, comp)
104     return mod
105
106 class PLCAPI:
107
108     # flat list of method names
109     native_methods = PLC.Methods.native_methods
110
111     # other_methods_map : dict {methodname: fullpath}
112     # e.g. 'Accessors' -> 'PLC.Accessors.Accessors'
113     other_methods_map={}
114     for subdir in [ 'Accessors' ]:
115         path="PLC."+subdir
116         # scan e.g. PLC.Accessors.__all__
117         pkg = __import__(path).__dict__[subdir]
118         for modulename in getattr(pkg,"__all__"):
119             fullpath=path+"."+modulename
120             for method in getattr(import_deep(fullpath),"methods"):
121                 other_methods_map[method] = fullpath
122
123     all_methods = native_methods + other_methods_map.keys()
124
125     def __init__(self, config = "/etc/planetlab/plc_config", encoding = "utf-8"):
126         self.encoding = encoding
127
128         # Better just be documenting the API
129         if config is None:
130             return
131
132         # Load configuration
133         self.config = Config(config)
134
135         # Initialize database connection
136         if self.config.PLC_DB_TYPE == "postgresql":
137             from PLC.PostgreSQL import PostgreSQL
138             self.db = PostgreSQL(self)
139         else:
140             raise PLCAPIError, "Unsupported database type " + self.config.PLC_DB_TYPE
141
142         # Aspects modify the API by injecting code before, after or
143         # around method calls. -- http://github.com/baris/pyaspects/blob/master/README
144         # 
145         # As of now we only have aspects for OMF integration, that's
146         # why we enable aspects only if PLC_OMF is set to true.
147         if self.config.PLC_OMF_ENABLED:
148             from aspects import apply_omf_aspect
149             apply_omf_aspect()
150         
151         if self.config.PLC_RATELIMIT_ENABLED:
152             from aspects import apply_ratelimit_aspect
153             apply_ratelimit_aspect()
154
155
156     def callable(self, method):
157         """
158         Return a new instance of the specified method.
159         """
160
161         # Look up method
162         if method not in self.all_methods:
163             raise PLCInvalidAPIMethod, method
164
165         # Get new instance of method
166         try:
167             classname = method.split(".")[-1]
168             if method in self.native_methods:
169                 fullpath="PLC.Methods." + method
170             else:
171                 fullpath=self.other_methods_map[method]
172             module = __import__(fullpath, globals(), locals(), [classname])
173             return getattr(module, classname)(self)
174         except ImportError, AttributeError:
175             raise PLCInvalidAPIMethod, "import error %s for %s" % (AttributeError,fullpath)
176
177     def call(self, source, method, *args):
178         """
179         Call the named method from the specified source with the
180         specified arguments.
181         """
182
183         function = self.callable(method)
184         function.source = source
185         return function(*args)
186
187     def handle(self, source, data):
188         """
189         Handle an XML-RPC or SOAP request from the specified source.
190         """
191
192         # Parse request into method name and arguments
193         try:
194             interface = xmlrpclib
195             (args, method) = xmlrpclib.loads(data)
196             methodresponse = True
197         except Exception, e:
198             if SOAPpy is not None:
199                 interface = SOAPpy
200                 (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
201                 method = r._name
202                 args = r._aslist()
203                 # XXX Support named arguments
204             else:
205                 raise e
206
207         try:
208             result = self.call(source, method, *args)
209         except PLCFault, fault:
210             # Handle expected faults
211             if interface == xmlrpclib:
212                 result = fault
213                 methodresponse = None
214             elif interface == SOAPpy:
215                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
216                 result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString))
217
218         # Return result
219         if interface == xmlrpclib:
220             if not isinstance(result, PLCFault):
221                 result = (result,)
222             data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
223         elif interface == SOAPpy:
224             data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
225
226         return data
227
228     def handle_json(self, source, data):
229         """
230         Handle a JSON request 
231         """
232         method, args = json.loads(data)
233         try:
234             result = self.call(source, method, *args)
235         except Exception, e:
236             result = str(e)
237        
238         return json.dumps(result) 
239         
240