diff options
Diffstat (limited to 'ldap/admin/src/scripts/ds-logpipe.py')
-rw-r--r-- | ldap/admin/src/scripts/ds-logpipe.py | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/ldap/admin/src/scripts/ds-logpipe.py b/ldap/admin/src/scripts/ds-logpipe.py new file mode 100644 index 00000000..5c605570 --- /dev/null +++ b/ldap/admin/src/scripts/ds-logpipe.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python + +import sys +import os, os.path +import errno +import signal +import pprint +import types +import time +import fcntl +import pwd + +maxlines = 1000 # set on command line +S_IFIFO = 0010000 + +buffer = [] # default circular buffer used by default plugin +totallines = 0 +logfname = "" # name of log pipe + +# default plugin just keeps a circular buffer +def defaultplugin(line): + global totallines + buffer.append(line) + totallines = totallines + 1 + if len(buffer) > maxlines: + del buffer[0] + return True + +def printbuffer(): + sys.stdout.writelines(buffer) + print "Read %d total lines" % totallines + print logfname, "=" * 60 + +def defaultpost(): printbuffer() + +plgfuncs = [] # list of plugin functions +plgpostfuncs = [] # list of post plugin funcs + +def finish(): + for postfunc in plgpostfuncs: postfunc() + sys.exit(0) + +def sighandler(signum, frame): + if signum != signal.SIGHUP: finish() + else: printbuffer() + +def isvalidpluginfile(plg): + return os.path.isfile(plg) + +def my_import(plgfile): + '''import plgfile as a python module and return + an error string if error - also return the prefunc if any''' + if not isvalidpluginfile(plgfile): + return ("%s is not a valid plugin filename" % plgfile, None, None) + # __import__ searches for the file in sys.path - we cannot + # __import__ a file by the full path + # __import__('basename') looks for basename.py in sys.path + (dir, fname) = os.path.split(plgfile) + base = os.path.splitext(fname)[0] + if not dir: dir = "." + sys.path.insert(0, dir) # put our path first so it will find our file + mod = __import__(base) # will throw exception if problem with python file + sys.path.pop(0) # remove our path + + # check for the plugin functions + plgfunc = getattr(mod, 'plugin', None) + if not plgfunc: + return ('%s does not specify a plugin function' % plgfile, None, base) + if not isinstance(plgfunc, types.FunctionType): + return ('the symbol "plugin" in %s is not a function' % plgfile, None, base) + plgfuncs.append(plgfunc) # add to list in cmd line order + + # check for 'post' func + plgpostfunc = getattr(mod, 'post', None) + if plgpostfunc: + if not isinstance(plgpostfunc, types.FunctionType): + return ('the symbol "post" in %s is not a function' % plgfile, None, base) + else: + plgpostfuncs.append(plgpostfunc) # add to list in cmd line order + + prefunc = getattr(mod, 'pre', None) + # check for 'pre' func + if prefunc and not isinstance(prefunc, types.FunctionType): + return ('the symbol "pre" in %s is not a function' % plgfile, None, base) + + return ('', prefunc, base) + +def parse_plugins(parser, options, args): + '''Each plugin in the plugins list may have additional + arguments, specified on the command line like this: + --plugin=foo.py foo.bar=1 foo.baz=2 ... + that is, each argument to plugin X will be specified as X.arg=value''' + if not options.plugins: return args + + for plgfile in options.plugins: + (errstr, prefunc, base) = my_import(plgfile) + if errstr: + parser.error(errstr) + return args + + # parse the arguments to the plugin given on the command line + bvals = {} # holds plugin args and values, if any + newargs = [] + for arg in args: + if arg.startswith(base + '.'): + argval = arg.replace(base + '.', '') + (plgarg, plgval) = argval.split('=', 1) # split at first = + if not plgarg in bvals: + bvals[plgarg] = plgval + elif isinstance(bvals[plgarg],list): + bvals[plgarg].append(plgval) + else: # convert to list + bvals[plgarg] = [bvals[plgarg], plgval] + else: + newargs.append(arg) + if prefunc: + print 'Calling "pre" function in', plgfile + if not prefunc(bvals): + parser.error('the "pre" function in %s returned an error' % plgfile) + args = newargs + + return args + +def open_pipe(logfname): + opencompleted = False + logf = None + while not opencompleted: + try: + logf = open(logfname, 'r') # blocks until there is some input + # set pipe to non-blocking +# oldflags = fcntl.fcntl(logf, fcntl.F_GETFL) +# fcntl.fcntl(logf, fcntl.F_SETFL, oldflags | os.O_NONBLOCK) + opencompleted = True + except IOError, e: + if e.errno == errno.EINTR: + continue # open was interrupted, try again + else: # hard error + raise Exception, "%s [%d]" % (e.strerror, e.errno) + return logf + +def get_server_pid(serverpidfile, servertimeout): + endtime = int(time.time()) + servertimeout + serverpid = 0 + if serverpidfile: + line = None + try: + spfd = open(serverpidfile, 'r') + line = spfd.readline() + spfd.close() + except IOError, e: + if e.errno != errno.ENOENT: # may not exist yet - that's ok + raise Exception, "%s [%d]" % (e.strerror, e.errno) + if line: + serverpid = int(line) + return serverpid + +def is_server_alive(serverpid): + retval = False + try: + os.kill(serverpid, 0) # sig 0 is a "ping" + retval = True + except OSError, e: + pass # no such process, or EPERM/EACCES + return retval + +def read_and_process_line(logf, plgfuncs): + line = None + done = False + readcompleted = False + while not readcompleted: + try: + line = logf.readline() + readcompleted = True # read completed + except IOError, e: + if e.errno == errno.EINTR: + continue # read was interrupted, try again + else: # hard error + raise Exception, "%s [%d]" % (e.strerror, e.errno) + if line: # read something + for plgfunc in plgfuncs: + if not plgfunc(line): + print "Aborting processing due to function %s" % str(plgfunc) + finish() + done = True + break + else: # EOF + done = True + return done + +def parse_options(): + from optparse import OptionParser + usage = "%prog <name of pipe> [options]" + parser = OptionParser(usage) + parser.add_option("-m", "--maxlines", dest="maxlines", type='int', + help="maximum number of lines to keep in the buffer", default=1000) + parser.add_option("-d", "--debug", dest="debug", action="store_true", + default=False, help="gather extra debugging information") + parser.add_option("-p", "--plugin", type='string', dest='plugins', action='append', + help='filename of a plugin to use with this log') + parser.add_option("-s", "--serverpidfile", type='string', dest='serverpidfile', + help='name of file containing the pid of the server to monitor') + parser.add_option("-t", "--servertimeout", dest="servertimeout", type='int', + help="timeout in seconds to wait for the serverpid to be alive", default=60) + parser.add_option("--serverpid", dest="serverpid", type='int', + help="process id of server to monitor", default=0) + parser.add_option("-u", "--user", type='string', dest='user', + help='name of user to set effective uid to') + + options, args = parser.parse_args() + + args = parse_plugins(parser, options, args) + + if len(args) < 1: + parser.error("You must specify the name of the pipe to use") + if len(args) > 1: + parser.error("error - unhandled command line arguments: %s" % args.join(' ')) + + return options, args[0] + +options, logfname = parse_options() + +if len(plgfuncs) == 0: + plgfuncs.append(defaultplugin) +if len(plgpostfuncs) == 0: + plgpostfuncs.append(defaultpost) + +if options.user: + try: userid = int(options.user) + except ValueError: # not a numeric userid - look it up + userid = pwd.getpwnam(options.user)[2] + os.seteuid(userid) + +serverpid = options.serverpid +if serverpid: + if not is_server_alive(serverpid): + print "Server pid [%d] is not alive - exiting" % serverpid + sys.exit(1) + +try: + if os.stat(logfname).st_mode & S_IFIFO: + print "Using existing log pipe", logfname + else: + print "Error:", logfname, "exists and is not a log pipe" + print "use a filename other than", logfname + sys.exit(1) +except OSError, e: + if e.errno == errno.ENOENT: + print "Creating log pipe", logfname + os.mkfifo(logfname) + os.chmod(logfname, 0600) + else: + raise Exception, "%s [%d]" % (e.strerror, e.errno) + +print "Listening to log pipe", logfname, "number of lines", maxlines + +# set up our signal handlers +signal.signal(signal.SIGHUP, sighandler) +signal.signal(signal.SIGINT, sighandler) +#signal.signal(signal.SIGPIPE, sighandler) +signal.signal(signal.SIGTERM, sighandler) +signal.signal(signal.SIGALRM, sighandler) + +if options.serverpidfile: + # start the timer to wait for the pid file to be available + signal.alarm(options.servertimeout) + +done = False +while not done: + # open the pipe - will hang until + # 1. something opens the other end + # 2. alarm goes off + logf = open_pipe(logfname) + + if serverpid: + if not is_server_alive(serverpid): + print "Server pid [%d] is not alive - exiting" % serverpid + sys.exit(1) + else: # cancel the timer + signal.alarm(0) # cancel timer - got pid + + innerdone = False + while not innerdone and not done: + # read and process the next line in the pipe + # if server exits while we are reading, we will get + # EOF and innerdone will be True + # we may have to read some number of lines until + # we can get the pid file + innerdone = read_and_process_line(logf, plgfuncs) + if not serverpid and options.serverpidfile: + serverpid = get_server_pid(options.serverpidfile, options.servertimeout) + if serverpid: + signal.alarm(0) # cancel timer - got pid + + if logf: + logf.close() + logf = None + + if not done: + if serverpid: + if not is_server_alive(serverpid): + print "Server pid [%d] is not alive - exiting" % serverpid + sys.exit(1) + else: # the server will sometimes close the log and reopen it + # however, at shutdown, the server will close the log before + # the process has exited - so is_server_alive will return + # true for a short time - if we then attempt to open the + # pipe, the open will hang forever - to avoid this situation + # we set the alarm again to wake up the open + signal.alarm(options.servertimeout) + + print "log pipe", logfname, "closed - reopening" + +finish() |