source: fedd/remote_service.py @ 9460b1e

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

move remote_service out of fedd_util

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