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