source: fedd/federation/remote_service.py @ 9556f2a

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

Deal with ZSI 2.1 by name aliases. The 2.1 function seems the same, so these simply alias the new names to the old if the old are unavailable.

  • Property mode set to 100644
File size: 18.5 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            try:
303                self.request_message = \
304                        getattr(fedd_services, request_message_name, None) or \
305                        getattr(fedd_internal_services, request_message_name,
306                                None)
307            except NameError:
308                self.request_message = \
309                        getattr(fedd_client, request_message_name, None) or \
310                        getattr(fedd_internal_client, request_message_name, 
311                                None)
312            if not self.request_message and strict:
313                raise service_error(service_error.internal,
314                        "Cannot find class for %s" % request_message_name)
315
316        if request_body_name is not None:
317            self.request_body_name = request_body_name
318        else: 
319            self.request_body_name = "%sRequestBody" % service_name
320
321        self.tracefile = tracefile
322        self.__call__ = self.call_service
323        if max_retries is not None: self.max_retries = max_retries
324        else: self.max_retries = 5
325        self.log = log
326
327    def serialize_soap(self, req):
328        """
329        Return a string containing the message that would be sent to call this
330        service with the given request.
331        """
332        msg = self.request_message()
333        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
334                None)
335        if not set_element:
336            raise service_error(service_error.internal,
337                    "Cannot get element setting method for %s" % \
338                            self.request_body_name)
339        set_element(self.pack_soap(msg, self.request_body_name, req))
340        sw = SoapWriter()
341        sw.serialize(msg)
342        return unicode(sw)
343
344    def call_xmlrpc_service(self, url, req, cert_file=None, cert_pwd=None, 
345            trusted_certs=None, context=None, tracefile=None):
346        """Send an XMLRPC request.  """
347
348
349        # If a context is given, use it.  Otherwise construct one from
350        # components.  The construction shouldn't call out for passwords.
351        if context:
352            ctx = context
353        else:
354            try:
355                ctx = fedd_ssl_context(cert_file, trusted_certs, 
356                        password=cert_pwd)
357            except SSL.SSLError, e:
358                raise service_error(service_error.server_config,
359                        "Certificates misconfigured: %s" % e)
360
361        # Of all the dumbass things.  The XMLRPC library in use here won't
362        # properly encode unicode strings, so we make a copy of req with
363        # the unicode objects converted.  We also convert the url to a
364        # basic string if it isn't one already.
365        r = self.strip_unicode(copy.deepcopy(req))
366        if self.request_body_name:
367            r  = self.apply_to_tags(\
368                    { self.request_body_name: r}, self.encap_fedids)
369        else:
370            r = self.apply_to_tags(r, self.encap_fedids)
371
372        url = str(url)
373        ok = False
374        retries = 0
375
376        while not ok and retries < self.max_retries:
377            try:
378                transport = SSL_Transport(ctx)
379                port = ServerProxy(url, transport=transport)
380                remote_method = getattr(port, self.service_name, None)
381                resp = remote_method(r)
382                ok = True
383            except socket_error, e:
384                raise service_error(service_error.connect, 
385                        "Cannot connect to %s: %s" % (url, e[1]))
386            except BIOError, e:
387                if self.log:
388                    self.log.warn("BIO error contacting %s: %s" % (url, e))
389                retries += 1
390            except sslerror, e:
391                if self.log:
392                    self.log.warn("SSL (socket) error contacting %s: %s" % 
393                            (url, e))
394                retries += 1
395            except SSLError, e:
396                if self.log:
397                    self.log.warn("SSL error contacting %s: %s" % (url, e))
398                retries += 1
399            except httplib.HTTPException, e:
400                if self.log:
401                    self.log.warn("HTTP error contacting %s: %s" % (url, e))
402                retries +=1
403            except Fault, f:
404                raise service_error(f.faultCode, f.faultString)
405            except Error, e:
406                raise service_error(service_error.protocol, 
407                        "Remote XMLRPC Fault: %s" % e)
408
409        if retries >= self.max_retries :
410            raise service_error(service_error.connect, "Too many SSL failures")
411
412        return self.apply_to_tags(resp, self.decap_fedids) 
413
414    def call_soap_service(self, url, req, cert_file=None, cert_pwd=None,
415            trusted_certs=None, context=None, tracefile=None):
416        """
417        Send req on to the real destination in dt and return the response
418
419        Req is just the requestType object.  This function re-wraps it.  It
420        also rethrows any faults.
421        """
422
423        tf = tracefile or self.tracefile or None
424
425        if not self.request_body_name:
426            raise service_error(service_error.internal, 
427                    "Call to soap service without a configured request body");
428
429        ok = False
430        retries = 0
431        while not ok and retries < self.max_retries:
432            try:
433                # Reconstruct the full request message
434                msg = self.request_message()
435                set_element = getattr(msg, "set_element_%s" % \
436                        self.request_body_name,
437                        None)
438                if not set_element:
439                    raise service_error(service_error.internal,
440                            "Cannot get element setting method for %s" % \
441                                    self.request_body_name)
442                set_element(self.pack_soap(msg, self.request_body_name, req))
443                # If a context is given, use it.  Otherwise construct one from
444                # components.  The construction shouldn't call out for
445                # passwords.
446                if context:
447                    if self.log:
448                        self.log.debug("Context passed in to call_soap")
449                    ctx = context
450                else:
451                    if self.log:
452                        self.log.debug(
453                                "Constructing context in call_soap: %s" % \
454                                        cert_file)
455                    try:
456                        ctx = fedd_ssl_context(cert_file, trusted_certs, 
457                                password=cert_pwd)
458                    except SSL.SSLError, e:
459                        if self.log:
460                            self.log.debug("Certificate error: %s" % e)
461                        raise service_error(service_error.server_config,
462                                "Certificates misconfigured: %s" % e)
463                loc = self.locator()
464                get_port = getattr(loc, self.port_name, None)
465                if not get_port:
466                    raise service_error(service_error.internal, 
467                            "Cannot get port %s from locator" % self.port_name)
468                port = get_port(url,
469                        transport=M2Crypto.httpslib.HTTPSConnection, 
470                        transdict={ 'ssl_context' : ctx },
471                        tracefile=tf)
472                remote_method = getattr(port, self.service_name, None)
473                if not remote_method:
474                    raise service_error(service_error.internal,
475                            "Cannot get service from SOAP port")
476
477                fail_exc = None
478                if self.log:
479                    self.log.debug("Calling %s (retry %d)" % \
480                            (self.service_name, retries))
481                resp = remote_method(msg)
482                ok = True
483            except socket_error, e:
484                raise service_error(service_error.connect, 
485                        "Cannot connect to %s: %s" % (url, e[1]))
486            except BIOError, e:
487                if self.log:
488                    self.log.warn("BIO error contacting %s: %s" % (url, e))
489                fail_exc = e
490                retries += 1
491            except sslerror, e:
492                if self.log:
493                    self.log.warn("SSL (socket) error contacting %s: %s" % 
494                            (url, e))
495                retries += 1
496            except SSLError, e:
497                if self.log:
498                    self.log.warn("SSL error contacting %s: %s" % (url, e))
499                fail_exc = e
500                retries += 1
501            except httplib.HTTPException, e:
502                if self.log:
503                    self.log.warn("HTTP error contacting %s: %s" % (url, e))
504                fail_exc = e
505                retries +=1
506            except ParseException, e:
507                raise service_error(service_error.protocol,
508                        "Bad format message (XMLRPC??): %s" % e)
509            except FaultException, e:
510                # If the method isn't implemented we get a FaultException
511                # without a detail (which would be a FeddFault).  If that's the
512                # case construct a service_error out of the SOAP fields of the
513                # fault, if they're present.
514                if e.fault.detail:
515                    det = e.fault.detail[0]
516                    ee = self.unpack_soap(det).get('FeddFaultBody', { })
517                else:
518                    ee = { 'code': service_error.internal, 
519                            'desc': e.fault.string or "Something Weird" }
520                if ee:
521                    raise service_error(ee.get('code', 'no code'), 
522                            ee.get('desc','no desc'))
523                else:
524                    raise service_error(service_error.internal,
525                            "Unexpected fault body")
526
527        if retries >= self.max_retries and fail_exc and not ok:
528            raise service_error(service_error.connect, 
529                    "Too many failures: %s" % fail_exc)
530
531        # Unpack and convert fedids to objects
532        r = self.apply_to_tags(self.unpack_soap(resp), self.fedid_to_object)
533
534        #  Make sure all strings are unicode
535        r = self.make_unicode(r)
536        return r
537
538    def call_service(self, url, req, cert_file=None, cert_pwd=None, 
539        trusted_certs=None, context=None, tracefile=None):
540        p_fault = None  # Any SOAP failure (sent unless XMLRPC works)
541        resp = None
542        try:
543            # Try the SOAP request
544            resp = self.call_soap_service(url, req, 
545                    cert_file, cert_pwd, trusted_certs, context, tracefile)
546            return resp
547        except service_error, e:
548            if e.code == service_error.protocol: p_fault = None
549            else: raise
550        except FaultException, f:
551            p_fault = f.fault.detail[0]
552               
553
554        # If we could not get a valid SOAP response to the request above,
555        # try the same address using XMLRPC and let any faults flow back
556        # out.
557        if p_fault == None:
558            resp = self.call_xmlrpc_service(url, req, cert_file,
559                    cert_pwd, trusted_certs, context, tracefile)
560            return resp
561        else:
562            # Build the fault
563            ee = unpack_soap(p_fault).get('FeddFaultBody', { })
564            if ee:
565                raise service_error(ee['code'], ee['desc'])
566            else:
567                raise service_error(service_error.internal,
568                        "Unexpected fault body")
Note: See TracBrowser for help on using the repository browser.