#!/usr/bin/env python
from __future__ import with_statement
"""
Program to load NetLogger log files into a relational database
"""
__rcsid__ = "$Id: nl_loader 971 2008-09-05 13:55:32Z dang $"
__author__ = "Dan Gunter"

import logging
import optparse
import os
import signal 
import sys
import time
import warnings
#
from netlogger.analysis import nlloader
from netlogger.analysis.loaderConfig import Configuration
from netlogger import nllog
from netlogger import pipeline
from netlogger import util

# Name of my program
g_prog = pipeline.PROG_LOADER

# Always have a valid log object
log = nllog.NullLogger()

def activateLogging(name="netlogger.nl_loader", pipeline_started=False):
    """Activate the logging in the script.
    If the pipeline has started, use a different suffix so the same
    log destination (an error file in /tmp) is not re-used.
    """
    global log
    if pipeline_started:
        log = nllog.getLogger(name + ".pipeline")
    else:
        log = nllog.getScriptLogger(name + ".script")
        warnings.simplefilter("ignore", RuntimeWarning)

def activateLoggingAfterPipeline():
    activateLogging(pipeline_started=True)

# global application obj (for signal handling)
app = None
# global pidfile obj (for signal handling)
g_pidfile = None

## Signal handlers ##

def killHandler(signo, frame):
    """Signal handler for a graceful exit.
    """
    # Keep flush before saveState to get file offset correct
    app.flush()
    app.saveState()
    log.warn("killed", msg="killed by signal", signo=signo)
    with nllog.logged(log, "exit", level=logging.INFO):
        _exit(0)

def hupHandler(signo, frame):
    """Signal handler for a hangup."""
    # Keep flush before saveState to get file offset correct
    app.flush()
    app.saveState()

def _exit(status):
    log.info("end", status=status)
    app.exit(status, g_prog, g_pidfile)

def parseArgs():
    desc = """Load NetLogger log files into a relational database
The program runs in two modes: 'standalone' and 'pipeline'.
The use of one mode or the other is signaled
by whether the '-u' option (for standalone mode) or '-c'
option (for pipeline mode) is given."""
    usage = "%prog [options]"
    parser = optparse.OptionParser(usage=usage, description=desc, version="2.0a")
    group = parser.add_option_group("Pipeline-mode options")
    group.add_option('-c', '--config', 
                     action="store", dest="config", default=None,
                     metavar="FILE",
                     help="use configuration in FILE")
    group.add_option('-d', '--daemon', action='store', metavar='PIDFILE',
                     default=None, dest="daemon",
                     help="run in daemon mode, writing PID to PIDFILE. "
                     "Requires -c, --config option.")
    group.add_option('-n', '--no-action', action='store_true',
                     default=False, dest="noexec",
                     help="do not do anything, just show what would be done"
                     " (default=%default)")
    group = parser.add_option_group("Standalone-mode options")
    group.add_option('-b','--insert-batch',
                     action='store', type="int", dest='ibatch',
                     default=100,
                     help="number of INSERT statements per transaction " + 
                     ("(default=%default)"))
    group.add_option('-C', '--create', action='store_true', dest="create", default=False,
                     help="create new database before using it")
    group.add_option('-D', '--drop', action='store_true', dest="drop", 
                     default=False,
                     help="drop existing database first; implies -C/--create")
    group.add_option('-i', '--input', action="store",
                     dest="ifile", default=None, metavar="FILE",
                     help="read input from FILE (default=stdin)")
    group.add_option('-p', '--param', action="append", dest="db_param",
                     default=[],
                     help="parameters for database connect() function, "
                     "formatted as 'name=value'. repeatable. "
                     "For MySQL, use read_default_file=~/.my.cnf to "
                     "avoid putting the password on the command-line.")
    group.add_option('-r', '--restore', action='store', dest='restore',
                     default=None, metavar='FILE',
                     help="restore log file name and offset from FILE") 
    group.add_option('-s', '--schema-file', action='store', 
                     dest='schema_file', default=None, metavar='FILE',
                     help="Read schema configuration from FILE")
    group.add_option('--secret', action='store',
                     dest="secret_file", default=None, metavar='FILE',
                     help="For pipeline mode, FILE has authentication "
                     "secret to use with commands with nl_pipeline. Users "
                     "should not need to worry about this option.")
    group.add_option('--schema-init', action='store', 
                     dest='schema_init', default="",
                     metavar='KEY1,KEY2, ..',
                     help="Comma-separated keys for type of "
                     "initialization schema to use (default=first in file)")
    group.add_option('--schema-finalize', action='store', 
                     dest='schema_finalize', default="", 
                     metavar='KEY1,KEY2, ..',
                     help="Comma-separated keys for type of "
                     "finalization schema to use (default=first in file)")
    group.add_option('-u', '--uri', default=None,
                     action='store', dest='db_uri', metavar='URI',
                     help="database connection URI, where the "
                     "database module name is used as the URI scheme. "
                     "MySQL and PostgreSQL modules require a server host; the "
                     " sqlite and test modules require a filename. "
                     "(required)")
    group.add_option('-U', '--no-unique', action='store_false', 
                     dest='unique', default=True,
                     help="do not enforce unique events. "
                     "With -C or -D, this removes the UNIQUE constraint "
                     "on the table. With existing tables, new keys are "
                     "guaranteed unique regardless of the event.")
    group.add_option('-v', '--verbose', action="count", default=0,
                     dest='verbosity', help="Verbose logging")
    options, args = parser.parse_args()
    return parser, options, args

def validateOptions(options, parser):
    """Validate the command-line options.
    """
    mode_pipeline = options.config is not None
    mode_standalone = options.db_uri is not None
    if not mode_pipeline and not mode_standalone:
        parser.error("one of '-c' or '-u' is required")
    elif mode_pipeline and mode_standalone:
        parser.error("'-c' and '-u' options conflict")
    if options.drop:
        options.create = True
    def check_bad(opts, mode):
        for opt in opts:
            val = getattr(options, parser.get_option(opt).dest)
            if val:
                opt_str = str(parser.get_option(opt))
                parser.error("bad option for '%s' mode: %s" % (mode,opt_str))
    if mode_pipeline:
        check_bad(('-C', '-u', '-p', '-i', '-r'), "pipeline")
        options.config = os.path.abspath(options.config)
        if not os.path.exists(options.config):
            parser.errorf("No such conf. file '%s'" % options.config)
    elif mode_standalone:
        check_bad(('-d', '-n'), "standalone")
        if options.ifile is None:
            options.ifile = sys.stdin.name
        # get parameters from cmdline
        options._params = [ ]
        for nameval in options.db_param:
            try:
                name, val = nameval.split('=')
                options._params.append((name, val))
            except ValueError:
                parser.error("'%s': bad paramater format, use name=value" % 
                             nameval)
    return mode_pipeline

def initSignals():
    util.handleSignals(
        (killHandler, ('SIGTERM', 'SIGINT', 'SIGUSR2')),
        (hupHandler, ('SIGHUP',)) )

def main():
    """Program entry point.
    """
    global app, g_pidfile

    parser, options, args = parseArgs()
    activateLogging()
    # Argument handling
    is_pipeline = validateOptions(options, parser)
    vb_level = nllog.verbosityToLevel(options.verbosity)
    daemonic = options.daemon is not None
    # Daemonize
    g_pidfile, is_daemonic = None, False
    if options.daemon:
        g_pidfile = pipeline.getPidFile(options.daemon, parser)
        pipeline.daemonize(log, g_prog, g_pidfile)
        activateLoggingAfterPipeline()
        is_daemonic = True
    else: # .. or not
        # Set default logging level from -v flag
        log.setDefaultLevel(vb_level)
    # Init, part 1
    init_ok = False
    with nllog.logged(log, "init", level=logging.INFO):
        app = nlloader.Application(options)
        if not is_pipeline and not daemonic:
            app.initLogging(vb_level, log.getDefaultHandler())
        app.configure()
        init_ok = True
    if not init_ok:
        sys.exit(-1)
    # Stop if a dry run
    if options.noexec:
        pipeline.dumpConfigAndExit(app)
    # Daemonic mode
    #if is_pipeline:
    #    log.redirectDefaultOutput('/dev/null')
    # Set up signal handlers
    with nllog.logged(log, "initSignals"):
        initSignals()
    # Run        
    status = app.run()
    _exit(status) 
    
if __name__ == '__main__':
    main()
