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