#!/usr/bin/perl -w
use strict;
use warnings;
use Data::Dumper;
use Config;
use JSON;
use LWP;
use URI;
use Win32::Daemon;
use File::Copy;

my $daemonName    = 'bit-monitoring';
my $dieNow        = 0;
my $version       = '$Revision: 198 $ $Date: 2015-07-24 15:57:56 +0200 (Fri, 24 Jul 2015) $ ' . $Config{osname};
my ($logMaxSize, $logFile, $pidFile, $logFilePath, $pidFilePath, $configFile, $pluginsFile, $lastStatus, $state);

Win32::Daemon::StartService();
while( SERVICE_START_PENDING != Win32::Daemon::State() ) {
    sleep( 1 );
}
Win32::Daemon::State( SERVICE_RUNNING );

$logMaxSize    = 16 * 1048576; # Rotate log every 16MB

$logFilePath   = 'c:\bit-monitoring-agent-perl\\';
$pidFilePath   = 'c:\bit-monitoring-agent-perl\\';
$configFile    = 'c:\bit-monitoring-agent-perl\agent.cfg';
$pluginsFile   = 'c:\bit-monitoring-agent-perl\plugins.cfg';
$logFile       = $logFilePath . $daemonName . ".log";
$pidFile       = $pidFilePath . $daemonName . ".pid";

$version =~ s/(?:\+.*|\$|Revision: |Date: |)//g;

chdir '/';
umask 022;

$|++;

my $config;
my $plugins;

if (-e $configFile) {
    $config = loadConfig($configFile);

    if ($$config{'reporting_hostname'} eq "") {
        my $hostname=`hostname --fqdn`;
        chomp($hostname);
        $$config{'reporting_hostname'} = $hostname;
    }
    if ($$config{'identity'} eq "") {
        $$config{'identity'} = $$config{'reporting_hostname'};
    }
    if ($$config{'proxy'} ) {
        $ENV{PERL_NET_HTTPS_SSL_SOCKET_CLASS} = "Net::SSL";
        $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
        $ENV{HTTPS_PROXY}    = $$config{'proxy'};
    }  
} else {
    logEntry("ERROR - Unable to load the configuration from ${configFile}!");
    exit;
}


if (-e $pluginsFile) {
    $plugins = loadConfig($pluginsFile);
} else {
    logEntry("ERROR - Unable to load the plugins from ${pluginsFile}!");
    exit;
}


if (!isdigit($$config{'alarm_report_interval'}) || $$config{'alarm_report_interval'} < 30) {
    logEntry("ERROR - Alarm reporting interval $$config{'alarm_report_interval'} does not make sense");
    exit;
}
if (!isdigit($$config{'normal_report_interval'}) || $$config{'normal_report_interval'} < 30) {
    logEntry("ERROR - Normal reporting interval $$config{'normal_report_interval'} does not make sense");
    exit;
}
if (keys %{$plugins} == 0) {
    logEntry("ERROR - No plugins configured, check the configuration file");
    exit;
}
if ($$config{'monitoring_enabled'} ne "true") {
    logEntry("ERROR - Monitoring is not enabled, refusing to start");
    exit;
}
if ($$config{'identity'} eq "") {
    logEntry("ERROR - identity invalid, hostname --fqdn returned empty or config not set. refusing to start");
    exit;
}


my $sleep;
my $alarms;
my $pluginalarms;
my $starttime;

logEntry("NOTICE - $daemonName is starting PID $$ version $version");
logEntry("NOTICE - $daemonName my hostname $$config{'reporting_hostname'} and identity $$config{'identity'}");

until ($dieNow) {
    $alarms = 0;
    $pluginalarms = 0;
    $starttime = time;

    logEntry("INFO - Starting normal run");

    # Then for each plugin we gather data and submit it
    while ((my $pluginName, my $pluginCommand) = each(%{$plugins})) {

        # Execute the plugin and gather results
        logEntry("INFO - Checking $pluginName with command $pluginCommand");

        my $pluginResult = 'UNKNOWN: No attempt to run the plugin was made at all';
        my $pluginExit = 3;

        my $retVal = eval {
            local $SIG{ALRM} = sub { die "sigalrm\n"; };
            alarm(60);

            $pluginResult = `$pluginCommand`;
            chop($pluginResult);
            $pluginExit = $? >> 8;

            alarm(0);
        };

        if ($@) {
            if ($@ =~ m#sigalrm#) {
                $pluginResult = 'UNKNOWN: Plugin did not return data after 60 seconds';
                $pluginExit = '3';
            } else {
                $pluginResult = 'UNKNOWN: Unexpected state from Plugin';
                $pluginExit = '3';
            }
        }

        if ($pluginExit != 0) {
            if ($pluginExit == -1) {
                $pluginResult = "Plugin not found or not executable";
            }
            logEntry("WARN - Failed $pluginName exit code $pluginExit result: $pluginResult");
            $pluginalarms++;
        } else {
            logEntry("INFO - Completed $pluginName exit code $pluginExit result: $pluginResult");
        }

        logEntry("INFO - Submitting checkresults for $pluginName to $$config{'destination_server'}");
        my $result = doJSON("SubmitCheckResult",
            {
                "command"       => "PROCESS_SERVICE_CHECK_RESULT",
                "host_name"     => $$config{'reporting_hostname'},
                "service_name"  => $pluginName,
                "status_code"   => $pluginExit,
                "plugin_output" => "AGENT ${pluginResult}",
            }
        );

        if (ref($result) ne 'HASH') {
            logEntry("ERROR - JSON: $result");
            $alarms++;

        } elsif ($$result{'result'}{'error'} ne "null") {
            logEntry("ERROR - JSON: $$result{'result'}{'error'}");
            $alarms++;

        } else {
            if ($$result{'result'}{'data'}{'code'} != "200") {
                logEntry("$$result{'result'}{'data'}{'code'} $$result{'result'}{'data'}{'status'} - $$result{'result'}{'data'}{'message'}");
                $alarms++;

            } else {
                logEntry("INFO - Completed plugin ${pluginName} reporting");

            }
        }        

    }
    logEntry("INFO - Completed normal run");

    # Reporting Agent Status
    logEntry("INFO - Submitting AGENT_STATUS to $$config{'destination_server'}");
    my $result = doJSON("SubmitCheckResult",
        {
        "command"       => "PROCESS_SERVICE_CHECK_RESULT",
        "host_name"     => $$config{'reporting_hostname'},
        "service_name"  => "AGENT_STATUS",
        "status_code"   => ($alarms > 0 ? '1' : '0'),
        "plugin_output" => "AGENT has $alarms alarms and running version $version",
        }
    );

    if (ref($result) ne 'HASH') {
        logEntry("ERROR - JSON: $result");
        $alarms++;

    }elsif ($$result{'result'}{'error'} ne "null") {
        logEntry("ERROR - JSON: $$result{'result'}{'error'}");
        $alarms++;

    } else {
        if ($$result{'result'}{'data'}{'code'} != "200") {
        logEntry("$$result{'result'}{'data'}{'code'} $$result{'result'}{'data'}{'status'} - $$result{'result'}{'data'}{'message'}");
        $alarms++;

        } else {
        logEntry("INFO - Completed agent status reporting");

        }
    }

    # Interval regulation - where there are problems we're checking faster
    $alarms = $alarms + $pluginalarms;
    if($alarms > 0) {
        $lastStatus = 1;
        $sleep = $$config{'alarm_report_interval'};
        logEntry("INFO - Got $alarms alarms so scheduling a fast run next");

    } else {
        $lastStatus = 0;

        $sleep = $$config{'normal_report_interval'} - (time - $starttime);

    }

    logEntry("INFO - Sleeping for $sleep seconds until next run");
    for (my $i = 0; $i < $sleep; $i++) {
        $state = Win32::Daemon::State();
        if (SERVICE_STOP_PENDING == $state) {
            Win32::Daemon::State( SERVICE_STOPPED );
            exit;
        }
        sleep( 1 );
    }
}



sub logEntry {
    my ($logText) = @_;
    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time);
    my $dateTime = sprintf "%4d-%02d-%02d %02d:%02d:%02d", $year + 1900, $mon + 1, $mday, $hour, $min, $sec;
    chomp($logText);

    my $logSize = (stat($logFile))[7];
    if ($logSize > $logMaxSize) {
        unlink "$logFile.1";
        move("$logFile", "$logFile.1"); # File::Copy::move
    }
    
    open LOG, ">>", $logFile or die "Can't write $logFile: $!\n";
    print LOG "$dateTime $logText\n";
    close LOG;
}


sub signalHandler {
    logEntry("NOTICE - $daemonName is shutting down");
    $dieNow = 1;
}


sub loadConfig {
    my ($file) = @_;

    my $blob = "";
    if (open(FD, "<".$file)) {
        local $/ = undef; $blob = <FD>; close(FD);
    } else {
        die "Can't read '$file': $!\n";
    }
    my $obj = eval 'my ' . $blob;
    return $obj;
}


sub doJSON {
    my ($method, $params) = @_;

    my $blob = {
        type => $$params{'command'} eq 'PROCESS_HOST_CHECK_RESULT' ? 'Host' : 'Service',
        exit_status => $$params{'status_code'},
        plugin_output => $$params{'plugin_output'},
        filter => 'host.name=="' . $$params{'host_name'} . '" && service.name=="' . $$params{'service_name'} . '"'
    };

    my $retVal = eval {
        local $SIG{ALRM} = sub { die "sigalrm\n"; };
        alarm(60);

        my $ua = LWP::UserAgent->new; $ua->agent("lwp/42 ");
        my $url = URI->new($$config{'destination_server'});
        $ua->credentials($url->host().":".$url->port(),"Icinga 2",$$config{identity},$$config{password});
        $ua->default_header('Accept', 'application/json');
        $ua->default_header('X-HTTP-Method-Override', 'POST');
        my $req = HTTP::Request->new(POST => $$config{'destination_server'} . "/v1/actions/process-check-result");
        $req->content_type('application/json');
        $req->content(encodeJSON($blob));

        my $res = $ua->request($req);
        if (not $res->is_success) {
            alarm(0);
            return { 'result' => { 'error' => "request failed: " . $res->status_line } };
        } else {
            my $val = $res->content();
            alarm(0);

            return { 'result' => { 'error' => "Data returned by server does not look like JSON: --$val--" } }
                if ($val !~ m#^{.*}$#);

            my $obj = decodeJSON($val);
            return { 'result' => { 'error' => "Decoded JSON is not of type HASH: --$val--" } }
                if (ref($obj) ne 'HASH');

            if (@{$$obj{'results'}}[0]->{'code'} != 200) {
                return { 'result' => { 'error' => "JSONRPC Error " . @{$$obj{'results'}}[0]->{'code'} . ":" . @{$$obj{'results'}}[0]->{'status'} } };
            }
            
            return {
                'result' => {
                    'error' => 'null',
                    'data' => {
                        'code' => @{$$obj{'results'}}[0]->{'code'},
                        'status' => @{$$obj{'results'}}[0]->{'code'},
                        'message' => @{$$obj{'results'}}[0]->{'status'},
                    }
                }
            }
        }
    };

    if ($@) {
        if ($@ =~ m#sigalrm#) {
            return { 'result' => { 'error' => "JSONRPC Error - Timeout in bit-monitoring for LWP request!" } };
        } else {
            return { 'result' => { 'error' => "JSONRPC Error - Unexpected state in bit-monitoring: '$@'" } };
        }
    }
    return $retVal;
}


sub encodeJSON {
    my ($blob) = @_;

    my $jsonblob = "";
    eval { $jsonblob = encode_json($blob) };
    if ($@ =~ m#Undefined subroutine.*encode_json called#) {
        eval { $jsonblob = objToJson($blob) };
        return $@ if ($@ ne "");
    } elsif ($@ ne "") {
        return $@;
    }
    return "encodeJSON failed." if ($jsonblob eq "");
    return $jsonblob;
}


sub decodeJSON {
    my ($jsonblob) = @_;

    my $blob = "";
    eval { $blob = decode_json($jsonblob) };
    if ($@ =~ m#Undefined subroutine.*code_json called#) {
        eval { $blob = objFromJson($jsonblob) };
        return $@ if ($@ ne "");
    } elsif ($@ ne "") {
        return $@;
    }
    return "decodeJSON failed." if ($blob eq "");
    return $blob;
}


sub isdigit {
    my ($digit) = @_;
    return 1 if $digit =~ m#^\d+$#;
    return 0;
}
