#!/usr/bin/env python
# -*- coding: ISO-8859-1 -*-
#
# Copyright 2002-2004 Norwegian University of Science and Technology
#
# This file is part of Network Administration Visualized (NAV)
#
# NAV 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.
#
# NAV 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 NAV; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
# $Id: $
# Authors: Magnus Nordseth <magnun@itea.ntnu.no>
#
"""
Pings multiple hosts in parallel
"""

import os
import signal
import getopt
import time
import pwd

from nav.statemon import rrd
from nav.statemon import megaping
from nav.statemon import db
from nav.statemon import config
from nav.statemon import circbuf
from nav.statemon import debug
from nav.statemon.event import Event
from nav.statemon.netbox import Netbox

class pinger:
    def __init__(self, **kwargs):
        signal.signal(signal.SIGHUP, self.signalhandler)
        signal.signal(signal.SIGTERM, self.signalhandler)
        self.config=config.pingconf()
        debug.setDebugLevel(int(self.config.get("debuglevel",5)))
        debug.debug("Setting debuglevel=%s "% self.config.get("debuglevel",5))
        self._isrunning=1
        self._looptime=int(self.config.get("checkinterval",60))
        debug.debug("Setting checkinterval=%i" %self._looptime)
        self._debuglevel=0
        self.dbconf=config.dbconf()
        self.db=db.db(self.dbconf)
        sock = kwargs.get("socket",None)
        self.pinger=megaping.MegaPing(sock)
        self._nrping = int(self.config.get("nrping" ,3))
        # To keep status...
        self.netboxmap = {} # hash netboxid -> netbox
        self.down = []      # list of netboxids down
        self.replies = {}      # hash netboxid -> circbuf
        self.ipToNetboxid = {}
                      
    def updateHostList(self):
        """
        Fetches all netboxes from the NAVdb, and updates
        internal data structures.
        """
        debug.debug("Getting hosts from database...",7)
        hosts = self.db.hostsToPing()
        netboxmap = {}
        self.ipToNetboxid = {}
        for host in hosts:
            netboxid, deviceid, sysname, ip, up = host
            netbox = Netbox(netboxid, deviceid, sysname, ip, up)
            if not self.netboxmap.has_key(netbox.netboxid):
                # new netbox. Be sure to get it's state
                if netbox.up != 'y':
                    debug.debug("Got new netbox, %s, currently "
                                "marked down in navDB" % netbox.ip, 7)
                    self.down.append(netbox.netboxid)
            if not self.replies.has_key(netbox.netboxid):
                self.replies[netbox.netboxid] = circbuf.CircBuf()
                if netbox.up != 'y':
                    buf = self.replies[netbox.netboxid]
                    # This genious line marks all-down for the whole buf
                    map(buf.push, [-1]*len(buf))
            netboxmap[netbox.netboxid]=netbox
            self.ipToNetboxid[netbox.ip] = netbox.netboxid
        # Update netboxmap
        self.netboxmap = netboxmap
        debug.debug("We now got %i hosts in our list to ping" % 
                    len(self.netboxmap), 7)
        #then update our pinger object
        self.pinger.setHosts(self.ipToNetboxid.keys())

    def generateEvents(self):
        """
        Report state changes to event engine.
        """
        debug.debug("Checks which hosts didn't answer",7)
        answers = self.pinger.results()
        for ip, rtt in answers:
            # rtt = round trip time (-1 => host didn't reply)
            netboxid = self.ipToNetboxid.get(ip)
            self.replies[netboxid].push(rtt)
            netbox = self.netboxmap[netboxid]
            if rtt != -1:
                rrd.update(netbox.netboxid, netbox.sysname, 'N', 'UP', rtt)
            else:
                # ugly...
                rrd.update(netbox.netboxid, netbox.sysname, 'N', 'DOWN', 5)

        downNow = []
        # Find out which netboxes to consider down
        for (netboxid, replies) in self.replies.items():
            if replies[:self._nrping] == [-1]*self._nrping:
                downNow.append(netboxid)
        
        debug.debug("No answer from %i hosts" %len(downNow),7)
        # Detect state changes since last run
        reportDown = filter(lambda x: x not in self.down, downNow)
        reportUp = filter(lambda x: x not in downNow, self.down)
        self.down = downNow

        # Reporting netboxes as down
        debug.debug("Starts reporting %i hosts as down" % len(reportDown),7)
        for netboxid in reportDown:
            netbox = self.netboxmap[netboxid]
            newEvent = Event(0,
                             netbox.netboxid,
                             netbox.deviceid,
                             Event.boxState,
                             "pping",
                             Event.DOWN
                             )
            self.db.newEvent(newEvent)
            debug.debug("%s marked as down." % netbox)
        # Reporting netboxes as up
        debug.debug("Starts reporting %i hosts as up" % len(reportUp),7)
        for netboxid in reportUp:
            try:
                netbox = self.netboxmap[netboxid]
            except:
                debug.debug("Netbox %s is no longer with us..." % netboxid)
                continue
            newEvent = Event(0,
                             netbox.netboxid,
                             netbox.deviceid,
                             Event.boxState,
                             "pping",
                             Event.UP
                             )
            self.db.newEvent(newEvent)
            debug.debug( "%s marked as up." % netbox)

    def main(self):
        """
        Loops until SIGTERM is caught.
        """
        self.db.start()
        while self._isrunning:
            start=time.time()
            debug.debug("Starts pinging....", 7)
            self.updateHostList()
            elapsedtime=self.pinger.ping()
            self.generateEvents()
            debug.debug("%i hosts checked in %03.3f secs. %i hosts "
                        "currently marked as down." %
                        (len(self.netboxmap), elapsedtime, len(self.down)))
            wait=self._looptime-elapsedtime
            if wait > 0:
                debug.debug("Sleeping %03.3f secs" % wait,6)
            else:
                wait=abs(self._looptime + wait)
                debug.debug("Check lasted longer than looptime. "
                            "Delaying next check for %03.3f secs" % wait,2)
            time.sleep(wait)

    def signalhandler(self, signum, frame):
        if signum == signal.SIGTERM:
            debug.debug("Caught SIGTERM. Exiting.",1)
            os.sys.exit(0)
        elif signum == signal.SIGHUP:
            # reopen the logfile
            logfile=self.config.get("logfile", "pping.log")
            debug.debug("Caught SIGHUP. Reopening logfile...")
            os.sys.stdout.close()
            os.sys.stderr.close()
            os.sys.stdout = open(logfile,'a')
            os.sys.stderr = open(logfile,'a')
            debug.debug("Reopened logfile: %s" % logfile)
        else:
            debug.debug( "Caught %s. Resuming operation." % (signum),2)




def help():
    print """Paralell pinger for NAV (Network Administration Visualized).
    
    Usage: %s [OPTIONS]
    -h  --help      Displays this message
    -n  --nofork    Run in foreground
    -v  --version   Display version and exit

    Written by Stian Søiland and Magnus Nordseth, 2002
    """  % os.path.basename(os.sys.argv[0])

def start(nofork):
    """
    Forks a new prosess, letting the service run as
    a daemon.
    """
    conf = config.pingconf()
    if not nofork:
        pid=os.fork()
        if pid > 0:
            os.sys.exit()

        # Decouple from parent environment
        os.chdir('/') # In case the cwd we started in is removed
        os.setsid()

        # Do second fork
        # (prevent us from accidentally reacquiring a controlling terminal)
        pid=os.fork()
        if pid > 0:
            os.sys.exit()

        # Close the underlying OS file descriptors
        os.sys.stdout.flush()
        os.sys.stderr.flush()
        os.close(os.sys.stdin.fileno())
        os.close(os.sys.stdout.fileno())
        os.close(os.sys.stderr.fileno())

        logfile = conf.get("logfile","pping.log")
        os.sys.stdout = open(logfile,"a")
        os.sys.stderr = open(logfile,"a")

    pidfilename = conf.get("pidfile","/usr/local/nav/var/run/pping.pid")
    pidfile = open(pidfilename, 'w')
    pidfile.write(str(os.getpid()))
    pidfile.close()

    myPinger=pinger(socket=sock)
    myPinger.main()


def setUser():
    conf = config.pingconf()
    username = conf.get("user", "nobody")
    try:
        uid = pwd.getpwnam(username)[2]
        gid = pwd.getpwnam(username)[3]
    except KeyError:
        print "User %s not found" % username
        print "Exiting"
        os.sys.exit()
    os.setegid(gid)
    os.seteuid(uid)
    # Make things read/write for user and group
    os.umask(0002)

if __name__=="__main__":
    nofork=0
    try:
        opts, args = getopt.getopt(os.sys.argv[1:],
                                   "hnv",
                                   ["help","nofork", "version"])
        for opt, val in opts:
            if opt in ("-h","--help"):
                help()
                os.sys.exit()
            elif opt in ("-n","--nofork"):
                nofork=1
            elif opt in ("-v","--version"):
                print __version__
                os.sys.exit(0)
                
    except (getopt.error):
        help()
        os.sys.exit(2)
    if os.getuid() != 0:
        print "Must be started as root"
        os.exit(0)
    sock = megaping.makeSocket()
    setUser()
    start(nofork)
