201de2d925f25a127c0eadc2fa64a264f8ac643c
[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         # As of now we only have aspects for OMF integration, that's
149         # why we enable aspects only if PLC_OMF is set to true.
150         if self.config.PLC_OMF_ENABLED:
151             from aspects import apply_omf_aspect
152             apply_omf_aspect()
153         
154         if self.config.PLC_RATELIMIT_ENABLED:
155             from aspects import apply_ratelimit_aspect
156             apply_ratelimit_aspect()
157
158         if getattr(self.config, "PLC_NETCONFIG_ENABLED", False):
159             from aspects.netconfigaspects import apply_netconfig_aspect
160             apply_netconfig_aspect()
161
162         # Enable Caching. Only for GetSlivers for the moment.
163         # TODO: we may consider to do this in an aspect like the ones above.
164         try:
165             if self.config.PLC_GETSLIVERS_CACHE:
166                 getslivers_cache = True
167         except AttributeError:
168             getslivers_cache = False
169
170         if getslivers_cache:
171             os.environ['DJANGO_SETTINGS_MODULE']='plc_django_settings'
172             from cache_utils.decorators import cached
173             from PLC.Methods.GetSlivers import GetSlivers
174
175             @cached(7200)
176             def cacheable_call(cls, auth, node_id_or_hostname):
177                 return cls.raw_call(auth, node_id_or_hostname)
178             
179             GetSlivers.call = cacheable_call
180             
181
182
183     def callable(self, method):
184         """
185         Return a new instance of the specified method.
186         """
187
188         # Look up method
189         if method not in self.all_methods:
190             raise PLCInvalidAPIMethod, method
191
192         # Get new instance of method
193         try:
194             classname = method.split(".")[-1]
195             if method in self.native_methods:
196                 fullpath="PLC.Methods." + method
197             else:
198                 fullpath=self.other_methods_map[method]
199             module = __import__(fullpath, globals(), locals(), [classname])
200             return getattr(module, classname)(self)
201         except ImportError, AttributeError:
202             raise PLCInvalidAPIMethod, "import error %s for %s" % (AttributeError,fullpath)
203
204     def call(self, source, method, *args):
205         """
206         Call the named method from the specified source with the
207         specified arguments.
208         """
209
210         function = self.callable(method)
211         function.source = source
212         return function(*args)
213
214     def handle(self, source, data):
215         """
216         Handle an XML-RPC or SOAP request from the specified source.
217         """
218
219         # Parse request into method name and arguments
220         try:
221             interface = xmlrpclib
222             (args, method) = xmlrpclib.loads(data)
223             methodresponse = True
224         except Exception, e:
225             if SOAPpy is not None:
226                 interface = SOAPpy
227                 (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
228                 method = r._name
229                 args = r._aslist()
230                 # XXX Support named arguments
231             else:
232                 raise e
233
234         try:
235             result = self.call(source, method, *args)
236         except PLCFault, fault:
237             # Handle expected faults
238             if interface == xmlrpclib:
239                 result = fault
240                 methodresponse = None
241             elif interface == SOAPpy:
242                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
243                 result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString))
244
245         # Return result
246         if interface == xmlrpclib:
247             if not isinstance(result, PLCFault):
248                 result = (result,)
249             data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
250         elif interface == SOAPpy:
251             data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
252
253         return data
254
255     def handle_json(self, source, data):
256         """
257         Handle a JSON request 
258         """
259         method, args = json.loads(data)
260         try:
261             result = self.call(source, method, *args)
262         except Exception, e:
263             result = str(e)
264        
265         return json.dumps(result) 
266         
267