whitespaces and pep8 and similar lifting
[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 from __future__ import print_function
11
12 import os
13 import string
14 import json
15
16 import xmlrpclib
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 = range(0x0, 0x8) + [0xB, 0xC] + 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     string.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: u"?" for invalid in invalid_codepoints}
35
36
37 def xmlrpclib_escape(s, replace=string.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     inputs = [
58         # full ASCII
59         "".join((chr(x) for x in range(128))),
60         # likewise but as a unicode string up to 256
61         u"".join((unichr(x) for x in range(256))),
62         ]
63     for input in inputs:
64         print("==================== xmlrpclib_escape INPUT")
65         print(type(input), '->', input)
66         print("==================== xmlrpclib_escape OUTPUT")
67         print(xmlrpclib_escape(input))
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, unicode)):
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.iteritems():
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 xmlrpclib.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.Types import faultType
108     from SOAPpy.NS import NS
109     from SOAPpy.SOAPBuilder import buildSOAP
110 except ImportError:
111     SOAPpy = None
112
113 from PLC.Config import Config
114 from PLC.Faults import *
115 import PLC.Methods
116 import PLC.Accessors
117
118
119 def import_deep(name):
120     mod = __import__(name)
121     components = name.split('.')
122     for comp in components[1:]:
123         mod = getattr(mod, comp)
124     return mod
125
126
127 class PLCAPI:
128
129     # flat list of method names
130     native_methods = PLC.Methods.native_methods
131
132     # other_methods_map : dict {methodname: fullpath}
133     # e.g. 'Accessors' -> 'PLC.Accessors.Accessors'
134     other_methods_map = {}
135     for subdir in ['Accessors']:
136         path = "PLC."+subdir
137         # scan e.g. PLC.Accessors.__all__
138         pkg = __import__(path).__dict__[subdir]
139         for modulename in getattr(pkg, "__all__"):
140             fullpath = path + "." + modulename
141             for method in getattr(import_deep(fullpath), "methods"):
142                 other_methods_map[method] = fullpath
143
144     all_methods = native_methods + other_methods_map.keys()
145
146     def __init__(self, config="/etc/planetlab/plc_config",
147                  encoding="utf-8"):
148         self.encoding = encoding
149
150         # Better just be documenting the API
151         if config is None:
152             return
153
154         # Load configuration
155         self.config = Config(config)
156 #        print("config has keys {}"
157 #              .format(vars(self.config).keys()))
158
159         # Initialize database connection
160         if self.config.PLC_DB_TYPE == "postgresql":
161             from PLC.PostgreSQL import PostgreSQL
162             self.db = PostgreSQL(self)
163         else:
164             raise PLCAPIError("Unsupported database type "
165                               + self.config.PLC_DB_TYPE)
166
167         # Aspects modify the API by injecting code before, after or
168         # around method calls.
169         # http://github.com/baris/pyaspects/blob/master/README
170         #
171         if self.config.PLC_RATELIMIT_ENABLED:
172             from aspects import apply_ratelimit_aspect
173             apply_ratelimit_aspect()
174
175         if getattr(self.config, "PLC_NETCONFIG_ENABLED", False):
176             from aspects.netconfigaspects import apply_netconfig_aspect
177             apply_netconfig_aspect()
178
179         # Enable Caching. Only for GetSlivers for the moment.
180         # TODO: we may consider to do this in an aspect like the ones above.
181         try:
182             if self.config.PLC_GETSLIVERS_CACHE:
183                 getslivers_cache = True
184         except AttributeError:
185             getslivers_cache = False
186
187         if getslivers_cache:
188             os.environ['DJANGO_SETTINGS_MODULE'] = 'plc_django_settings'
189             from cache_utils.decorators import cached
190             from PLC.Methods.GetSlivers import GetSlivers
191
192             @cached(7200)
193             def cacheable_call(cls, auth, node_id_or_hostname):
194                 return cls.raw_call(auth, node_id_or_hostname)
195
196             GetSlivers.call = cacheable_call
197
198     def callable(self, method):
199         """
200         Return a new instance of the specified method.
201         """
202         # Look up method
203         if method not in self.all_methods:
204             raise PLCInvalidAPIMethod(method)
205
206         # Get new instance of method
207         try:
208             classname = method.split(".")[-1]
209             if method in self.native_methods:
210                 fullpath = "PLC.Methods." + method
211             else:
212                 fullpath = self.other_methods_map[method]
213             module = __import__(fullpath, globals(), locals(), [classname])
214             return getattr(module, classname)(self)
215         except (ImportError, AttributeError):
216             raise PLCInvalidAPIMethod("import error %s for %s"
217                                       % (AttributeError, fullpath))
218
219     def call(self, source, method, *args):
220         """
221         Call the named method from the specified source with the
222         specified arguments.
223         """
224
225         function = self.callable(method)
226         function.source = source
227         return function(*args)
228
229     def handle(self, source, data):
230         """
231         Handle an XML-RPC or SOAP request from the specified source.
232         """
233
234         # Parse request into method name and arguments
235         try:
236             interface = xmlrpclib
237             (args, method) = xmlrpclib.loads(data)
238             methodresponse = True
239         except Exception as exc:
240             if SOAPpy is not None:
241                 interface = SOAPpy
242                 (r, header, body, attrs) = \
243                     parseSOAPRPC(data, header=1, body=1, attrs=1)
244                 method = r._name
245                 args = r._aslist()
246                 # XXX Support named arguments
247             else:
248                 raise exc
249
250         try:
251             result = self.call(source, method, *args)
252         except PLCFault as fault:
253             # Handle expected faults
254             if interface == xmlrpclib:
255                 result = fault
256                 methodresponse = None
257             elif interface == SOAPpy:
258                 result = faultParameter(NS.ENV_T + ":Server",
259                                         "Method Failed", method)
260                 result._setDetail("Fault %d: %s"
261                                   % (fault.faultCode, fault.faultString))
262
263         # Return result
264         if interface == xmlrpclib:
265             if not isinstance(result, PLCFault):
266                 result = (result,)
267             data = xmlrpclib.dumps(result, methodresponse=True,
268                                    encoding=self.encoding, allow_none=1)
269         elif interface == SOAPpy:
270             data = buildSOAP(
271                 kw={'%sResponse' % method: {'Result': result}},
272                 encoding=self.encoding)
273
274         return data
275
276     def handle_json(self, source, data):
277         """
278         Handle a JSON request
279         """
280         method, args = json.loads(data)
281         try:
282             result = self.call(source, method, *args)
283         except Exception as exc:
284             result = str(exc)
285
286         return json.dumps(result)
287
288 # one simple unit test
289 if __name__ == '__main__':
290     test_xmlrpclib_escape()