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