eeb3d6ace842cc3b7050321c5b0fa06d5549e258
[sfa.git] / sfa / util / api.py
1 #
2 # SFA XML-RPC and SOAP interfaces
3 #
4
5 import sys
6 import os
7 import traceback
8 import string
9 import xmlrpclib
10 import sfa.util.xmlrpcprotocol as xmlrpcprotocol
11 from sfa.util.sfalogging import logger
12 from sfa.trust.auth import Auth
13 from sfa.util.config import *
14 from sfa.util.faults import *
15 from sfa.util.cache import Cache
16 from sfa.trust.credential import *
17 from sfa.trust.certificate import *
18
19 # See "2.2 Characters" in the XML specification:
20 #
21 # #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD]
22 # avoiding
23 # [#x7F-#x84], [#x86-#x9F], [#xFDD0-#xFDDF]
24
25 invalid_xml_ascii = map(chr, range(0x0, 0x8) + [0xB, 0xC] + range(0xE, 0x1F))
26 xml_escape_table = string.maketrans("".join(invalid_xml_ascii), "?" * len(invalid_xml_ascii))
27
28 def xmlrpclib_escape(s, replace = string.replace):
29     """
30     xmlrpclib does not handle invalid 7-bit control characters. This
31     function augments xmlrpclib.escape, which by default only replaces
32     '&', '<', and '>' with entities.
33     """
34
35     # This is the standard xmlrpclib.escape function
36     s = replace(s, "&", "&amp;")
37     s = replace(s, "<", "&lt;")
38     s = replace(s, ">", "&gt;",)
39
40     # Replace invalid 7-bit control characters with '?'
41     return s.translate(xml_escape_table)
42
43 def xmlrpclib_dump(self, value, write):
44     """
45     xmlrpclib cannot marshal instances of subclasses of built-in
46     types. This function overrides xmlrpclib.Marshaller.__dump so that
47     any value that is an instance of one of its acceptable types is
48     marshalled as that type.
49
50     xmlrpclib also cannot handle invalid 7-bit control characters. See
51     above.
52     """
53
54     # Use our escape function
55     args = [self, value, write]
56     if isinstance(value, (str, unicode)):
57         args.append(xmlrpclib_escape)
58
59     try:
60         # Try for an exact match first
61         f = self.dispatch[type(value)]
62     except KeyError:
63         raise
64         # Try for an isinstance() match
65         for Type, f in self.dispatch.iteritems():
66             if isinstance(value, Type):
67                 f(*args)
68                 return
69         raise TypeError, "cannot marshal %s objects" % type(value)
70     else:
71         f(*args)
72
73 # You can't hide from me!
74 xmlrpclib.Marshaller._Marshaller__dump = xmlrpclib_dump
75
76 # SOAP support is optional
77 try:
78     import SOAPpy
79     from SOAPpy.Parser import parseSOAPRPC
80     from SOAPpy.Types import faultType
81     from SOAPpy.NS import NS
82     from SOAPpy.SOAPBuilder import buildSOAP
83 except ImportError:
84     SOAPpy = None
85
86
87 def import_deep(name):
88     mod = __import__(name)
89     components = name.split('.')
90     for comp in components[1:]:
91         mod = getattr(mod, comp)
92     return mod
93
94 class ManagerWrapper:
95     """
96     This class acts as a wrapper around an SFA interface manager module, but
97     can be used with any python module. The purpose of this class is raise a 
98     SfaNotImplemented exception if the a someone attepmts to use an attribute 
99     (could be a callable) thats not available in the library by checking the
100     library using hasattr. This helps to communicate better errors messages 
101     to the users and developers in the event that a specifiec operation 
102     is not implemented by a libarary and will generally be more helpful than
103     the standard AttributeError         
104     """
105     def __init__(self, manager, interface):
106         self.manager = manager
107         self.interface = interface
108         
109     def __getattr__(self, method):
110         
111         if not hasattr(self.manager, method):
112             raise SfaNotImplemented(method, self.interface)
113         return getattr(self.manager, method)
114         
115 class BaseAPI:
116
117     protocol = None
118   
119     def __init__(self, config = "/etc/sfa/sfa_config.py", encoding = "utf-8", 
120                  methods='sfa.methods', peer_cert = None, interface = None, 
121                  key_file = None, cert_file = None, cache = None):
122
123         self.encoding = encoding
124         
125         # flat list of method names
126         self.methods_module = methods_module = __import__(methods, fromlist=[methods])
127         self.methods = methods_module.all
128
129         # Better just be documenting the API
130         if config is None:
131             return
132         # Load configuration
133         self.config = Config(config)
134         self.auth = Auth(peer_cert)
135         self.hrn = self.config.SFA_INTERFACE_HRN
136         self.interface = interface
137         self.key_file = key_file
138         self.key = Keypair(filename=self.key_file)
139         self.cert_file = cert_file
140         self.cert = Certificate(filename=self.cert_file)
141         self.cache = cache
142         if self.cache is None:
143             self.cache = Cache()
144         self.credential = None
145         self.source = None 
146         self.time_format = "%Y-%m-%d %H:%M:%S"
147         self.logger = logger
148  
149         # load registries
150         from sfa.server.registry import Registries
151         self.registries = Registries() 
152
153         # load aggregates
154         from sfa.server.aggregate import Aggregates
155         self.aggregates = Aggregates()
156
157
158     def get_interface_manager(self, manager_base = 'sfa.managers'):
159         """
160         Returns the appropriate manager module for this interface.
161         Modules are usually found in sfa/managers/
162         """
163         
164         if self.interface in ['registry']:
165             mgr_type = self.config.SFA_REGISTRY_TYPE
166             manager_module = manager_base + ".registry_manager_%s" % mgr_type
167         elif self.interface in ['aggregate']:
168             mgr_type = self.config.SFA_AGGREGATE_TYPE
169             manager_module = manager_base + ".aggregate_manager_%s" % mgr_type 
170         elif self.interface in ['slicemgr', 'sm']:
171             mgr_type = self.config.SFA_SM_TYPE
172             manager_module = manager_base + ".slice_manager_%s" % mgr_type
173         elif self.interface in ['component', 'cm']:
174             mgr_type = self.config.SFA_CM_TYPE
175             manager_module = manager_base + ".component_manager_%s" % mgr_type
176         else:
177             raise SfaAPIError("No manager for interface: %s" % self.interface)  
178         manager = __import__(manager_module, fromlist=[manager_base])
179         # this isnt necessary but will help to produce better error messages
180         # if someone tries to access an operation this manager doesn't implement  
181         manager = ManagerWrapper(manager, self.interface)
182
183         return manager
184
185     def callable(self, method):
186         """
187         Return a new instance of the specified method.
188         """
189         # Look up method
190         if method not in self.methods:
191             raise SfaInvalidAPIMethod, method
192         
193         # Get new instance of method
194         try:
195             classname = method.split(".")[-1]
196             module = __import__(self.methods_module.__name__ + "." + method, globals(), locals(), [classname])
197             callablemethod = getattr(module, classname)(self)
198             return getattr(module, classname)(self)
199         except ImportError, AttributeError:
200             raise SfaInvalidAPIMethod, method
201
202     def call(self, source, method, *args):
203         """
204         Call the named method from the specified source with the
205         specified arguments.
206         """
207         function = self.callable(method)
208         function.source = source
209         self.source = source
210         return function(*args)
211
212     
213     def handle(self, source, data, method_map):
214         """
215         Handle an XML-RPC or SOAP request from the specified source.
216         """
217         # Parse request into method name and arguments
218         try:
219             interface = xmlrpclib
220             self.protocol = 'xmlrpclib'
221             (args, method) = xmlrpclib.loads(data)
222             if method_map.has_key(method):
223                 method = method_map[method]
224             methodresponse = True
225             
226         except Exception, e:
227             if SOAPpy is not None:
228                 self.protocol = 'soap'
229                 interface = SOAPpy
230                 (r, header, body, attrs) = parseSOAPRPC(data, header = 1, body = 1, attrs = 1)
231                 method = r._name
232                 args = r._aslist()
233                 # XXX Support named arguments
234             else:
235                 raise e
236
237         try:
238             result = self.call(source, method, *args)
239         except SfaFault, fault:
240             result = fault 
241         except Exception, fault:
242             logger.log_exc("BaseAPI.handle has caught Exception")
243             result = SfaAPIError(fault)
244
245
246         # Return result
247         response = self.prepare_response(result, method)
248         return response
249     
250     def prepare_response(self, result, method=""):
251         """
252         convert result to a valid xmlrpc or soap response
253         """   
254  
255         if self.protocol == 'xmlrpclib':
256             if not isinstance(result, SfaFault):
257                 result = (result,)
258             response = xmlrpclib.dumps(result, methodresponse = True, encoding = self.encoding, allow_none = 1)
259         elif self.protocol == 'soap':
260             if isinstance(result, Exception):
261                 result = faultParameter(NS.ENV_T + ":Server", "Method Failed", method)
262                 result._setDetail("Fault %d: %s" % (result.faultCode, result.faultString))
263             else:
264                 response = buildSOAP(kw = {'%sResponse' % method: {'Result': result}}, encoding = self.encoding)
265         else:
266             if isinstance(result, Exception):
267                 raise result 
268             
269         return response
270
271     def get_cached_server_version(self, server):
272         cache_key = server.url + "-version"
273         server_version = None
274         if self.cache:
275             server_version = self.cache.get(cache_key)
276         if not server_version:
277             server_version = server.GetVersion()
278             # cache version for 24 hours
279             self.cache.add(cache_key, server_version, ttl= 60*60*24)
280         return server_version