source: fedd/federation/remote_service.py @ 55779d4

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

Put a few more escapes in here to let the code be used with non-SOAP
non-autogenerated XMLRPC services.

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