#!/usr/bin/python
####################################################################################
#
# nagios-ar
#
# Nagios-AR daemon is a part of Nagios automatic hosts self-registration software.
# NAR for simplicity.
# For detailer NAR description read README file.
#
# Usage:
#   Nagios-AR is indended to run as a system daemon. I use Ubuntu and Debian. The
#   easiest way to run it is to create systemd configuraton. Sample 'nagios-ar.conf
#   is provided in NAR package. Script should be placed in /etc/init folder.
#   Later you can # use regular service nagios-ar start/stop/status commands to
#   manage Nagios-AR daemon.
#
# Requirements:
#   Pyhon packages - beanstalkc, daemon. The rest are IMHO standard in Ubuntu/Debian
#   Nagios V3 - is up and running on the same computer
#   Beanstalkd - tasks queue manager should be installed and running prior Nagios-AR
#      starts.
#   Nagios-AR daemon should have enough rights to restart Nagios with service
#      command.
#
# Configuration:
#   logfile - runtime log file name
#   nagios_hosts - path where Nagios keeps hosts configuration files. Must be
#      accesible for writing for Nagios-AR daemon
#
# (C) 2016 Andrey A. Porodko <andrey.porodko@gmail.com>
# (C) 2016 Axian Ltd, http://axian.ru
#
# v0.1 - initial release
# v0.2 - bigfixing
# v0.21 - bugfixing again (forgor to catch exception in supplimental queue 
#         processing)
#
###################################################################################
import sys, os, string, time
import beanstalkc, threading
import subprocess, time
import logging, daemon

logfile = '/var/log/nagios3/nagios-ar.log'
nagios_hosts = '/etc/nagiosql/hosts'
nagios = 'nagios3'
fatal_error = False

def getConfigTemplate():
    """ Defines nagios host configuration file template."""
    template = """###############################################################################
    #
    # Host configuration file
    #
    # Created by: Nagios AR Version 0.1
    # Date:	      $date
    # Version:    Nagios 3.x config file
    #
    # --- DO NOT EDIT THIS FILE BY HAND ---
    # Nagios AR will overwite all manual settings during the next update
    #
    ###############################################################################
    define host {
    \thost_name\t$host
    \talias\t$host
    \taddress\t$host
    \thostgroups\t$hostgroup
    \tuse\tdynamic-host,$use
    \tregister\t1
    }"""
    return template

def reloadNagios():
    global nagios
    """ Realod Nagios in order to pick up configuration changes."""
    command = ['service', nagios, 'reload'];
    subprocess.call(command, shell=False)
    return

def registerHost(path,host,register=True):
    """ For existent host configuration changes 'register' status."""
    filename = getConfigName(path,host)
    try:
        with open(filename,'r') as f:
            conf = f.read()
        f.closed
        strings = string.split(conf,'\n')
        with open(filename,'w') as f:
            for str in strings:
                foundat = str.find('register')
                if foundat <> -1:
                    if register == True:
                        str = string.replace(str,'0','1')
                    else:
                        str =string.replace(str,'1','0')
                f.write(str+'\n')
        f.closed
        r = True
    except Exception as e:
        logging.error('Unable to write configuration for '+host+'. Error: '+repr(e))
        r = False
    return r

def getConfigName(path,host):
    """ That's simple - builds full path to host configuration file."""
    return path+'/'+host+'.cfg'

def delConfig(path,host):
    """ Removes host configuration file."""
    try:
        os.remove(getConfigName(path,host))
        r = True
    except Exception as e:
        logging.error('Unable to remove configuration file. Error: '+repr(e))
        r = False
    return r

#
# There is nothing fancy about this code. It's rather selfexplanatory.
#
def addConfig(path,host,hostgroup=None,use=None):
    """ Builds and writes new host configuration file."""
    config = string.Template(getConfigTemplate())
    if hostgroup is None:
        hostgroup = 'servers'
    if use is None:
        use = 'generic-host';
    conf = config.substitute(date=time.strftime("%c"),host=host,hostgroup=hostgroup,use=use)

    filename = getConfigName(path,host)
    try:
        with open(filename,'w') as f:
            f.write(conf)
        f.closed
        r = True
    except Exception as e:
        logging.error('Unable to write configuration for '+host+'. Error: '+repr(e))
        r = False
    return r

def commandProcessor():
    global fatal_error
    global nagios_hosts
    """ Processes commands from beanstalk task queue."""
    logging.info('Starting commands processor...');
    try:
        commands = beanstalkc.Connection(host='localhost', port=11300)
        refresher = beanstalkc.Connection(host='localhost', port=11300)
        refresher.use('nagios-reload')
        commands.watch('nagios')
        while fatal_error == False:
            try:
                job = commands.reserve(timeout=10)
                if job <> None:
                    lexems = string.split(job.body,' ')
                    if len(lexems) < 2:
                        logging.error('Not enough paramaters in the messages: ',lexems)
                    else:
                        cmd = string.lower(lexems[0])
                        if cmd == 'add':
                            if len(lexems) < 4:
                                logging.error('Not enough paramaters in the "add" command: ', lexems)
                            else:
                                addConfig(nagios_hosts,lexems[1],lexems[2],lexems[3])
                                logging.info('New host has been added: '+lexems[1])
                                refresher.put('1')
                        elif cmd == 'remove':
                            delConfig(nagios_hosts,lexems[1])
                            logging.info('Host has been removed: '+lexems[1])
                            refresher.put('2')
                        elif cmd == 'register':
                            if registerHost(nagios_hosts, lexems[1], True) == True:
                                logging.info('New host has been registered: '+lexems[1])
                                refresher.put('3')
                        elif cmd == 'deregister':
                            if registerHost(nagios_hosts, lexems[1], False) == True:
                                logging.info('New host has been deregistered: '+lexems[1])
                                refresher.put('4')
                        else:
                            logging.error('Unknown command: '+cmd)
                    job.delete()
            except Exception as e:
                logging.error('Error in '+commandProcessor.__name__+' processing: "'+job.body+'": '+repr(e))
                job.delete()
    except Exception as e:
        logging.critical('Beanstalk error in '+commandProcessor.__name__+': '+repr(e))
        fatal_error = True
    return

#
# Idea for nagios refresher is simple - commandProcessor after each succesfull command
# processed saves a value to a supplimental queue. nagiosRefresher indefinitely waits for
# values in this queue. When it discovers any value it starts to read all of them with 5
# seconds intervals until queue is empty. After that nagosRefresher reload Nagios. Thus
# we avoid frequent Nagios reloading during massive updates - Nagios will be reloaded
# only once if updates come less in 5 seconds intervals.
#
def nagiosRefresher():
    global fatal_error
    """ Processes supplimental queue 'nagios-reload'."""
    logging.info('Starting nagios refresher...')
    try:
        refresher = beanstalkc.Connection(host='localhost', port=11300)
        refresher.watch('nagios-reload')
        while fatal_error == False:
    	    try:
    		restart = False
        	job = refresher.reserve()
                while job <> None:
		    job.delete()
            	    job = refresher.reserve(timeout=5)
            	    restart = True
                if restart == True:
		    logging.info('Reloading Nagios daemon ...')
		    reloadNagios()
            except Exception as e:
                logging.error('Error in '+nagios_Refresher.__name__+' processing supplimental queue: '+repr(e))
                job.delete()
    except Exception as e:
        logging.critical('Error in '+nagiosRefresher.__name__+': '+repr(e))
        fatal_error = True
    return

#
# Main process.
#
def mainProcess():
    global fatal_error
    """ Main process. Starts threads and waits external OS signals."""
    try:
        threads = []
        fatal_error = False
        try:
            cp = threading.Thread(target=commandProcessor)
            cp.daemon = True
            threads.append(cp)
            cp.start()
            nr = threading.Thread(target=nagiosRefresher)
            nr.daemon = True
            threads.append(nr)
            nr.start()
        except ThreadError as e:
            logging.critical('Unable to start threads. Error: '+repr(e))
        while fatal_error == False:
            time.sleep(1)
    except KeyboardInterrupt:
        logging.critical('Stopping application...')
    return

if __name__ == '__main__':
    with daemon.DaemonContext():
	logging.basicConfig(filename=logfile,level=logging.INFO,
	    format='%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s', datefmt="%Y-%m-%d %H:%M:%S")
        mainProcess()
    if fatal_error:
	exit_code = -1
	logging.critical('Critical error. Exit application.')
    else:
	exit_code = 0
        logging.info('Exit application.')
    sys.exit(exit_code)
