source: fedd/remote_service.py @ 1b57352

axis_examplecompt_changesinfo-opsversion-1.30version-2.00version-3.01version-3.02
Last change on this file since 1b57352 was 1b57352, checked in by Ted Faber <faber@…>, 15 years ago

Deal with common connection and SSL errors cleanly

  • Property mode set to 100644
File size: 13.6 KB
Line 
1#!/usr/local/bin/python
2
3import copy
4
5from socket import error as socket_error
6
7import M2Crypto.httpslib
8from M2Crypto.m2xmlrpclib import SSL_Transport
9from M2Crypto.SSL import SSLError
10from ZSI import ParseException, FaultException, SoapWriter
11
12from service_error import *
13from xmlrpclib import ServerProxy, dumps, loads, Fault, Error, Binary
14
15from fedd_util import fedd_ssl_context
16from fedid import fedid
17
18
19# Used by the remote_service_base class.
20def to_binary(o):
21    """
22    A function that converts an object into an xmlrpclib.Binary using
23    either its internal packing method, or the standard Binary constructor.
24    """
25    pack = getattr(o, 'pack_xmlrpc', None)
26    if callable(pack): return Binary(pack())
27    else: return Binary(o)
28
29# Classes that encapsulate the process of making and dealing with requests to
30# WSDL-generated and XMLRPC remote accesses. 
31
32class remote_service_base:
33    """
34    This invisible base class encapsulates the functions used to massage the
35    dictionaries used to pass parameters into and out of the RPC formats.  It's
36    mostly a container for the static methods to do that work, but defines some
37    maps sued by sub classes on apply_to_tags
38    """
39    # A map used to convert fedid fields to fedid objects (when the field is
40    # already a string)
41    fedid_to_object = ( ('fedid', lambda x: fedid(bits=x)),)
42    # A map used by apply_to_tags to convert fedids from xmlrpclib.Binary
43    # objects to fedid objects in one sweep.
44    decap_fedids = (('fedid', lambda x: fedid(bits=x.data)),)
45    # A map used to encapsulate fedids into xmlrpclib.Binary objects
46    encap_fedids = (('fedid', to_binary),)
47
48    @staticmethod
49    def pack_soap(container, name, contents):
50        """
51        Convert the dictionary in contents into a tree of ZSI classes.
52
53        The holder classes are constructed from factories in container and
54        assigned to either the element or attribute name.  This is used to
55        recursively create the SOAP message.
56        """
57        if getattr(contents, "__iter__", None) != None:
58            attr =getattr(container, "new_%s" % name, None)
59            if attr: obj = attr()
60            else:
61                raise TypeError("%s does not have a new_%s attribute" % \
62                        (container, name))
63            for e, v in contents.iteritems():
64                assign = getattr(obj, "set_element_%s" % e, None) or \
65                        getattr(obj, "set_attribute_%s" % e, None)
66                if isinstance(v, type(dict())):
67                    assign(remote_service_base.pack_soap(obj, e, v))
68                elif getattr(v, "__iter__", None) != None:
69                    assign([ remote_service_base.pack_soap(obj, e, val ) \
70                            for val in v])
71                elif getattr(v, "pack_soap", None) != None:
72                    assign(v.pack_soap())
73                else:
74                    assign(v)
75            return obj
76        else: return contents
77
78    @staticmethod
79    def unpack_soap(element):
80        """
81        Convert a tree of ZSI SOAP classes intro a hash.  The inverse of
82        pack_soap
83
84        Elements or elements that are empty are ignored.
85        """
86        methods = [ m for m in dir(element) \
87                if m.startswith("get_element") or m.startswith("get_attribute")]
88        if len(methods) > 0:
89            rv = { }
90            for m in methods:
91                if m.startswith("get_element_"):
92                    n = m.replace("get_element_","",1)
93                else:
94                    n = m.replace("get_attribute_", "", 1)
95                sub = getattr(element, m)()
96                if sub != None:
97                    if isinstance(sub, basestring):
98                        rv[n] = sub
99                    elif getattr(sub, "__iter__", None) != None:
100                        if len(sub) > 0: rv[n] = \
101                                [remote_service_base.unpack_soap(e) \
102                                    for e in sub]
103                    else:
104                        rv[n] = remote_service_base.unpack_soap(sub)
105            return rv
106        else: 
107            return element
108
109    @staticmethod
110    def apply_to_tags(e, map):
111        """
112        Map is an iterable of ordered pairs (tuples) that map a key to a
113        function.
114        This function walks the given message and replaces any object with a
115        key in the map with the result of applying that function to the object.
116        """
117        if isinstance(e, dict):
118            for k in e.keys():
119                for tag, fcn in map:
120                    if k == tag:
121                        if isinstance(e[k], list):
122                            e[k] = [ fcn(b) for b in e[k]]
123                        else:
124                            e[k] = fcn(e[k])
125                    elif isinstance(e[k], dict):
126                        remote_service_base.apply_to_tags(e[k], map)
127                    elif isinstance(e[k], list):
128                        for ee in e[k]:
129                            remote_service_base.apply_to_tags(ee, map)
130        # Other types end the recursion - they should be leaves
131        return e
132
133    @staticmethod
134    def strip_unicode(obj):
135        """Walk through a message and convert all strings to non-unicode
136        strings"""
137        if isinstance(obj, dict):
138            for k in obj.keys():
139                obj[k] = remote_service_base.strip_unicode(obj[k])
140            return obj
141        elif isinstance(obj, basestring) and not isinstance(obj, str):
142            return str(obj)
143        elif getattr(obj, "__iter__", None):
144            return [ remote_service_base.strip_unicode(x) for x in obj]
145        else:
146            return obj
147
148    @staticmethod
149    def make_unicode(obj):
150        """Walk through a message and convert all strings to unicode"""
151        if isinstance(obj, dict):
152            for k in obj.keys():
153                obj[k] = remote_service_base.make_unicode(obj[k])
154            return obj
155        elif isinstance(obj, basestring) and not isinstance(obj, unicode):
156            return unicode(obj)
157        elif getattr(obj, "__iter__", None):
158            return [ remote_service_base.make_unicode(x) for x in obj]
159        else:
160            return obj
161
162
163
164class soap_handler(remote_service_base):
165    """
166    Encapsulate the handler code to unpack and pack SOAP requests and responses
167    and call the given method.
168
169    The code to decapsulate and encapsulate parameters encoded in SOAP is the
170    same modulo a few parameters.  This is a functor that calls a fedd service
171    trhough a soap interface.  The parameters are the typecode of the request
172    parameters, the method to call (usually a bound instance of a method on a
173    fedd service providing class), the constructor of a response packet and the
174    name of the body element of that packet.  The handler takes a ParsedSoap
175    object (the request) and returns an instance of the class created by
176    constructor containing the response.  Failures of the constructor or badly
177    created constructors will result in None being returned.
178    """
179    def __init__(self, typecode, method, constructor, body_name):
180        self.typecode = typecode
181        self.method = method
182        self.constructor = constructor
183        self.body_name = body_name
184
185    def __call__(self, ps, fid):
186        req = ps.Parse(self.typecode)
187        # Convert the message to a dict with the fedid strings converted to
188        # fedid objects
189        req = self.apply_to_tags(self.unpack_soap(req), self.fedid_to_object)
190
191        msg = self.method(req, fid)
192
193        resp = self.constructor()
194        set_element = getattr(resp, "set_element_%s" % self.body_name, None)
195        if set_element and callable(set_element):
196            try:
197                set_element(self.pack_soap(resp, self.body_name, msg))
198                return resp
199            except (NameError, TypeError):
200                return None
201        else:
202            return None
203
204class xmlrpc_handler(remote_service_base):
205    """
206    Generate the handler code to unpack and pack XMLRPC requests and responses
207    and call the given method.
208
209    The code to marshall and unmarshall XMLRPC parameters to and from a fedd
210    service is largely the same.  This helper creates such a handler.  The
211    parameters are the method name, and the name of the body struct that
212    contains the response.  A handler is created that takes the params response
213    from an xmlrpclib.loads on the incoming rpc and a fedid and responds with
214    a hash representing the struct ro be returned to the other side.  On error
215    None is returned.  Fedid fields are decapsulated from binary and converted
216    to fedid objects on input and encapsulated as Binaries on output.
217    """
218    def __init__(self, method, body_name):
219        self.method = method
220        self.body_name = body_name
221
222    def __call__(self, params, fid):
223        msg = None
224
225        p = self.apply_to_tags(params[0], self.decap_fedids)
226        try:
227            msg = self.method(p, fid)
228        except service_error, e:
229            raise Fault(e.code_string(), e.desc)
230        if msg != None:
231            return self.make_unicode(self.apply_to_tags(\
232                    { self.body_name: msg }, self.encap_fedids))
233        else:
234            return None
235
236class service_caller(remote_service_base):
237    def __init__(self, service_name, port_name, locator, request_message, 
238            request_body_name, tracefile=None):
239        self.service_name = service_name
240        self.port_name = port_name
241        self.locator = locator
242        self.request_message = request_message
243        self.request_body_name = request_body_name
244        self.tracefile = tracefile
245        self.__call__ = self.call_service
246
247    def serialize_soap(self, req):
248        """
249        Return a string containing the message that would be sent to call this
250        service with the given request.
251        """
252        msg = self.request_message()
253        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
254                None)
255        if not set_element:
256            raise service_error(service_error.internal,
257                    "Cannot get element setting method for %s" % \
258                            self.request_body_name)
259        set_element(self.pack_soap(msg, self.request_body_name, req))
260        sw = SoapWriter()
261        sw.serialize(msg)
262        return unicode(sw)
263
264    def call_xmlrpc_service(self, url, req, cert_file=None, cert_pwd=None, 
265            trusted_certs=None, context=None, tracefile=None):
266        """Send an XMLRPC request.  """
267
268
269        # If a context is given, use it.  Otherwise construct one from
270        # components.  The construction shouldn't call out for passwords.
271        if context:
272            ctx = context
273        else:
274            try:
275                ctx = fedd_ssl_context(cert_file, trusted_certs, 
276                        password=cert_pwd)
277            except SSL.SSLError:
278                raise service_error(service_error.server_config,
279                        "Certificates misconfigured")
280
281        # Of all the dumbass things.  The XMLRPC library in use here won't
282        # properly encode unicode strings, so we make a copy of req with
283        # the unicode objects converted.  We also convert the url to a
284        # basic string if it isn't one already.
285        r = self.strip_unicode(copy.deepcopy(req))
286        url = str(url)
287       
288        transport = SSL_Transport(ctx)
289        port = ServerProxy(url, transport=transport)
290        # Make the call, and convert faults back to service_errors
291        try:
292            remote_method = getattr(port, self.service_name, None)
293            resp = remote_method(self.apply_to_tags(\
294                    { self.request_body_name: r}, self.encap_fedids))
295        except socket_error, e:
296            raise service_error(service_error.protocol, 
297                    "Cannot connect to %s: %s" % (url, e[1]))
298        except SSLError, e:
299            raise service_error(service_error.protocol,
300                    "SSL error contacting %s: %s" % (url, e.message))
301        except Fault, f:
302            raise service_error(None, f.faultString, f.faultCode)
303        except Error, e:
304            raise service_error(service_error.protocol, 
305                    "Remote XMLRPC Fault: %s" % e)
306
307        return self.apply_to_tags(resp, self.decap_fedids) 
308
309    def call_soap_service(self, url, req, cert_file=None, cert_pwd=None,
310            trusted_certs=None, context=None, tracefile=None):
311        """
312        Send req on to the real destination in dt and return the response
313
314        Req is just the requestType object.  This function re-wraps it.  It
315        also rethrows any faults.
316        """
317
318        tf = tracefile or self.tracefile or None
319
320        # If a context is given, use it.  Otherwise construct one from
321        # components.  The construction shouldn't call out for passwords.
322        if context:
323            ctx = context
324        else:
325            try:
326                ctx = fedd_ssl_context(cert_file, trusted_certs, 
327                        password=cert_pwd)
328            except SSL.SSLError:
329                raise service_error(service_error.server_config,
330                        "Certificates misconfigured")
331        loc = self.locator()
332        get_port = getattr(loc, self.port_name, None)
333        if not get_port:
334            raise service_error(service_error.internal, 
335                    "Cannot get port %s from locator" % self.port_name)
336        port = get_port(url,
337                transport=M2Crypto.httpslib.HTTPSConnection, 
338                transdict={ 'ssl_context' : ctx },
339                tracefile=tf)
340        remote_method = getattr(port, self.service_name, None)
341        if not remote_method:
342            raise service_error(service_error.internal,
343                    "Cannot get service from SOAP port")
344
345        # Reconstruct the full request message
346        msg = self.request_message()
347        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
348                None)
349        if not set_element:
350            raise service_error(service_error.internal,
351                    "Cannot get element setting method for %s" % \
352                            self.request_body_name)
353        set_element(self.pack_soap(msg, self.request_body_name, req))
354        try:
355            resp = remote_method(msg)
356        except socket_error, e:
357            raise service_error(service_error.protocol, 
358                    "Cannot connect to %s: %s" % (url, e[1]))
359        except SSLError, e:
360            raise service_error(service_error.protocol,
361                    "SSL error contacting %s: %s" % (url, e.message))
362        except ParseException, e:
363            raise service_error(service_error.protocol,
364                    "Bad format message (XMLRPC??): %s" % e)
365        except FaultException, e:
366            ee = self.unpack_soap(e.fault.detail[0]).get('FeddFaultBody', { })
367            if ee:
368                raise service_error(ee['code'], ee['desc'])
369            else:
370                raise service_error(service_error.internal,
371                        "Unexpected fault body")
372        # Unpack and convert fedids to objects
373        r = self.apply_to_tags(self.unpack_soap(resp), self.fedid_to_object)
374        #  Make sure all strings are unicode
375        r = self.make_unicode(r)
376        return r
377
378    def call_service(self, url, req, cert_file=None, cert_pwd=None, 
379        trusted_certs=None, context=None, tracefile=None):
380        p_fault = None  # Any SOAP failure (sent unless XMLRPC works)
381        resp = None
382        try:
383            # Try the SOAP request
384            resp = self.call_soap_service(url, req, 
385                    cert_file, cert_pwd, trusted_certs, context, tracefile)
386            return resp
387        except service_error, e:
388            if e.code == service_error.protocol: p_fault = None
389            else: raise
390        except FaultException, f:
391            p_fault = f.fault.detail[0]
392               
393
394        # If we could not get a valid SOAP response to the request above,
395        # try the same address using XMLRPC and let any faults flow back
396        # out.
397        if p_fault == None:
398            resp = self.call_xmlrpc_service(url, req, cert_file,
399                    cert_pwd, trusted_certs, context, tracefile)
400            return resp
401        else:
402            # Build the fault
403            ee = unpack_soap(p_fault).get('FeddFaultBody', { })
404            if ee:
405                raise service_error(ee['code'], ee['desc'])
406            else:
407                raise service_error(service_error.internal,
408                        "Unexpected fault body")
Note: See TracBrowser for help on using the repository browser.