source: fedd/fedd.py @ 4ed10ae

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

Proxy key additions working

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