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