#!/usr/bin/python2 -s

import argparse
import collections
import glob
import importlib
import logging.handlers
import os
import resource
import snmpy.mibgen
import snmpy.server
import socket
import sys
import textwrap
import urlparse
import yaml

LOG = logging.getLogger()


def parse_conf(parser):
    try:
        args = parser.parse_args()
        conf = yaml.load(open(args.config_file))

        if args.create_mib:
            conf['snmpy_global']['create_pid'] = None

        parser.set_defaults(**(conf['snmpy_global']))

        args = parser.parse_args()
        conf['snmpy_global'].update(vars(args))

        if conf['snmpy_global']['create_pid']:
            background(conf['snmpy_global']['create_pid'])

        create_log(conf['snmpy_global']['logger_dest'])
    except (IOError, yaml.parser.ParserError, yaml.scanner.ScannerError) as e:
        parser.error('cannot parse configuration file: %s' % e)

    if conf['snmpy_global']['include_dir']:
        for item in glob.glob('%s/[0-9][0-9][0-9][0-9]_*.y*ml' % conf['snmpy_global']['include_dir']):
            try:
                indx, name = os.path.splitext(os.path.basename(item))[0].split('_', 1)
                if name in conf:
                    raise ValueError('%s: plugin name already assigned at another index', name)
                if int(indx) < 1:
                    raise ValueError('%s: invalid plugin index', indx)
                if int(indx) in list(v['snmpy_index'] for k, v in conf.items() if k != 'snmpy_global'):
                    raise ValueError('%s: index already assigned to another plugin', indx)

                conf[name] = {
                    'name':        name,
                    'time':        0,
                    'save':        False,
                    'snmpy_index': int(indx),
                    'snmpy_extra': dict(args.extra_data),
                }
                conf[name].update(yaml.load(open(item)) or {})

                if conf['snmpy_global']['persist_dir'] and conf[name].get('retain', False):
                    conf[name]['save'] = '%s/%s.dat' % (conf['snmpy_global']['persist_dir'], name)

            except (IOError, yaml.parser.ParserError, yaml.scanner.ScannerError) as e:
                parser.error('cannot parse configuration file: %s' % e)

    return conf


def create_log(logger=None):
    arg = {}
    log = logging.NullHandler()
    fmt = '%(asctime)s.%(msecs)03d%(module)20s:%(lineno)-3d %(threadName)-12s %(levelname)8s: %(message)s'

    if logger:
        url = urlparse.urlparse(logger)
        arg = urlparse.parse_qs(url.query)

        if url.scheme in ('file', '') and url.path:
            log = logging.handlers.WatchedFileHandler(url.path)
        elif url.scheme.startswith('syslog'):
            fmt = 'snmpy[%(process)d]: %(module)s:%(lineno)d - %(threadName)s - %(message)s'
            if url.scheme == 'syslog+tcp':
                log = logging.handlers.SysLogHandler(address=(url.hostname or 'localhost', url.port or logging.handlers.SYSLOG_TCP_PORT), facility=arg.get('facility', ['user'])[0].lower(), socktype=socket.SOCK_STREAM)
            elif url.scheme == 'syslog+udp':
                log = logging.handlers.SysLogHandler(address=(url.hostname or 'localhost', url.port or logging.handlers.SYSLOG_UDP_PORT), facility=arg.get('facility', ['user'])[0].lower(), socktype=socket.SOCK_DGRAM)
            elif url.scheme == 'syslog+unix':
                log = logging.handlers.SysLogHandler(address=url.path or '/dev/log', facility=arg.get('facility', ['user'])[0].lower())
        elif url.scheme == 'console':
            log = logging.StreamHandler()

    log.setFormatter(logging.Formatter(fmt, '%Y-%m-%d %H:%M:%S'))

    LOG.addHandler(log)
    LOG.setLevel(getattr(logging, arg.get('level', ['info'])[0].upper()))

    LOG.info('logging started')


def background(path):
    if os.fork() == 0:
        os.setsid()

        pid = os.fork()
        if pid != 0:
            path.write('%d\n' % pid)
            path.flush()
            path.close()
            os._exit(0)
    else:
        os._exit(0)

    fd_lim = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
    for fd_num in range(fd_lim if fd_lim != resource.RLIM_INFINITY else 1024):
        try:
            os.close(fd_num)
        except OSError:
            pass


def initialize(conf):
    LOG.info('initialization started')

    plugins = collections.OrderedDict()

    try:
        keys = [i for i in sorted(conf.keys()) if i != 'snmpy_global']
        LOG.info('configuring %d plugin(s): %s', len(keys), ', '.join(keys))

        for name in keys:
            item = conf[name]['module']
            pycs = sys.dont_write_bytecode

            if '/' in item:
                sys.dont_write_bytecode = not conf['snmpy_global']['compile_bc']

                path = os.path.dirname(item)
                base = os.path.splitext(os.path.basename(item))[0]
                full = base

                if path not in sys.path:
                    sys.path.append(path)
                    LOG.info('%s: added to system path', path)
            else:
                base = item
                full = 'snmpy.module.%s' % base

            LOG.debug('%s: creating plugin instance from %s', name, full)

            code = importlib.import_module(full)
            sys.dont_write_bytecode = pycs

            plugins[name] = getattr(code, base)(conf[name])

            LOG.debug('%s: created plugin instance of %s (%s)', name, base, plugins[name])
    except Exception as e:
        snmpy.log_fatal(e, 'initialization failed')

    LOG.info('initialization complete')
    return plugins.values()


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Modular SNMP AgentX system', version=snmpy.__version__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument('-f', '--config-file', default='/etc/snmpy/snmpy.yml',
                        help='system configuration file')
    parser.add_argument('-i', '--include-dir', default='/etc/snmpy/conf.d',
                        help='plugin configuration path')
    parser.add_argument('-t', '--persist-dir',
                        help='plugin state persistence path')
    parser.add_argument('-r', '--parent-root', default='enterprises',
                        help='parent root class name')
    parser.add_argument('-s', '--system-root', type=int, default=1123,
                        help='system root object id')
    parser.add_argument('-l', '--logger-dest',
                        help='logger destination url')
    parser.add_argument('-w', '--httpd-port', default=1123, type=int,
                        help='httpd server listens on this port')
    parser.add_argument('-p', '--create-pid', nargs='?', const='/var/run/snmpy.pid', type=argparse.FileType('w'), metavar='PID_FILE',
                        help='daemonize and write pidfile')
    parser.add_argument('-m', '--create-mib', nargs='?', const=sys.stdout, type=argparse.FileType('w'), metavar='MIB_FILE',
                        help='display generated mib file and exit')
    parser.add_argument('-e', '--extra-data', default=[], action='append', nargs=2, metavar=('KEY', 'VAL'),
                        help='extra key/val data for plugins')
    parser.add_argument('-c', '--compile-bc', default=False, action='store_true',
                        help='compile bytecode for custom modules')
    parser.epilog = textwrap.dedent('''
        supported logger formats:
          -  console://?level=LEVEL
          -  file://PATH?level=LEVEL
          -  syslog+tcp://HOST:PORT/?facility=FACILITY&level=LEVEL
          -  syslog+udp://HOST:PORT/?facility=FACILITY&level=LEVEL
          -  syslog+unix://PATH?facility=FACILITY&level=LEVEL
    ''')
    if 'H2M_ENABLED' in os.environ:
        parser.usage  = argparse.SUPPRESS

    conf = parse_conf(parser)
    mods = initialize(conf)

    if conf['snmpy_global']['create_mib']:
        conf['snmpy_global']['create_mib'].write(snmpy.mibgen.create_mib(conf, mods))
    else:
        snmpy.server.SnmpyAgent(conf, mods)

# vim: sw=4 ts=4 sts=4 sta si et nu ruler cinwords=if,elif,else,for,while,try,except,finally,def,class
