source: fedd/federation/remote_service.py @ 8445caf

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

Missed a couple try blocks that must go.

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