95538ad3a873cd15e25eeabd50d508354cfb306c
[sfa.git] / sfa / server / modpythonapi / BaseApi.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 # $Id: API.py 14587 2009-07-19 13:18:50Z thierry $
9 # $URL: https://svn.planet-lab.org/svn/PLCAPI/trunk/PLC/API.py $
10 #
11
12 import sys
13 import traceback
14 import string
15
16 import xmlrpclib
17 import logging
18 import logging.handlers
19
20 from ApiExceptionCodes import *
21
22 # Wrapper around xmlrpc fault to include a traceback of the server to the
23 # client. This is done to aid in debugging from a client perspective.
24
25 class FaultWithTraceback(xmlrpclib.Fault):
26     def __init__(self, code, faultString, exc_info):
27         type, value, tb = exc_info
28         exc_str = ''.join(traceback.format_exception(type, value, tb))
29         faultString = faultString + "\nFAULT_TRACEBACK:" + exc_str
30         xmlrpclib.Fault.__init__(self, code, faultString)
31
32 # Exception to report to the caller when some non-XMLRPC fault occurs on the
33 # server. For example a TypeError.
34
35 class UnhandledServerException(FaultWithTraceback):
36     def __init__(self, exc_info):
37         type, value, tb = exc_info
38         faultString = "Unhandled exception: " + str(type)
39         FaultWithTraceback.__init__(self, FAULT_UNHANDLEDSERVEREXCEPTION, faultString, exc_info)
40
41 # See "2.2 Characters" in the XML specification:
42 #
43 # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
44 # avoiding
45 # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
46
47 invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
48 xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
49
50 def xmlrpclib_escape(s, replace = string.replace):
51     """
52     xmlrpclib does not handle invalid 7-bit control characters. This
53     function augments xmlrpclib.escape, which by default only replaces
54     '&', '<', and '>' with entities.
55     """
56
57     # This is the standard xmlrpclib.escape function
58     s = replace(s, "&", "&amp;")
59     s = replace(s, "<", "&lt;")
60     s = replace(s, ">", "&gt;",)
61
62     # Replace invalid 7-bit control characters with '?'
63     return s.translate(xml_escape_table)
64
65 def xmlrpclib_dump(self, value, write):
66     """
67     xmlrpclib cannot marshal instances of subclasses of built-in
68     types. This function overrides xmlrpclib.Marshaller.__dump so that
69     any value that is an instance of one of its acceptable types is
70     marshalled as that type.
71
72     xmlrpclib also cannot handle invalid 7-bit control characters. See
73     above.
74     """
75
76     # Use our escape function
77     args = [self, value, write]
78     if isinstance(value, (str, unicode)):
79         args.append(xmlrpclib_escape)
80
81     try:
82         # Try for an exact match first
83         f = self.dispatch[type(value)]
84     except KeyError:
85         # Try for an isinstance() match
86         for Type, f in self.dispatch.iteritems():
87             if isinstance(value, Type):
88                 f(*args)
89                 return
90         raise TypeError, "cannot marshal %s objects" % type(value)
91     else:
92         f(*args)
93
94 # You can't hide from me!
95 xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
96
97 # SOAP support is optional
98 try:
99     import SOAPpy
100     from SOAPpy.Parser import parseSOAPRPC
101     from SOAPpy.Types import faultType
102     from SOAPpy.NS import NS
103     from SOAPpy.SOAPBuilder import buildSOAP
104 except ImportError:
105     SOAPpy = None
106
107 def import_deep(name):
108     mod = __import__(name)
109     components = name.split('.')
110     for comp in components[1:]:
111         mod = getattr(mod, comp)
112     return mod
113
114 class BaseApi:
115     def __init__(self, encoding = "utf-8"):
116         self.encoding = encoding
117         self.init_logger()
118         self.funcs = {}
119         self.register_functions()
120
121     def init_logger(self):
122         self.logger = logging.getLogger("ApiLogger")
123         self.logger.setLevel(logging.INFO)
124         self.logger.addHandler(logging.handlers.RotatingFileHandler(self.get_log_name(), maxBytes=100000, backupCount=5))
125
126     def get_log_name(self):
127         return "/tmp/apilogfile.txt"
128
129     def register_functions(self):
130         self.register_function(self.noop)
131
132     def register_function(self, function, name = None):
133         if name is None:
134             name = function.__name__
135         self.funcs[name] = function
136
137     def call(self, source, method, *args):
138         """
139         Call the named method from the specified source with the
140         specified arguments.
141         """
142
143         if not method in self.funcs:
144             raise "Unknown method: " + method
145
146         return self.funcs[method](*args)
147
148     def handle(self, source, data):
149         """
150         Handle an XML-RPC or SOAP request from the specified source.
151         """
152
153         # Parse request into method name and arguments
154         try:
155             interface = xmlrpclib
156             (args, method) = xmlrpclib.loads(data)
157             methodresponse = True
158         except Exception, e:
159             if SOAPpy is not None:
160                 interface = SOAPpy
161                 (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
162                 method = r._name
163                 args = r._aslist()
164                 # XXX Support named arguments
165             else:
166                 raise e
167
168         self.logger.debug("OP:" + str(method) + " from " + str(source))
169
170         try:
171             result = self.call(source, method, *args)
172         except xmlrpclib.Fault, fault:
173             self.logger.warning("FAULT: " + str(fault.faultCode) + " " + str(fault.faultString))
174             self.logger.info(traceback.format_exc())
175             # Handle expected faults
176             if interface == xmlrpclib:
177                 result = FaultWithTraceback(fault.faultCode, fault.faultString, sys.exc_info())
178                 methodresponse = None
179             elif interface == SOAPpy:
180                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
181                 result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString))
182                 self.logger.debug
183         except:
184             self.logger.warning("EXCEPTION: " + str(sys.exc_info()[0]))
185             self.logger.info(traceback.format_exc())
186             result = UnhandledServerException(sys.exc_info())
187             methodresponse = None
188
189         # Return result
190         if interface == xmlrpclib:
191             if not isinstance(result, xmlrpclib.Fault):
192                 result = (result,)
193             data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
194         elif interface == SOAPpy:
195             data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
196
197         return data
198
199     def noop(self, value):
200         return value
201
202
203