source: fedd/federation/remote_service.py @ c179764

axis_examplecompt_changesinfo-ops
Last change on this file since c179764 was c179764, checked in by Ted Faber <faber@…>, 13 years ago

Deal with ZSI 2.1

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