source: fedd/remote_service.py @ c7e40f5

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

restore serialize

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