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