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