source: fedd/federation/remote_service.py @ 0a49bd7

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

merge from current

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