source: fedd/remote_service.py @ 51cc9df

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

split fedid out

  • Property mode set to 100644
File size: 12.5 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
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 call_xmlrpc_service(self, url, req, cert_file=None, cert_pwd=None, 
245            trusted_certs=None, context=None, tracefile=None):
246        """Send an XMLRPC request.  """
247
248
249        # If a context is given, use it.  Otherwise construct one from
250        # components.  The construction shouldn't call out for passwords.
251        if context:
252            ctx = context
253        else:
254            try:
255                ctx = fedd_ssl_context(cert_file, trusted_certs, 
256                        password=cert_pwd)
257            except SSL.SSLError:
258                raise service_error(service_error.server_config,
259                        "Certificates misconfigured")
260
261        # Of all the dumbass things.  The XMLRPC library in use here won't
262        # properly encode unicode strings, so we make a copy of req with
263        # the unicode objects converted.  We also convert the url to a
264        # basic string if it isn't one already.
265        r = self.strip_unicode(copy.deepcopy(req))
266        url = str(url)
267       
268        transport = SSL_Transport(ctx)
269        port = ServerProxy(url, transport=transport)
270        # Make the call, and convert faults back to service_errors
271        try:
272            remote_method = getattr(port, self.service_name, None)
273            resp = remote_method(self.apply_to_tags(\
274                    { self.request_body_name: r}, self.encap_fedids))
275        except Fault, f:
276            raise service_error(None, f.faultString, f.faultCode)
277        except Error, e:
278            raise service_error(service_error.protocol, 
279                    "Remote XMLRPC Fault: %s" % e)
280
281        return self.apply_to_tags(resp, self.decap_fedids) 
282
283    def call_soap_service(self, url, req, cert_file=None, cert_pwd=None,
284            trusted_certs=None, context=None, tracefile=None):
285        """
286        Send req on to the real destination in dt and return the response
287
288        Req is just the requestType object.  This function re-wraps it.  It
289        also rethrows any faults.
290        """
291
292        tf = tracefile or self.tracefile or None
293
294        # If a context is given, use it.  Otherwise construct one from
295        # components.  The construction shouldn't call out for passwords.
296        if context:
297            ctx = context
298        else:
299            try:
300                ctx = fedd_ssl_context(cert_file, trusted_certs, 
301                        password=cert_pwd)
302            except SSL.SSLError:
303                raise service_error(service_error.server_config,
304                        "Certificates misconfigured")
305        loc = self.locator()
306        get_port = getattr(loc, self.port_name, None)
307        if not get_port:
308            raise service_error(service_error.internal, 
309                    "Cannot get port %s from locator" % self.port_name)
310        port = get_port(url,
311                transport=M2Crypto.httpslib.HTTPSConnection, 
312                transdict={ 'ssl_context' : ctx },
313                tracefile=tf)
314        remote_method = getattr(port, self.service_name, None)
315        if not remote_method:
316            raise service_error(service_error.internal,
317                    "Cannot get service from SOAP port")
318
319        # Reconstruct the full request message
320        msg = self.request_message()
321        set_element = getattr(msg, "set_element_%s" % self.request_body_name,
322                None)
323        if not set_element:
324            raise service_error(service_error.internal,
325                    "Cannot get element setting method for %s" % \
326                            self.request_body_name)
327        set_element(self.pack_soap(msg, self.request_body_name, req))
328        try:
329            resp = remote_method(msg)
330        except ParseException, e:
331            raise service_error(service_error.protocol,
332                    "Bad format message (XMLRPC??): %s" % e)
333        except FaultException, e:
334            ee = self.unpack_soap(e.fault.detail[0]).get('FeddFaultBody', { })
335            if ee:
336                raise service_error(ee['code'], ee['desc'])
337            else:
338                raise service_error(service_error.internal,
339                        "Unexpected fault body")
340        # Unpack and convert fedids to objects
341        r = self.apply_to_tags(self.unpack_soap(resp), self.fedid_to_object)
342        #  Make sure all strings are unicode
343        r = self.make_unicode(r)
344        return r
345
346    def call_service(self, url, req, cert_file=None, cert_pwd=None, 
347        trusted_certs=None, context=None, tracefile=None):
348        p_fault = None  # Any SOAP failure (sent unless XMLRPC works)
349        resp = None
350        try:
351            # Try the SOAP request
352            resp = self.call_soap_service(url, req, 
353                    cert_file, cert_pwd, trusted_certs, context, tracefile)
354            return resp
355        except service_error, e:
356            if e.code == service_error.protocol: p_fault = None
357            else: raise
358        except FaultException, f:
359            p_fault = f.fault.detail[0]
360               
361
362        # If we could not get a valid SOAP response to the request above,
363        # try the same address using XMLRPC and let any faults flow back
364        # out.
365        if p_fault == None:
366            resp = self.call_xmlrpc_service(url, req, cert_file,
367                    cert_pwd, trusted_certs, context, tracefile)
368            return resp
369        else:
370            # Build the fault
371            ee = unpack_soap(p_fault).get('FeddFaultBody', { })
372            if ee:
373                raise service_error(ee['code'], ee['desc'])
374            else:
375                raise service_error(service_error.internal,
376                        "Unexpected fault body")
Note: See TracBrowser for help on using the repository browser.