# # PLCAPI XML-RPC and SOAP interfaces # # Aaron Klingaman # Mark Huang # # Copyright (C) 2004-2006 The Trustees of Princeton University # $Id: API.py 14587 2009-07-19 13:18:50Z thierry $ # $URL: https://svn.planet-lab.org/svn/PLCAPI/trunk/PLC/API.py $ # import sys import traceback import string import xmlrpclib import logging import logging.handlers from ApiExceptionCodes import * # Wrapper around xmlrpc fault to include a traceback of the server to the # client. This is done to aid in debugging from a client perspective. class FaultWithTraceback(xmlrpclib.Fault): def __init__(self, code, faultString, exc_info): type, value, tb = exc_info exc_str = ''.join(traceback.format_exception(type, value, tb)) faultString = faultString + "\nFAULT_TRACEBACK:" + exc_str xmlrpclib.Fault.__init__(self, code, faultString) # Exception to report to the caller when some non-XMLRPC fault occurs on the # server. For example a TypeError. class UnhandledServerException(FaultWithTraceback): def __init__(self, exc_info): type, value, tb = exc_info faultString = "Unhandled exception: " + str(type) FaultWithTraceback.__init__(self, FAULT_UNHANDLEDSERVEREXCEPTION, faultString, exc_info) # See "2.2 Characters" in the XML specification: # # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] # avoiding # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF] invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F)) xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii)) def xmlrpclib_escape(s, replace = string.replace): """ xmlrpclib does not handle invalid 7-bit control characters. This function augments xmlrpclib.escape, which by default only replaces '&', '<', and '>' with entities. """ # This is the standard xmlrpclib.escape function s = replace(s, "&", "&") s = replace(s, "<", "<") s = replace(s, ">", ">",) # Replace invalid 7-bit control characters with '?' return s.translate(xml_escape_table) def xmlrpclib_dump(self, value, write): """ xmlrpclib cannot marshal instances of subclasses of built-in types. This function overrides xmlrpclib.Marshaller.__dump so that any value that is an instance of one of its acceptable types is marshalled as that type. xmlrpclib also cannot handle invalid 7-bit control characters. See above. """ # Use our escape function args = [self, value, write] if isinstance(value, (str, unicode)): args.append(xmlrpclib_escape) try: # Try for an exact match first f = self.dispatch[type(value)] except KeyError: # Try for an isinstance() match for Type, f in self.dispatch.iteritems(): if isinstance(value, Type): f(*args) return raise TypeError, "cannot marshal %s objects" % type(value) else: f(*args) # You can't hide from me! xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump # SOAP support is optional try: import SOAPpy from SOAPpy.Parser import parseSOAPRPC from SOAPpy.Types import faultType from SOAPpy.NS import NS from SOAPpy.SOAPBuilder import buildSOAP except ImportError: SOAPpy = None def import_deep(name): mod = __import__(name) components = name.split('.') for comp in components[1:]: mod = getattr(mod, comp) return mod class BaseApi: def __init__(self, encoding = "utf-8"): self.encoding = encoding self.init_logger() self.funcs = {} self.register_functions() def init_logger(self): self.logger = logging.getLogger("ApiLogger") self.logger.setLevel(logging.INFO) self.logger.addHandler(logging.handlers.RotatingFileHandler(self.get_log_name(), maxBytes=100000, backupCount=5)) def get_log_name(self): return "/tmp/apilogfile.txt" def register_functions(self): self.register_function(self.noop) def register_function(self, function, name = None): if name is None: name = function.__name__ self.funcs[name] = function def call(self, source, method, *args): """ Call the named method from the specified source with the specified arguments. """ if not method in self.funcs: raise "Unknown method: " + method return self.funcs[method](*args) def handle(self, source, data): """ Handle an XML-RPC or SOAP request from the specified source. """ # Parse request into method name and arguments try: interface = xmlrpclib (args, method) = xmlrpclib.loads(data) methodresponse = True except Exception, e: if SOAPpy is not None: interface = SOAPpy (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1) method = r._name args = r._aslist() # XXX Support named arguments else: raise e self.logger.debug("BaseApi.handle OP:" + str(method) + " from " + str(source)) try: result = self.call(source, method, *args) except xmlrpclib.Fault, fault: self.logger.warning("FAULT: " + str(fault.faultCode) + " " + str(fault.faultString)) self.logger.info(traceback.format_exc()) # Handle expected faults if interface == xmlrpclib: result = FaultWithTraceback(fault.faultCode, fault.faultString, sys.exc_info()) methodresponse = None elif interface == SOAPpy: result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method) result._setDetail("Fault %d: %s" % (fault.faultCode, fault.faultString)) self.logger.debug except: self.logger.warning("EXCEPTION: " + str(sys.exc_info()[0])) self.logger.info(traceback.format_exc()) result = UnhandledServerException(sys.exc_info()) methodresponse = None # Return result if interface == xmlrpclib: if not isinstance(result, xmlrpclib.Fault): result = (result,) data = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1) elif interface == SOAPpy: data = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding) return data def noop(self, value): return value