source: fedd/fedd/remote_service.py @ 6a0c9f4

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since 6a0c9f4 was 6a0c9f4, checked in by Ted Faber <faber@…>, 15 years ago

More namespace cleanup

  • Property mode set to 100644
File size: 13.8 KB
Line 
1#!/usr/local/bin/python
2
3import copy
4
5from socket import error as socket_error
6
7import M2Crypto.httpslib
8from M2Crypto import SSL
9from M2Crypto.m2xmlrpclib import SSL_Transport
10from M2Crypto.SSL import SSLError
11from ZSI import ParseException, FaultException, SoapWriter
12
13from service_error import *
14from xmlrpclib import ServerProxy, dumps, loads, Fault, Error, Binary
15
16from util import fedd_ssl_context
17from fedid import fedid
18import parse_detail
19
20# Turn off the matching of hostname to certificate ID
21SSL.Connection.clientPostConnectionCheck = None
22
23# Used by the remote_service_base class.
24def to_binary(o):
25    """
26    A function that converts an object into an xmlrpclib.Binary using
27    either its internal packing method, or the standard Binary constructor.
28    """
29    pack = getattr(o, 'pack_xmlrpc', None)
30    if callable(pack): return Binary(pack())
31    else: return Binary(o)
32
33# Classes that encapsulate the process of making and dealing with requests to
34# WSDL-generated and XMLRPC remote accesses. 
35
36class remote_service_base:
37    """
38    This invisible base class encapsulates the functions used to massage the
39    dictionaries used to pass parameters into and out of the RPC formats.  It's
40    mostly a container for the static methods to do that work, but defines some
41    maps sued by sub classes on apply_to_tags
42    """
43    # A map used to convert fedid fields to fedid objects (when the field is
44    # already a string)
45    fedid_to_object = ( ('fedid', lambda x: fedid(bits=x)),)
46    # A map used by apply_to_tags to convert fedids from xmlrpclib.Binary
47    # objects to fedid objects in one sweep.
48    decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
49    # A map used to encapsulate fedids into xmlrpclib.Binary objects
50    encap_fedids = (('fedid', to_binary),)
51
52    @staticmethod
53    def pack_soap(container, name, contents):
54        """
55        Convert the dictionary in contents into a tree of ZSI classes.
56
57        The holder classes are constructed from factories in container and
58        assigned to either the element or attribute name.  This is used to
59        recursively create the SOAP message.
60        """
61        if getattr(contents, "__iter__", None) != None:
62            attr =getattr(container, "new_%s" % name, None)
63            if attr: obj = attr()
64            else:
65                raise TypeError("%s does not have a new_%s attribute" % \
66                        (container, name))
67            for e, v in contents.iteritems():
68                assign = getattr(obj, "set_element_%s" % e, None) or \
69                        getattr(obj, "set_attribute_%s" % e, None)
70                if isinstance(v, type(dict())):
71                    assign(remote_service_base.pack_soap(obj, e, v))
72                elif getattr(v, "__iter__", None) != None:
73                    assign([ remote_service_base.pack_soap(obj, e, val ) \
74                            for val in v])
75                elif getattr(v, "pack_soap", None) != None:
76                    assign(v.pack_soap())
77                else:
78                    assign(v)
79            return obj
80        else: return contents
81
82    @staticmethod
83    def unpack_soap(element):
84        """
85        Convert a tree of ZSI SOAP classes intro a hash.  The inverse of
86        pack_soap
87
88        Elements or elements that are empty are ignored.
89        """
90        methods = [ m for m in dir(element) \
91                if m.startswith("get_element") or m.startswith("get_attribute")]
92        if len(methods) > 0:
93            rv = { }
94            for m in methods:
95                if m.startswith("get_element_"):
96                    n = m.replace("get_element_","",1)
97                else:
98                    n = m.replace("get_attribute_", "", 1)
99                sub = getattr(element, m)()
100                if sub != None:
101                    if isinstance(sub, basestring):
102                        rv[n] = sub
103                    elif getattr(sub, "__iter__", None) != None:
104                        if len(sub) > 0: rv[n] = \
105                                [remote_service_base.unpack_soap(e) \
106                                    for e in sub]
107                    else:
108                        rv[n] = remote_service_base.unpack_soap(sub)
109            return rv
110        else: 
111            return element
112
113    @staticmethod
114    def apply_to_tags(e, map):
115        """
116        Map is an iterable of ordered pairs (tuples) that map a key to a
117        function.
118        This function walks the given message and replaces any object with a
119        key in the map with the result of applying that function to the object.
120        """
121        if isinstance(e, dict):
122            for k in e.keys():
123                for tag, fcn in map:
124                    if k == tag:
125                        if isinstance(e[k], list):
126                            e[k] = [ fcn(b) for b in e[k]]
127                        else:
128                            e[k] = fcn(e[k])
129                    elif isinstance(e[k], dict):
130                        remote_service_base.apply_to_tags(e[k], map)
131                    elif isinstance(e[k], list):
132                        for ee in e[k]:
133                            remote_service_base.apply_to_tags(ee, map)
134        # Other types end the recursion - they should be leaves
135        return e
136
137    @staticmethod
138    def strip_unicode(obj):
139        """Walk through a message and convert all strings to non-unicode
140        strings"""
141        if isinstance(obj, dict):
142            for k in obj.keys():
143                obj[k] = remote_service_base.strip_unicode(obj[k])
144            return obj
145        elif isinstance(obj, basestring) and not isinstance(obj, str):
146            return str(obj)
147        elif getattr(obj, "__iter__", None):
148            return [ remote_service_base.strip_unicode(x) for x in obj]
149        else:
150            return obj
151
152    @staticmethod
153    def make_unicode(obj):
154        """Walk through a message and convert all strings to unicode"""
155        if isinstance(obj, dict):
156            for k in obj.keys():
157                obj[k] = remote_service_base.make_unicode(obj[k])
158            return obj
159        elif isinstance(obj, basestring) and not isinstance(obj, unicode):
160            return unicode(obj)
161        elif getattr(obj, "__iter__", None):
162            return [ remote_service_base.make_unicode(x) for x in obj]
163        else:
164            return obj
165
166
167
168class soap_handler(remote_service_base):
169    """
170    Encapsulate the handler code to unpack and pack SOAP requests and responses
171    and call the given method.
172
173    The code to decapsulate and encapsulate parameters encoded in SOAP is the
174    same modulo a few parameters.  This is a functor that calls a fedd service
175    trhough a soap interface.  The parameters are the typecode of the request
176    parameters, the method to call (usually a bound instance of a method on a
177    fedd service providing class), the constructor of a response packet and the
178    name of the body element of that packet.  The handler takes a ParsedSoap
179    object (the request) and returns an instance of the class created by
180    constructor containing the response.  Failures of the constructor or badly
181    created constructors will result in None being returned.
182    """
183    def __init__(self, typecode, method, constructor, body_name):
184        self.typecode = typecode
185        self.method = method
186        self.constructor = constructor
187        self.body_name = body_name
188
189    def __call__(self, ps, fid):
190        req = ps.Parse(self.typecode)
191        # Convert the message to a dict with the fedid strings converted to
192        # fedid objects
193        req = self.apply_to_tags(self.unpack_soap(req), self.fedid_to_object)
194
195        msg = self.method(req, fid)
196
197        resp = self.constructor()
198        set_element = getattr(resp, "set_element_%s" % self.body_name, None)
199        if set_element and callable(set_element):
200            try:
201                set_element(self.pack_soap(resp, self.body_name, msg))
202                return resp
203            except (NameError, TypeError):
204                return None
205        else:
206            return None
207
208class xmlrpc_handler(remote_service_base):
209    """
210    Generate the handler code to unpack and pack XMLRPC requests and responses
211    and call the given method.
212
213    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
214    service is largely the same.  This helper creates such a handler.  The
215    parameters are the method name, and the name of the body struct that
216    contains the response.  A handler is created that takes the params response
217    from an xmlrpclib.loads on the incoming rpc and a fedid and responds with
218    a hash representing the struct ro be returned to the other side.  On error
219    None is returned.  Fedid fields are decapsulated from binary and converted
220    to fedid objects on input and encapsulated as Binaries on output.
221    """
222    def __init__(self, method, body_name):
223        self.method = method
224        self.body_name = body_name
225
226    def __call__(self, params, fid):
227        msg = None
228
229        p = self.apply_to_tags(params[0], self.decap_fedids)
230        try:
231            msg = self.method(p, fid)
232        except service_error, e:
233            raise Fault(e.code_string(), e.desc)
234        if msg != None:
235            return self.make_unicode(self.apply_to_tags(\
236                    { self.body_name: msg }, self.encap_fedids))
237        else:
238            return None
239
240class service_caller(remote_service_base):
241    def __init__(self, service_name, port_name, locator, request_message, 
242            request_body_name, tracefile=None):
243        self.service_name = service_name
244        self.port_name = port_name
245        self.locator = locator
246        self.request_message = request_message
247        self.request_body_name = request_body_name
248        self.tracefile = tracefile
249        self.__call__ = self.call_service
250
251    def serialize_soap(self, req):
252        """
253        Return a string containing the message that would be sent to call this
254        service with the given request.
255        """
256        msg = self.request_message()
257        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
258                None)
259        if not set_element:
260            raise service_error(service_error.internal,
261                    "Cannot get element setting method for %s" % \
262                            self.request_body_name)
263        set_element(self.pack_soap(msg, self.request_body_name, req))
264        sw = SoapWriter()
265        sw.serialize(msg)
266        return unicode(sw)
267
268    def call_xmlrpc_service(self, url, req, cert_file=None, cert_pwd=None, 
269            trusted_certs=None, context=None, tracefile=None):
270        """Send an XMLRPC request.  """
271
272
273        # If a context is given, use it.  Otherwise construct one from
274        # components.  The construction shouldn't call out for passwords.
275        if context:
276            ctx = context
277        else:
278            try:
279                ctx = fedd_ssl_context(cert_file, trusted_certs, 
280                        password=cert_pwd)
281            except SSL.SSLError:
282                raise service_error(service_error.server_config,
283                        "Certificates misconfigured")
284
285        # Of all the dumbass things.  The XMLRPC library in use here won't
286        # properly encode unicode strings, so we make a copy of req with
287        # the unicode objects converted.  We also convert the url to a
288        # basic string if it isn't one already.
289        r = self.strip_unicode(copy.deepcopy(req))
290        url = str(url)
291       
292        transport = SSL_Transport(ctx)
293        port = ServerProxy(url, transport=transport)
294        # Make the call, and convert faults back to service_errors
295        try:
296            remote_method = getattr(port, self.service_name, None)
297            resp = remote_method(self.apply_to_tags(\
298                    { self.request_body_name: r}, self.encap_fedids))
299        except socket_error, e:
300            raise service_error(service_error.protocol, 
301                    "Cannot connect to %s: %s" % (url, e[1]))
302        except SSLError, e:
303            raise service_error(service_error.protocol,
304                    "SSL error contacting %s: %s" % (url, e.message))
305        except Fault, f:
306            raise service_error(None, f.faultString, f.faultCode)
307        except Error, e:
308            raise service_error(service_error.protocol, 
309                    "Remote XMLRPC Fault: %s" % e)
310
311        return self.apply_to_tags(resp, self.decap_fedids) 
312
313    def call_soap_service(self, url, req, cert_file=None, cert_pwd=None,
314            trusted_certs=None, context=None, tracefile=None):
315        """
316        Send req on to the real destination in dt and return the response
317
318        Req is just the requestType object.  This function re-wraps it.  It
319        also rethrows any faults.
320        """
321
322        tf = tracefile or self.tracefile or None
323
324        # If a context is given, use it.  Otherwise construct one from
325        # components.  The construction shouldn't call out for passwords.
326        if context:
327            ctx = context
328        else:
329            try:
330                ctx = fedd_ssl_context(cert_file, trusted_certs, 
331                        password=cert_pwd)
332            except SSL.SSLError:
333                raise service_error(service_error.server_config,
334                        "Certificates misconfigured")
335        loc = self.locator()
336        get_port = getattr(loc, self.port_name, None)
337        if not get_port:
338            raise service_error(service_error.internal, 
339                    "Cannot get port %s from locator" % self.port_name)
340        port = get_port(url,
341                transport=M2Crypto.httpslib.HTTPSConnection, 
342                transdict={ 'ssl_context' : ctx },
343                tracefile=tf)
344        remote_method = getattr(port, self.service_name, None)
345        if not remote_method:
346            raise service_error(service_error.internal,
347                    "Cannot get service from SOAP port")
348
349        # Reconstruct the full request message
350        msg = self.request_message()
351        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
352                None)
353        if not set_element:
354            raise service_error(service_error.internal,
355                    "Cannot get element setting method for %s" % \
356                            self.request_body_name)
357        set_element(self.pack_soap(msg, self.request_body_name, req))
358        try:
359            resp = remote_method(msg)
360        except socket_error, e:
361            raise service_error(service_error.protocol, 
362                    "Cannot connect to %s: %s" % (url, e[1]))
363        except SSLError, e:
364            raise service_error(service_error.protocol,
365                    "SSL error contacting %s: %s" % (url, e.message))
366        except ParseException, e:
367            raise service_error(service_error.protocol,
368                    "Bad format message (XMLRPC??): %s" % e)
369        except FaultException, e:
370            ee = self.unpack_soap(e.fault.detail[0]).get('FeddFaultBody', { })
371            if ee:
372                raise service_error(ee['code'], ee['desc'])
373            else:
374                raise service_error(service_error.internal,
375                        "Unexpected fault body")
376        # Unpack and convert fedids to objects
377        r = self.apply_to_tags(self.unpack_soap(resp), self.fedid_to_object)
378        #  Make sure all strings are unicode
379        r = self.make_unicode(r)
380        return r
381
382    def call_service(self, url, req, cert_file=None, cert_pwd=None, 
383        trusted_certs=None, context=None, tracefile=None):
384        p_fault = None  # Any SOAP failure (sent unless XMLRPC works)
385        resp = None
386        try:
387            # Try the SOAP request
388            resp = self.call_soap_service(url, req, 
389                    cert_file, cert_pwd, trusted_certs, context, tracefile)
390            return resp
391        except service_error, e:
392            if e.code == service_error.protocol: p_fault = None
393            else: raise
394        except FaultException, f:
395            p_fault = f.fault.detail[0]
396               
397
398        # If we could not get a valid SOAP response to the request above,
399        # try the same address using XMLRPC and let any faults flow back
400        # out.
401        if p_fault == None:
402            resp = self.call_xmlrpc_service(url, req, cert_file,
403                    cert_pwd, trusted_certs, context, tracefile)
404            return resp
405        else:
406            # Build the fault
407            ee = unpack_soap(p_fault).get('FeddFaultBody', { })
408            if ee:
409                raise service_error(ee['code'], ee['desc'])
410            else:
411                raise service_error(service_error.internal,
412                        "Unexpected fault body")
Note: See TracBrowser for help on using the repository browser.