source: fedd/federation/remote_service.py @ 9c73557

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

apply_to_tags miscoded for multiple applications of the map. Basic python stuff. This is from when I was learning.

  • Property mode set to 100644
File size: 18.7 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            'credential': lambda x: x.data}
68    # A map used to encapsulate fedids into xmlrpclib.Binary objects
69    encap_fedids = {'fedid': to_binary, 'credential': to_binary}
70
71    # fields that are never unicoded, because they represent non strings.
72    do_not_unicode = set(['credential'])
73
74    @staticmethod
75    def pack_soap(container, name, contents):
76        """
77        Convert the dictionary in contents into a tree of ZSI classes.
78
79        The holder classes are constructed from factories in container and
80        assigned to either the element or attribute name.  This is used to
81        recursively create the SOAP message.
82        """
83        if getattr(contents, "__iter__", None) != None:
84            attr =getattr(container, "new_%s" % name, None)
85            if attr: obj = attr()
86            else:
87                raise TypeError("%s does not have a new_%s attribute" % \
88                        (container, name))
89            for e, v in contents.iteritems():
90                assign = getattr(obj, "set_element_%s" % e, None) or \
91                        getattr(obj, "set_attribute_%s" % e, None)
92                if isinstance(v, type(dict())):
93                    assign(remote_service_base.pack_soap(obj, e, v))
94                elif getattr(v, "__iter__", None) != None:
95                    assign([ remote_service_base.pack_soap(obj, e, val ) \
96                            for val in v])
97                elif getattr(v, "pack_soap", None) != None:
98                    assign(v.pack_soap())
99                else:
100                    assign(v)
101            return obj
102        else: return contents
103
104    @staticmethod
105    def unpack_soap(element):
106        """
107        Convert a tree of ZSI SOAP classes intro a hash.  The inverse of
108        pack_soap
109
110        Elements or elements that are empty are ignored.
111        """
112        methods = [ m for m in dir(element) \
113                if m.startswith("get_element") or m.startswith("get_attribute")]
114        if len(methods) > 0:
115            rv = { }
116            for m in methods:
117                if m.startswith("get_element_"):
118                    n = m.replace("get_element_","",1)
119                else:
120                    n = m.replace("get_attribute_", "", 1)
121                sub = getattr(element, m)()
122                if sub != None:
123                    if isinstance(sub, basestring):
124                        rv[n] = sub
125                    elif getattr(sub, "__iter__", None) != None:
126                        if len(sub) > 0: rv[n] = \
127                                [remote_service_base.unpack_soap(e) \
128                                    for e in sub]
129                    else:
130                        rv[n] = remote_service_base.unpack_soap(sub)
131            return rv
132        else: 
133            return element
134
135    @staticmethod
136    def apply_to_tags(e, map):
137        """
138        Map is an iterable of ordered pairs (tuples) that map a key to a
139        function.
140        This function walks the given message and replaces any object with a
141        key in the map with the result of applying that function to the object.
142        """
143        if isinstance(e, dict):
144            for k in e.keys():
145                if k in map:
146                    fcn = map[k]
147                    if isinstance(e[k], list):
148                        e[k] = [ fcn(b) for b in e[k]]
149                    else:
150                        e[k] = fcn(e[k])
151                elif isinstance(e[k], dict):
152                    remote_service_base.apply_to_tags(e[k], map)
153                elif isinstance(e[k], list):
154                    for ee in e[k]:
155                        remote_service_base.apply_to_tags(ee, map)
156        # Other types end the recursion - they should be leaves
157        return e
158
159    @staticmethod
160    def strip_unicode(obj):
161        """Walk through a message and convert all strings to non-unicode
162        strings"""
163        if isinstance(obj, dict):
164            for k in obj.keys():
165                obj[k] = remote_service_base.strip_unicode(obj[k])
166            return obj
167        elif isinstance(obj, basestring) and not isinstance(obj, str):
168            return str(obj)
169        elif getattr(obj, "__iter__", None):
170            return [ remote_service_base.strip_unicode(x) for x in obj]
171        else:
172            return obj
173
174    @staticmethod
175    def make_unicode(obj):
176        """Walk through a message and convert all strings to unicode"""
177        if isinstance(obj, dict):
178            for k in obj.keys():
179                if k not in remote_service_base.do_not_unicode:
180                    obj[k] = remote_service_base.make_unicode(obj[k])
181            return obj
182        elif isinstance(obj, basestring) and not isinstance(obj, unicode):
183            return unicode(obj)
184        elif getattr(obj, "__iter__", None):
185            return [ remote_service_base.make_unicode(x) for x in obj]
186        else:
187            return obj
188
189
190
191class soap_handler(remote_service_base):
192    """
193    Encapsulate the handler code to unpack and pack SOAP requests and responses
194    and call the given method.
195
196    The code to decapsulate and encapsulate parameters encoded in SOAP is the
197    same modulo a few parameters.  This is a functor that calls a fedd service
198    trhough a soap interface.  The parameters are the typecode of the request
199    parameters, the method to call (usually a bound instance of a method on a
200    fedd service providing class), the constructor of a response packet and the
201    name of the body element of that packet.  The handler takes a ParsedSoap
202    object (the request) and returns an instance of the class created by
203    constructor containing the response.  Failures of the constructor or badly
204    created constructors will result in None being returned.
205    """
206    def __init__(self, service_name, method, typecode=None,
207            constructor=None, body_name=None):
208        self.method = method
209
210        response_class_name = "%sResponseMessage" % service_name
211        request_class_name = "%sRequestMessage" % service_name
212
213        if body_name: self.body_name = body_name
214        else: self.body_name = "%sResponseBody" % service_name
215
216        if constructor: self.constructor = constructor
217        else:
218            self.constructor = self.get_class(response_class_name)
219            if not self.constructor:
220                raise service_error(service_error.internal,
221                        "Cannot find class for %s" % response_class_name)
222
223        if typecode: self.typecode = typecode
224        else: 
225            req = self.get_class(request_class_name)
226            if req:
227                self.typecode = req.typecode
228            else:
229                raise service_error(service_error.internal,
230                        "Cannot find class for %s" % request_class_name)
231
232            if not self.typecode:
233                raise service_error(service_error.internal,
234                        "Cannot get typecode for %s" % class_name)
235
236    def get_class(self, class_name):
237        return getattr(fedd_services, class_name, None) or \
238                getattr(fedd_internal_services, class_name, None)
239
240    def __call__(self, ps, fid):
241        req = ps.Parse(self.typecode)
242        # Convert the message to a dict with the fedid strings converted to
243        # fedid objects
244        req = self.apply_to_tags(self.unpack_soap(req), self.fedid_to_object)
245
246        msg = self.method(req, fid)
247
248        resp = self.constructor()
249        set_element = getattr(resp, "set_element_%s" % self.body_name, None)
250        if set_element and callable(set_element):
251            try:
252                set_element(self.pack_soap(resp, self.body_name, msg))
253                return resp
254            except (NameError, TypeError):
255                return None
256        else:
257            return None
258
259class xmlrpc_handler(remote_service_base):
260    """
261    Generate the handler code to unpack and pack XMLRPC requests and responses
262    and call the given method.
263
264    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
265    service is largely the same.  This helper creates such a handler.  The
266    parameters are the method name, and the name of the body struct that
267    contains the response.  A handler is created that takes the params response
268    from an xmlrpclib.loads on the incoming rpc and a fedid and responds with
269    a hash representing the struct ro be returned to the other side.  On error
270    None is returned.  Fedid fields are decapsulated from binary and converted
271    to fedid objects on input and encapsulated as Binaries on output.
272    """
273    def __init__(self, service_name, method):
274        self.method = method
275        self.body_name = "%sResponseBody" % service_name
276
277    def __call__(self, params, fid):
278        msg = None
279
280        p = self.apply_to_tags(params[0], self.decap_fedids)
281        try:
282            msg = self.method(p, fid)
283        except service_error, e:
284            raise Fault(e.code, "%s: %s" % (e.code_string(), e.desc))
285        if msg != None:
286            return self.make_unicode(self.apply_to_tags(\
287                    { self.body_name: msg }, self.encap_fedids))
288        else:
289            return None
290
291class service_caller(remote_service_base):
292    def __init__(self, service_name, request_message=None, 
293            request_body_name=None, tracefile=None, strict=True,
294            log=None, max_retries=None):
295        self.service_name = service_name
296
297        if getattr(fedd_services.feddBindingSOAP, service_name, None):
298            self.locator = fedd_services.feddServiceLocator
299            self.port_name = service_port_name
300        elif getattr(fedd_internal_services.feddInternalBindingSOAP, 
301                service_name, None):
302            self.locator = fedd_internal_services.feddInternalServiceLocator
303            self.port_name = internal_service_port_name
304
305        if request_message: self.request_message = request_message
306        else:
307            request_message_name = "%sRequestMessage" % service_name
308            self.request_message = \
309                    getattr(fedd_services, request_message_name, None) or \
310                    getattr(fedd_internal_services, 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                    if 'proof' in ee: 
522                        pl = [ proof.from_dict(p) for p in ee['proof']]
523                    else: 
524                        pl = None
525                    raise service_error(ee.get('code', 'no code'), 
526                            ee.get('desc','no desc'), proof=pl)
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.