summaryrefslogtreecommitdiffstats
path: root/ldap/admin/src/scripts/ds-logpipe.py
blob: 5c605570c2ec27f6068064cd86a97576b4729e36 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
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()