Fix bug
[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 import time
15 import xmlrpclib
16 import simplejson
17 try:
18     # Try to use jsonlib before using simpljson. This is a hack to get around
19     # the fact that the version of simplejson avaialble for f8 is slightly 
20     # faster than xmlrpc but not as fast as jsonlib. There is no jsonlib 
21     # pacakge available for f8, so this has to be installed manually and
22     # is not expected to always be available. Remove this once we move away
23     # from f8 based MyPLC's         
24     import jsonlib
25     json = jsonlib
26 except:
27     json = simplejson 
28
29 # See "2.2 Characters" in the XML specification:
30 #
31 # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
32 # avoiding
33 # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
34
35 invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
36 xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
37
38 def xmlrpclib_escape(s, replace = string.replace):
39     """
40     xmlrpclib does not handle invalid 7-bit control characters. This
41     function augments xmlrpclib.escape, which by default only replaces
42     '&', '<', and '>' with entities.
43     """
44
45     # This is the standard xmlrpclib.escape function
46     s = replace(s, "&", "&amp;")
47     s = replace(s, "<", "&lt;")
48     s = replace(s, ">", "&gt;",)
49
50     # Replace invalid 7-bit control characters with '?'
51     return s.translate(xml_escape_table)
52
53 def xmlrpclib_dump(self, value, write):
54     """
55     xmlrpclib cannot marshal instances of subclasses of built-in
56     types. This function overrides xmlrpclib.Marshaller.__dump so that
57     any value that is an instance of one of its acceptable types is
58     marshalled as that type.
59
60     xmlrpclib also cannot handle invalid 7-bit control characters. See
61     above.
62     """
63
64     # Use our escape function
65     args = [self, value, write]
66     if isinstance(value, (str, unicode)):
67         args.append(xmlrpclib_escape)
68
69     try:
70         # Try for an exact match first
71         f = self.dispatch[type(value)]
72     except KeyError:
73         # Try for an isinstance() match
74         for Type, f in self.dispatch.iteritems():
75             if isinstance(value, Type):
76                 f(*args)
77                 return
78         raise TypeError, "cannot marshal %s objects" % type(value)
79     else:
80         f(*args)
81
82 # You can't hide from me!
83 xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
84
85 # SOAP support is optional
86 try:
87     import SOAPpy
88     from SOAPpy.Parser import parseSOAPRPC
89     from SOAPpy.Types import faultType
90     from SOAPpy.NS import NS
91     from SOAPpy.SOAPBuilder import buildSOAP
92 except ImportError:
93     SOAPpy = None
94
95 from PLC.Config import Config
96 from PLC.Logger import logger
97 from PLC.Faults import *
98 from PLC.NovaShell import NovaShell
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/plcapi_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         # admin connection to nova api 
139         self.admin_shell = NovaShell()
140         self.client_shell = NovaShell()
141
142         # Aspects modify the API by injecting code before, after or
143         # around method calls. -- http://github.com/baris/pyaspects/blob/master/README
144         # 
145         # As of now we only have aspects for OMF integration, that's
146         # why we enable aspects only if PLC_OMF is set to true.
147         if self.config.api_omf_enabled:
148             from aspects import apply_omf_aspect
149             apply_omf_aspect()
150         
151         if self.config.api_ratelimit_enabled:
152             from aspects import apply_ratelimit_aspect
153             apply_ratelimit_aspect()
154
155     def callable(self, method):
156         """
157         Return a new instance of the specified method.
158         """
159
160         # Look up method
161         if method not in self.all_methods:
162             raise PLCInvalidAPIMethod, method
163
164         # Get new instance of method
165         try:
166             classname = method.split(".")[-1]
167             if method in self.native_methods:
168                 fullpath="PLC.Methods." + method
169             else:
170                 fullpath=self.other_methods_map[method]
171             module = __import__(fullpath, globals(), locals(), [classname])
172             return getattr(module, classname)(self)
173         except ImportError, AttributeError:
174             raise
175             #raise PLCInvalidAPIMethod, "import error %s for %s" % (AttributeError,fullpath)
176
177     def call(self, source, method, *args):
178         """
179         Call the named method from the specified source with the
180         specified arguments.
181         """
182
183         function = self.callable(method)
184         function.source = source
185         return function(*args)
186
187     def handle(self, source, data):
188         """
189         Handle an XML-RPC or SOAP request from the specified source.
190         """
191
192         # Parse request into method name and arguments
193         start = time.time() 
194         try:
195             interface = xmlrpclib
196             (args, method) = xmlrpclib.loads(data)
197             methodresponse = True
198         except Exception, e:
199             if SOAPpy is not None:
200                 interface = SOAPpy
201                 (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
202                 method = r._name
203                 args = r._aslist()
204                 # XXX Support named arguments
205             else:
206                 raise e
207
208         try:
209             result = self.call(source, method, *args)
210             end = time.time()
211             logger.info("%s %s %s" % (source, method, end-start))
212         except PLCFault, fault:
213             end = time.time()
214             logger.log_exc("%s %s %s" % (source, method, end-start))
215             # Handle expected faults
216             if interface == xmlrpclib:
217                 result = fault
218                 methodresponse = None
219             elif interface == SOAPpy:
220                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
221                 result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString))
222         except Exception, fault:
223             end = time.time()
224             logger.log_exc("%s %s %s" % (source, method, end-start))
225             if interface == xmlrpclib:
226                 result = PLCFault(500, str(fault))
227                 methodresponse = None
228             elif interface == SOAPpy:
229                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
230                 result._setDetail("Fault %d: %s" % (500, str(fault)))     
231
232         # Return result
233         if interface == xmlrpclib:
234             if not isinstance(result, PLCFault):
235                 result = (result,)
236             data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
237         elif interface == SOAPpy:
238             data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
239
240         return data
241
242     def handle_json(self, source, data):
243         """
244         Handle a JSON request 
245         """
246         method, args = json.loads(data)
247         try:
248             result = self.call(source, method, *args)
249         except Exception, e:
250             result = str(e)
251        
252         return json.dumps(result) 
253         
254