#!/usr/bin/python
##
# Copyright (C) 2003 by Duke University
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
# 02111-1307, USA.
#
# $Id$
#
# @Author Konstantin Ryabitsev <icon@linux.duke.edu>
# @version $Date$
#

import os
import sys
import getopt
import time
import libxml2

sys.path.insert(0, '/usr/lib/python2.7/site-packages')
from epylog import *

DEFAULT_EPYLOG_CONFIG = '/etc/epylog/epylog.conf'
EPYLOG_PIDFILE = '/var/run/epylog.pid'

def unxmlify_offsets(ofile, logger):
    """
    Take the XML file with offsets and return them as a dictionary.
    """
    logger.put(5, '>epylog.unxmlify_offsets')
    logger.put(3, 'Checking if we can read "%s"' % ofile)
    if not os.access(ofile, os.R_OK):
        logger.put(3, 'Could not read offsets file "%s"' % ofile)
        logger.put(3, 'Returning blank tuple')
        logger.put(5, '<epylog.unxmlify_offsets')
        return []
    try:
        doc = libxml2.parseFile(ofile)
    except:
        logger.put(3, 'Could not parse offsets file "%s"' % ofile)
        logger.put(3, 'Returning blank tuple')
        logger.put(5, '<epylog.unxmlify_offsets')
        return []
    
    omap = []
    root = doc.getRootElement()
    enode = root.children
    while enode:
        if enode.name == 'entry':
            kid = enode.children
            while kid:
                if kid.name == 'log': entry = kid.content.strip()
                elif kid.name == 'inode': inode = int(kid.content)
                elif kid.name == 'offset': offset = int(kid.content)
                kid = kid.next
            omap.append([entry, inode, offset])
        enode = enode.next
    doc.freeDoc()
    logger.put(5, omap)
    logger.put(5, '<epylog.unxmlify_offsets')
    return omap

def xmlify_offsets(omap, ofile, logger):
    """
    Take a dictionary of offset data and stick it into an XML file.
    """
    logger.put(5, '>epylog.xmlify_offsets')
    try:
        logger.put(3, 'Trying to open "%s" for writing.' % ofile)
        fh = open(ofile, 'w')
    except IOError:
        logger.put(0, 'Could not open "%s" for writing! Offsets not saved!')
        return
    logger.puthang(3, 'Making XML out of offset map')
    doc = libxml2.newDoc('1.0')
    root = doc.newChild(None, 'epylog-offsets', None)
    for entry in omap:
        enode = root.newChild(None, 'entry', None)
        enode.newChild(None, 'log', entry[0])
        enode.newChild(None, 'inode', str(entry[1]))
        enode.newChild(None, 'offset', str(entry[2]))
    logger.endhang(3)
    offsets = doc.serialize()
    doc.freeDoc()
    logger.put(5, offsets)
    import fcntl
    logger.put(3, 'Locking the offsets file')
    fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
    logger.puthang(3, 'Writing the offsets into "%s"' % ofile)
    fh.write(offsets)
    logger.endhang(3)
    logger.put(3, 'Unlocking the offsets file')
    fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
    fh.close()
    logger.put(5, '<epylog.xmlify_offsets')

def restore_offsets(epylog):
    """
    Restore offset data from disk and pass it to epylog.
    """
    logger = epylog.logger
    logger.put(5, '>epylog.restore_offsets')
    ofile = os.path.join(epylog.vardir, 'offsets.xml')
    omap = unxmlify_offsets(ofile, logger)
    for o in omap:
        try:
            epylog.logtracker.set_start_offset_by_entry(o[0], o[1], o[2])
        except NoSuchLogError:
            logger.put(0, 'No such log in tracker: %s' % o[0])
    logger.put(5, '<epylog.restore_offsets')
    
def store_offsets(epylog):
    """
    Take offset data from epylog and store it on disk to be used later.
    """
    logger = epylog.logger
    logger.put(5, '>epylog.store_offsets')
    ofile = os.path.join(epylog.vardir, 'offsets.xml')
    omap = epylog.logtracker.get_offset_map()
    xmlify_offsets(omap, ofile, logger)
    logger.put(5, '<epylog.store_offsets')

def epylock(mode=0777):
    """
    Locking when running in cron mode, so in case of a WHOLE LOT of
    entries the processes do not overlap.
    """
    mypid = str(os.getpid())
    try:
        fd = os.open(EPYLOG_PIDFILE, os.O_EXCL|os.O_CREAT|os.O_WRONLY, mode)
    except OSError, msg:
        if not msg.strerror == "File exists": raise msg
        try:
            pid = int(open(EPYLOG_PIDFILE).read())
        except ValueError:
            msg = 'Pidfile %s contains non numeric value'
            sys.exit(msg % EPYLOG_PIDFILE)
        import errno
        try: os.kill(pid, 0)
        except OSError, why:
            if why[0] == errno.ESRCH:
                # The pid doesn't exists.
                os.remove(EPYLOG_PIDFILE)
            else:
                msg = "Can't check status of PID %s from pidfile %s: %s"
                sys.exit(msg % (pid, EPYLOG_PIDFILE, why[1]))
        else:
            msg = "Another Epylog process seems to be running, PID %s."
            sys.exit(msg % pid)
    else:
        os.write(fd, mypid)
        os.close(fd)

def epyunlock():
    """
    Remove the lock.
    """
    os.unlink(EPYLOG_PIDFILE)

def parselast(last):
    """
    Make sense of the --last value
    """
    msg = "Unknown setting for --last: %s. See --help" % last
    if last == 'hour': last = '1h'
    elif last == 'day': last = '1d'
    elif last == 'week': last = '1w'
    elif last == 'month': last = '1m'
    cat = last[-1:].lower()
    try: num = int(last[:-1])
    except: sys.exit(msg)
    if cat == 'h': mult = 1
    elif cat == 'd': mult = 24
    elif cat == 'w': mult = 24*7
    elif cat == 'm': mult = 24*30
    else: sys.exit(msg)
    now = int(time.time())
    then = now - (num * mult * 60 * 60)
    return then

def usage():
    print """
    Usage: epylog [--quiet] [--store-offsets] [--last] [-c] [-d]

        -c config-file
            read a custom config file instead of /etc/epylog/epylog.conf

        -d debug-level
            a number from 0 to 5. 0 means only critical output, while 5
            means lots and lots of debugging info.

        --store-offsets
            this will store an offset.xml file in /var/lib/epylog. This
            is useful when running epylog from cron, since then it relies
            on actual offsets as opposed to timestamps, which do not have to
            be accurate.

        --quiet
            completely identical to -d 0

        --cron
            Equivalent of --quiet --store-offsets, plus it will create a
            lock file that will not allow more than one cron instance of
            epylog to run.

        --last [hour|day|week|month|Nh|Nd|Nw|Nm]
            will analyze strings from the past [time period] specified.

        If no command-line options are provided, then the logs will be
        processed in their entirety (WARNING: this can mean a LOT of logs).
        A useful way to init a system would be to run:
        epylog --last [hour|day] --store-offsets

    Example:
        epylog --last day
    """
    sys.exit(1)

def main(args):
    debuglvl = 1
    o_stor = 0
    o_stamp = 0
    o_cron = 0
    config_file = DEFAULT_EPYLOG_CONFIG
    cmdargs = args[1:]
    try:
        gopts, cmds = getopt.getopt(cmdargs, 'd:c:h',
                                    ['quiet', 'store-offsets', 'last=',
                                     'help', 'cron'])
        for o,a in gopts:
            if o == '-d': debuglvl = int(a)
            elif o == '--quiet': debuglvl = 0
            elif o == '--store-offsets': o_stor = 1
            elif o == '--cron': o_cron = 1
            elif o == '--last': o_stamp = parselast(a)
            elif o == '-c': config_file = a
            elif o == '-h' or o == '--help': usage()                
    except getopt.error, e:
        print 'Error: %s' % e
        usage()
    if o_cron:
        ##
        # Cron mode. Try to lock, and set --quiet and --store-offsets
        #
        epylock()
        o_stor = 1
        debuglvl = 0        
    logger = Logger(debuglvl)
    logger.puthang(1, 'Initializing epylog')
    try:
        epylog = Epylog(config_file, logger)
    except (ConfigError, ModuleError), e:
        logger.put(0, "Error returned: %s" % e)
        sys.exit(1)
    logger.endhang(1, 'done')
    if o_stamp == 0:
        logger.puthang(1, 'Restoring log offsets')
        restore_offsets(epylog)
        logger.endhang(1, 'done')
    else:
        logger.puthang(1, 'Setting the offsets by timestamp')
        epylog.logtracker.set_range_by_timestamps(o_stamp, int(time.time()))
        logger.endhang(1)
    logger.put(1, 'Invoking the module execution routines:')
    epylog.process_modules()
    logger.put(1, 'Finished processing modules')
    logger.puthang(1, 'Making the report')
    useful = epylog.make_report()
    logger.endhang(1, 'done')
    if useful:
        logger.puthang(1, 'Publishing the report')
        epylog.publish_report()
        logger.endhang(1, 'done')
        if o_stor:
            logger.puthang(1, 'Storing the offsets')
            store_offsets(epylog)
            logger.endhang(1, 'done')
        
    else:
        logger.put(1, 'Report is empty. Exiting.')

    logger.puthang(1, 'Cleaning up')
    epylog.cleanup()
    logger.endhang(1, 'done')
    if o_cron: epyunlock()

if __name__ == '__main__':
    main(sys.argv)

##
# local variables:
# mode: python
# end:
