source: fedd/fedd.py @ c35207d

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

More comprehensible error message for a bad service parameter

  • Property mode set to 100755
File size: 12.9 KB
RevLine 
[6ff0b91]1#!/usr/local/bin/python
2
3import sys
4
5from BaseHTTPServer import BaseHTTPRequestHandler
6
7from ZSI import Fault, ParseException, FaultFromNotUnderstood, \
8    FaultFromZSIException, FaultFromException, ParsedSoap, SoapWriter
9
10from M2Crypto import SSL
[8ecfbad]11from M2Crypto.SSL.SSLServer import ThreadingSSLServer
[329f61d]12import xmlrpclib
[6ff0b91]13
14from optparse import OptionParser
15
[51cc9df]16from fedd_util import fedd_ssl_context
17from fedid import fedid
[19cc408]18from fedd_deter_impl import new_feddservice
[f4f4117]19from fedd_services import ns0
[19cc408]20from service_error import *
[6ff0b91]21
[a97394b]22from threading import *
[11a08b0]23from signal import signal, pause, SIGINT, SIGTERM
[d199ced]24from select import select, error
[0ea11af]25from time import sleep
[11a08b0]26import logging
[72ed6e4]27from ConfigParser import *
[a97394b]28
[6ff0b91]29# The SSL server here is based on the implementation described at
30# http://www.xml.com/pub/a/ws/2004/01/20/salz.html
31
32# Turn off the matching of hostname to certificate ID
33SSL.Connection.clientPostConnectionCheck = None
34
[72ed6e4]35class fedd_config_parser(SafeConfigParser):
36    """
37    A SafeConfig parser with a more forgiving get attribute
38    """
[f4f4117]39
40    def safe_get(self, sect, opt, method, default=None):
41        """
42        If the option is present, return it, otherwise the default.
43        """
44        if self.has_option(sect, opt): return method(self, sect, opt)
45        else: return default
46
[72ed6e4]47    def get(self, sect, opt, default=None):
48        """
49        This returns the requested option or a given default.
50
51        It's more like getattr than get.
52        """
[f4f4117]53
54        return self.safe_get(sect, opt, SafeConfigParser.get, default)
55
56    def getint(self, sect, opt, default=0):
57        """
58        Returns the selected option as an int or a default.
59        """
60
61        return self.safe_get(sect, opt, SafeConfigParser.getint, default)
62
63    def getfloat(self, sect, opt, default=0.0):
64        """
65        Returns the selected option as an int or a default.
66        """
67
68        return self.safe_get(sect, opt, SafeConfigParser.getfloat, default)
69
70    def getboolean(self, sect, opt, default=False):
71        """
72        Returns the selected option as a boolean or a default.
73        """
74
75        return self.safe_get(sect, opt, SafeConfigParser.getboolean, default)
[72ed6e4]76
[8ecfbad]77class fedd_server(ThreadingSSLServer):
[0ea11af]78    """
79    Interface the fedd services to the XMLRPC and SOAP interfaces
80    """
[6ff0b91]81    def __init__(self, ME, handler, ssl_ctx, impl):
[0ea11af]82        """
83        Create an SSL server that handles the transport in handler using the
84        credentials in ssl_ctx, and interfacing to the implementation of fedd
85        services in fedd.  ME is the host port pair on which to bind.
86        """
[8ecfbad]87        ThreadingSSLServer.__init__(self, ME, handler, ssl_ctx)
[6ff0b91]88        self.impl = impl
[0b466d1]89        self.soap_methods = impl.soap_services
90        self.xmlrpc_methods = impl.xmlrpc_services
91        self.log = logging.getLogger("fedd")
[6ff0b91]92
[329f61d]93class fedd_soap_handler(BaseHTTPRequestHandler):
[0ea11af]94    """
95    Standard connection between SOAP and the fedd services in impl.
96
97    Much of this is boilerplate from
98    http://www.xml.com/pub/a/ws/2004/01/20/salz.html
99    """
[6ff0b91]100    server_version = "ZSI/2.0 fedd/0.1 " + BaseHTTPRequestHandler.server_version
101
102    def send_xml(self, text, code=200):
103        """Send an XML document as reply"""
104        self.send_response(code)
105        self.send_header('Content-type', 'text/xml; charset="utf-8"')
106        self.send_header('Content-Length', str(len(text)))
107        self.end_headers()
108        self.wfile.write(text)
109        self.wfile.flush()
[dab4d56]110        self.request.socket.close()
[6ff0b91]111
112    def send_fault(self, f, code=500):
113        """Send a SOAP encoded fault as reply"""
[0a47d52]114        self.send_xml(f.AsSOAP(processContents="lax"), code)
[6ff0b91]115
116    def check_headers(self, ps):
117        """Send a fault for any required envelope headers"""
118        for (uri, localname) in ps.WhatMustIUnderstand():
119            self.send_fault(FaultFromNotUnderstood(uri, lname, 'fedd'))
120            return False
121        return  True
122
123    def check_method(self, ps):
124        """Confirm that this class implements the namespace and SOAP method"""
125        root = ps.body_root
[7aec37d]126        if root.namespaceURI not in self.server.impl.soap_namespaces:
[6ff0b91]127            self.send_fault(Fault(Fault.Client, 
128                'Unknown namespace "%s"' % root.namespaceURI))
129            return False
[329f61d]130
131        if getattr(root, 'localName', None) == None:
132            self.send_fault(Fault(Fault.Client, 'No method"'))
[6ff0b91]133            return False
134        return True
135
136    def do_POST(self):
137        """Treat an HTTP POST request as a SOAP service call"""
138        try:
139            cl = int(self.headers['content-length'])
140            data = self.rfile.read(cl)
141            ps = ParsedSoap(data)
142        except ParseException, e:
[0a47d52]143            self.send_fault(Fault(Fault.Client, str(e)))
[6ff0b91]144            return
145        except Exception, e:
146            self.send_fault(FaultFromException(e, 0, sys.exc_info()[2]))
147            return
148        if not self.check_headers(ps): return
149        if not self.check_method(ps): return
150        try:
[19cc408]151            resp = self.soap_dispatch(ps.body_root.localName, ps,
[329f61d]152                    fedid(cert=self.request.get_peer_cert()))
[6ff0b91]153        except Fault, f:
154            self.send_fault(f)
155            resp = None
156       
157        if resp != None:
158            sw = SoapWriter()
159            sw.serialize(resp)
160            self.send_xml(str(sw))
161
[0b466d1]162    def log_request(self, code=0, size=0):
163        """
164        Log request to the fedd logger
165        """
166        self.server.log.info("Successful SOAP request code %d" % code)
167
[19cc408]168    def soap_dispatch(self, method, req, fid):
[0ea11af]169        """
170        The connection to the implementation, using the  method maps
171
172        The implementation provides a mapping from SOAP method name to the
173        method in the implementation that provides the service.
174        """
[19cc408]175        if self.server.soap_methods.has_key(method):
176            try:
177                return self.server.soap_methods[method](req, fid)
178            except service_error, e:
179                de = ns0.faultType_Def(
180                        (ns0.faultType_Def.schema,
181                            "FeddFaultBody")).pyclass()
182                de._code=e.code
183                de._errstr=e.code_string()
184                de._desc=e.desc
185                if  e.is_server_error():
186                    raise Fault(Fault.Server, e.code_string(), detail=de)
187                else:
188                    raise Fault(Fault.Client, e.code_string(), detail=de)
189        else:
190            raise Fault(Fault.Client, "Unknown method: %s" % method)
191
192
[329f61d]193class fedd_xmlrpc_handler(BaseHTTPRequestHandler):
[0ea11af]194    """
195    Standard connection between XMLRPC and the fedd services in impl.
196
197    Much of this is boilerplate from
198    http://www.xml.com/pub/a/ws/2004/01/20/salz.html
199    """
[329f61d]200    server_version = "ZSI/2.0 fedd/0.1 " + BaseHTTPRequestHandler.server_version
201
202    def send_xml(self, text, code=200):
203        """Send an XML document as reply"""
204        self.send_response(code)
205        self.send_header('Content-type', 'text/xml; charset="utf-8"')
206        self.send_header('Content-Length', str(len(text)))
207        self.end_headers()
208        self.wfile.write(text)
209        self.wfile.flush()
[dab4d56]210        # Make sure to close the socket when we're done
211        self.request.socket.close()
[329f61d]212
213    def do_POST(self):
214        """Treat an HTTP POST request as an XMLRPC service call"""
[058f58e]215        # NB: XMLRPC faults are not HTTP errors, so the code is always 200,
216        # unless an HTTP error occurs, which we don't handle.
[329f61d]217
218        resp = None
219        data = None
[0a47d52]220        method = None
[329f61d]221        cl = int(self.headers['content-length'])
222        data = self.rfile.read(cl)
223
224        try:
[0a47d52]225            params, method = xmlrpclib.loads(data)
226        except xmlrpclib.ResponseError:
227            data = xmlrpclib.dumps(xmlrpclib.Fault("Client", 
228                "Malformed request"), methodresponse=True)
[dab4d56]229
[0a47d52]230        if method != None:
231            try:
[19cc408]232                resp = self.xmlrpc_dispatch(method, params,
[0a47d52]233                            fedid(cert=self.request.get_peer_cert()))
[bcbf543]234                data = xmlrpclib.dumps((resp,), encoding='UTF-8', 
235                        methodresponse=True)
[0a47d52]236            except xmlrpclib.Fault, f:
237                data = xmlrpclib.dumps(f, methodresponse=True)
238                resp = None
[dab4d56]239
[058f58e]240        self.send_xml(data)
[329f61d]241
[0b466d1]242    def log_request(self, code=0, size=0):
243        """
244        Log request to the fedd logger
245        """
246        self.server.log.info("Successful XMLRPC request code %d" % code)
247
[329f61d]248
[19cc408]249    def xmlrpc_dispatch(self, method, req, fid):
[0ea11af]250        """
251        The connection to the implementation, using the  method maps
252
253        The implementation provides a mapping from XMLRPC method name to the
254        method in the implementation that provides the service.
255        """
[19cc408]256        if self.server.xmlrpc_methods.has_key(method):
257            try:
258                return self.server.xmlrpc_methods[method](req, fid)
259            except service_error, e:
260                raise xmlrpclib.Fault(e.code_string(), e.desc)
261        else:
262            raise xmlrpclib.Fault(100, "Unknown method: %s" % method)
263
[6ff0b91]264class fedd_opts(OptionParser):
265    """Encapsulate option processing in this class, rather than in main"""
266    def __init__(self):
267        OptionParser.__init__(self, usage="%prog [opts] (--help for details)",
268                version="0.1")
269
[a97394b]270        self.set_defaults(host="localhost", port=23235, transport="soap",
[11a08b0]271                logfile=None, debug=0)
[6ff0b91]272
273        self.add_option("-d", "--debug", action="count", dest="debug", 
274                help="Set debug.  Repeat for more information")
275        self.add_option("-f", "--configfile", action="store",
276                dest="configfile", help="Configuration file (required)")
277        self.add_option("-H", "--host", action="store", type="string",
278                dest="host", help="Hostname to listen on (default %default)")
[11a08b0]279        self.add_option("-l", "--logfile", action="store", dest="logfile", 
280                help="File to send log messages to")
[6ff0b91]281        self.add_option("-p", "--port", action="store", type="int",
282                dest="port", help="Port to listen on (default %default)")
[a97394b]283        self.add_option("-s", "--service", action="append", type="string",
284                dest="services",
285                help="Service description: host:port:transport")
[329f61d]286        self.add_option("-x","--transport", action="store", type="choice",
287                choices=("xmlrpc", "soap"),
288                help="Transport for request (xmlrpc|soap) (Default: %default)")
[6ff0b91]289        self.add_option("--trace", action="store_const", dest="tracefile", 
290                const=sys.stderr, help="Print SOAP exchange to stderr")
291
[0ea11af]292servers_active = True       # Sub-servers run while this is True
293services = [ ]              # Service descriptions
294servers = [ ]               # fedd_server instances instantiated from services
295servers_lock = Lock()       # Lock to manipulate servers from sub-server threads
[11a08b0]296
297def shutdown(sig, frame):
[0ea11af]298    """
299    On a signal, stop running sub-servers. 
300   
301    This is connected to signals below
302    """
[11a08b0]303    global servers_active, flog
[d199ced]304
[11a08b0]305    servers_active = False
306    flog.info("Received signal %d, shutting down" % sig);
307
[a97394b]308def run_server(s):
[0ea11af]309    """
310    Operate a subserver, shutting down when servers_active is false.
311
312    Each server (that is host/port/transport triple) has a thread running this
313    function, so each can handle requests independently.  They all call in to
314    the same implementation, which must manage its own synchronization.
315    """
[11a08b0]316    global servers_active   # Not strictly needed: servers_active is only read
[0ea11af]317    global servers          # List of active servers
318    global servers_lock     # Lock to manipulate servers
[11a08b0]319
[0ea11af]320    while servers_active:
[d199ced]321        try:
322            i, o, e = select((s,), (), (), 1.0)
323            if s in i: s.handle_request()
324        except error:
325            # The select call seems to get interrupted by signals as well as
326            # the main thread.  This essentially ignores signals in this
327            # thread.
328            pass
[a97394b]329
[0ea11af]330    # Done.  Remove us from the list
331    servers_lock.acquire()
332    servers.remove(s)
333    servers_lock.release()
[a97394b]334
[6ff0b91]335opts, args = fedd_opts().parse_args()
336
[0ea11af]337# Logging setup
[11a08b0]338flog = logging.getLogger("fedd")
[0ea11af]339ffmt = logging.Formatter("%(asctime)s %(name)s %(message)s",
340        '%d %b %y %H:%M:%S')
[11a08b0]341
342if opts.logfile: fh = logging.FileHandler(opts.logfile)
343else: fh = logging.StreamHandler(sys.stdout)
344
345# The handler will print anything, setting the logger level will affect what
346# gets recorded.
347fh.setLevel(logging.DEBUG)
348
349if opts.debug: flog.setLevel(logging.DEBUG)
350else: flog.setLevel(logging.INFO)
351
352fh.setFormatter(ffmt)
353flog.addHandler(fh)
354
[72ed6e4]355
356
[0ea11af]357# Initialize the implementation
[6ff0b91]358if opts.configfile != None: 
359    try:
[72ed6e4]360        config= fedd_config_parser()
361        config.read(opts.configfile)
362    except e:
363        sys.exit("Cannot parse confgi file: %s" % e)
[6ff0b91]364else: 
365    sys.exit("--configfile is required")
366
[72ed6e4]367try:
368    impl = new_feddservice(config)
369except RuntimeError, e:
370    str = getattr(e, 'desc', None) or getattr(e,'message', None) or \
371            "No message"
372    sys.exit("Error configuring fedd: %s" % str)
373
[6ff0b91]374if impl.cert_file == None:
375    sys.exit("Must supply certificate file (probably in config)")
376
[0ea11af]377# Create the SSL credentials
[2106ed1]378ctx = None
379while ctx == None:
[6ff0b91]380    try:
381        ctx = fedd_ssl_context(impl.cert_file, impl.trusted_certs, 
382                password=impl.cert_pwd)
383    except SSL.SSLError, e:
[2106ed1]384        if str(e) != "bad decrypt" or impl.cert_pwd != None:
[6ff0b91]385            raise
386
[0ea11af]387# Walk through the service descriptions and pack them into the services list.
388# That list has the form (transport (host, port)).
[a97394b]389if opts.services:
390    for s in opts.services:
[0b123ff]391        try:
392            h, p, t  = s.split(':')
393        except ValueError:
394            sys.exit("Invalid services specification: %s" % s)
[329f61d]395
[a97394b]396        if not h: h = opts.host
397        if not p: p = opts.port
398        if not t: h = opts.transport
399
400        p = int(p)
401
402        services.append((t, (h, p)))
403else:
404    services.append((opts.transport, (opts.host, opts.port)))
405
[0ea11af]406# Create the servers and put them into a list
[a97394b]407for s in services:
408    if s[0] == "soap":
409        servers.append(fedd_server(s[1], fedd_soap_handler, ctx, impl))
410    elif s[0] == "xmlrpc":
411        servers.append(fedd_server(s[1], fedd_xmlrpc_handler, ctx, impl))
[11a08b0]412    else: flog.warning("Unknown transport: %s" % s[0])
413
[0ea11af]414#  Make sure that there are no malformed servers in the list
415services = [ s for s in services if s ]
416
417# Catch signals
[11a08b0]418signal(SIGINT, shutdown)
419signal(SIGTERM, shutdown)
[a97394b]420
[0ea11af]421# Start the servers
[a97394b]422for s in servers:
[0ea11af]423    Thread(target=run_server, args=(s,)).start()
424
425# Main thread waits for signals
426while servers_active:
[d199ced]427    sleep(1.0)
[0ea11af]428
429#Once shutdown starts wait for all the servers to terminate.
430while True:
431    servers_lock.acquire()
432    if len(servers) == 0: 
433        servers_lock.release()
434        flog.info("All servers exited.  Terminating")
435        sys.exit(0)
436    servers_lock.release()
437    sleep(1)
[11a08b0]438
Note: See TracBrowser for help on using the repository browser.