#!/usr/bin/perl

use strict;

use LWP::UserAgent;
use MIME::Parser;
use NOCpulse::Config;
use NOCpulse::SatCluster;
use NOCpulse::Gritch;
use NOCpulse::NPRecords;
##use NOCpulse::PlugFrame::ProbeGenerator;
use NOCpulse::Scheduler::Event::ProbeEvent;
use NOCpulse::Probe::Config::Command;
use NOCpulse::Probe::ModuleMap;
use NOCpulse::SetID;
use NOCpulse::Utils::XML;
use NOCpulse::CommandLineApplicationComponent;
use FreezeThaw qw(freeze);
use Digest::MD5 qw(md5_hex);
use Getopt::Long;
use POSIX qw(ceil);
use GDBM_File;
use Storable;

use constant STATUS_OK          => 0;
use constant STATUS_WARNING     => 1;
use constant STATUS_ERROR       => 2;
use constant STATUS_FATAL_ERROR => 3;

use constant NUM_PARTS => 8;

# Keep in synch with SatConfig/ConfigDocument
use constant CONFIG_DOC_VERSION => '1.1';

my $PluginOutput = '';

my $nsId;

sub untaint {
  my $tainted = shift;

  $tainted =~ /(.*)/ && return $1;

}

sub errorOut {
  leave(STATUS_ERROR, @_);
}

sub fatalErrorOut {
  leave(STATUS_FATAL_ERROR, @_);
}

sub leave {
  my ($error, @messages) = @_;

  # Finish off the XML when bailing out or completing a successful run.
  my $message .= join("\n", @messages);

  if ($error) {
    my $soapbox = new NOCpulse::Gritch("/var/lib/nocpulse/scheduleEvents.db");
    $soapbox->gritch("scheduleEvents problem on satellite $nsId", $message);
  }

  addStepMessage($message);
  endConfigStep($error);
  endConfig();

  print getXML();

  exit(getOverallStatus());
} ## end sub leave

# Create LongLegs config file for a probe.
sub writeLongLegsConfig {
  my ($config, $probeRecord) = @_;

  my $llcfgdir = $config->val('netsaint', 'llConfigDir');

  if (!-d $llcfgdir) {
    mkdir($llcfgdir, 0755)
      or die "Couldn't create LL config dir $llcfgdir: $!\n";
  }

  if (-d $llcfgdir) {
    my $llprobeid = $probeRecord->get_RECID;
    if (open(FILE, ">$llcfgdir/$llprobeid")) {
      print FILE $probeRecord->get('llconfig');
      close(FILE);
    } else {
      die "Couldn't create LL config file $llcfgdir/$llprobeid: $!\n";
    }
  }
} ## end sub writeLongLegsConfig

sub createTrapReceiverConfig {
  my ($probeRecord, $trapReceiverCfgRef) = @_;

  # This is an SNMP trap receiver probe.  Add its config to
  # %trapReceiverCfg
  my $probeid = $probeRecord->get_RECID;
  my $addr    = $probeRecord->get_hostAddress;
  my %args    = %{ $probeRecord->get_parsedCommandLine };

  # Convert oid specs to regular expressions and save
  my ($ttype, $oidspec);
  foreach $ttype (qw(crit warn)) {
    $oidspec = $args{ $ttype . '_oids' };
    if ($oidspec =~ m,^/,) {

      # $oidspec is a Perl5 regular expression
      $oidspec =~ s,^/|/$,,g;
    } else {

      # $oidspec is a pseudo-glob pattern
      $oidspec =~ s/\./\\./g;
      $oidspec =~ s/^\*/.STAR/;
      $oidspec =~ s/\*$/.STAR/;
      $oidspec =~ s/\*/[0-9]*/g;
      $oidspec =~ s/STAR/*/g;
      $oidspec = '^\.' . $oidspec . "\$";
    } ## end else [ if ($oidspec =~ m,^/,)
    push(@{ $trapReceiverCfgRef->{$addr}->{$oidspec} }, [ $probeid, $ttype ]);
  } ## end foreach $ttype (qw(crit warn))
} ## end sub createTrapReceiverConfig

#
# XML output management
#
{
  my $xmlDocument     = '';
  my $xmlStepElements = '';
  my $xmlStepMessage  = '';
  my $overallStatus   = STATUS_OK;
  my @statusName      = ("ok", "warning", "error", "fatalError");

  sub getXML {
    return $xmlDocument;
  }

  sub getOverallStatus {
    return $overallStatus;
  }

  sub startConfig {
    my ($satClusterId) = @_;
    $xmlDocument .= '<?xml version="1.0"?>' . "\n";
    $xmlDocument .=
        '<sat_config id="'
      . $satClusterId
      . '" version="'
      . CONFIG_DOC_VERSION . '">' . "\n";
  }

# Current step names:
# requestConfigData, loadCommands, buildProbeRecordDB, buildProbeDB,
# generateEvents, copyProbeDB, schedulerReload, replicateHAConfig, purgeOldState
# The checkUser step can appear if there's a problem with the nocpulse account.
  sub startConfigStep {
    my ($name) = @_;
    $xmlDocument .= '<step name="' . $name . '">' . "\n";
    $xmlStepElements = '';
    $xmlStepMessage  = '';
  }

  sub addStepElement {
    $xmlStepElements .= join("\n", @_);
  }

  sub addStepMessage {
    $xmlStepMessage .= join("\n", @_);
  }

  sub addStepStatus {
    my ($status) = @_;
    $xmlStepElements .= getStatusTag($status);
  }

  sub getStatusTag {
    my ($status) = @_;

    # Assign overall run status based on step status.
    if ($status == STATUS_FATAL_ERROR) {
      $overallStatus = STATUS_ERROR;

    } elsif ($overallStatus != STATUS_FATAL_ERROR && $status != STATUS_OK) {

      # Individual step failure does not mean the whole run has failed.
      $overallStatus = STATUS_WARNING;
    }
    return "<status>" . $statusName[$status] . "</status>\n";
  } ## end sub getStatusTag

  sub endConfigStep {
    my ($status) = @_;

    $xmlDocument .= getStatusTag($status);

    if ($xmlStepMessage) {
      $xmlDocument .= "<message>" . $xmlStepMessage . "</message>\n";
    }
    $xmlDocument .= $xmlStepElements;
    $xmlDocument .= "</step>\n";
  } ## end sub endConfigStep

  sub endConfig {
    $xmlDocument .= "<status>" . $statusName[$overallStatus] . "</status>\n";
    $xmlDocument .= "</sat_config>\n";
  }
}

sub saveStorable {
  my ($ref, $file) = @_;
  my $tempFile = $file . 'NEW';
  Storable::store $ref, $tempFile or die "Couldn't open $tempFile: $!";
  unlink($file);
  rename($tempFile, $file) or die "Couldn't rename $tempFile to $file: $!";
}

################################# BEGIN MAINLINE ########################################

my $requiredUser = 'nocpulse';
if ($requiredUser) {
  my $wrongUser = 0;
  my $noUser    = 0;

  if ($< == 0) {
    if (getpwnam($requiredUser) > 0) {
      NOCpulse::SetID->new(user => $requiredUser)->su(permanent => 1);
    } else {
      $noUser = 1;
    }
  } elsif (!(getpwuid($<) eq $requiredUser)) {
    $wrongUser = 1;
  }
  if ($noUser || $wrongUser) {
    startConfig(-1);
    startConfigStep('checkUser');
    if ($wrongUser) {
      fatalErrorOut(
                  "$0 must be run as user $requiredUser, but you are currently "
                    . getpwuid($<));
    } else {
      fatalErrorOut("No 'nocpulse' user found on this satellite.");
    }
  } ## end if ($noUser || $wrongUser)
} ## end if ($requiredUser)

# Process options
my $longlegs;
GetOptions("longlegs+" => \$longlegs);

my $config = NOCpulse::Config->new;
if ($longlegs) {

  # Cron invocation -- only generate the config if we're on a scout
  my $llfile = $config->get('netsaint', 'llnetsaintFile');
  exit(0) unless (-f $llfile);
}

my $ua = new LWP::UserAgent;
$ua->agent("NPSatelliteConfigGrabber/3.0 " . $ua->agent);
$ua->timeout(600);

my $cluster = NOCpulse::SatCluster->newInitialized($config);
startConfig($cluster->get_id);

startConfigStep('requestConfigData');

my $url =
    $config->get('satellite', 'configGrabUrl')
  . '?satcluster='
  . $cluster->get_id;
my $req = new HTTP::Request GET => $url;
my $res = $ua->request($req);
my ($checksums, $version, $netsaintCfg, $probeDB, $commandParamDB,
    $commandMetricDB);
my $fetchStatus = STATUS_OK;

if ($res->is_success) {
  my $parser = MIME::Parser->new;
  $parser->output_to_core(1);

# Strip the leading HTTP, if it exists
# CGI.pm has changed since perl 5.6, headers are different
  my $content = $res->content;
  $content =~ s/^HTTP.*\n//m;

  my $stuff = $parser->parse_data($content);

  if ($stuff->parts != NUM_PARTS) {
    if ($stuff->parts > 0) {
      fatalErrorOut(
                    "Incomplete configuration received from $url: Expected "
                      . NUM_PARTS
                      . " sections but received "
                      . scalar($stuff->parts)
                      . " sections",
                    $stuff->parts(0)->bodyhandle->as_string,
                    "\n"
                   );
    } ## end if ($stuff->parts > 0)
    fatalErrorOut("Empty configuration received from $url");
  } else {
    my $fetchMsg = $stuff->parts(0)->bodyhandle->as_string;
    addStepMessage($fetchMsg);
    if ($fetchMsg =~ 'ERROR') {
      $fetchStatus = STATUS_WARNING;
    }
    $checksums       = $stuff->parts(1)->bodyhandle->as_string;
    $version         = $stuff->parts(2)->bodyhandle->as_string;
    $netsaintCfg     = $stuff->parts(3)->bodyhandle->as_string;
    $probeDB         = $stuff->parts(4)->bodyhandle->as_string;
    $commandParamDB  = $stuff->parts(5)->bodyhandle->as_string;
    $commandMetricDB = $stuff->parts(6)->bodyhandle->as_string;

    # Verify we got everything
    if (!$version || $version ne CONFIG_DOC_VERSION) {
      fatalErrorOut(  "Wrong configuration document version: Expecting '"
                    . CONFIG_DOC_VERSION
                    . "' but received '$version' from $url");
    }
    if (!$netsaintCfg) {
      fatalErrorOut("No satellite configuration received from $url");
    } elsif (!$probeDB) {
      fatalErrorOut("No probe definitions received from $url");
    } elsif (!$commandParamDB) {
      fatalErrorOut("No command parameter definitions received from $url");
    } elsif (!$commandMetricDB) {
      fatalErrorOut("No command metric definitions received from $url");
    }

    # Verify checksums
    my @checksums = split(/:/, $checksums);
    if ($checksums[0] ne md5_hex($netsaintCfg)) {
      fatalErrorOut('Satellite configuration failed MD5 check');
    } elsif ($checksums[1] ne md5_hex($probeDB)) {
      fatalErrorOut('Probe DB failed MD5 check');
    } elsif ($checksums[2] ne md5_hex($commandParamDB)) {
      fatalErrorOut('Command param DB failed MD5 check');
    } elsif ($checksums[3] ne md5_hex($commandMetricDB)) {
      fatalErrorOut('Command metric DB failed MD5 check');
    }
  } ## end else [ if ($stuff->parts != NUM_PARTS)
} else {
  fatalErrorOut("Request for configuration data from URL '$url' failed: ",
                $res->status_line);
}

endConfigStep($fetchStatus);

startConfigStep('loadCommandParameters');

my $cmdParams    = NOCpulse::Utils::XML->unserialize($commandParamDB);
my $cmdMetrics   = NOCpulse::Utils::XML->unserialize($commandMetricDB);
my $cmdParamFile = $config->get('netsaint', 'commandParameterDatabase');

eval { saveStorable([ $cmdParams, $cmdMetrics ], $cmdParamFile); };
if ($@) {
  addStepStatus(STATUS_ERROR);
  addStepMessage("Failed to save command parameter database: $@\n");
  endConfigStep(STATUS_ERROR);
} else {
  endConfigStep(STATUS_OK);
}
NOCpulse::Probe::Config::Command->load($cmdParamFile);

# Probe database records go in their own file.
startConfigStep('buildProbeRecordDB');

my @events;
$NOCpulse::CommandLineApplicationComponent::OutputTarget = \$PluginOutput;

# Load probe records from XML, for later use and to save with Storable.
my $probeListRef = ProbeRecord->LoadFromXML($probeDB, 'RECID');

my %probeRecordHash = ();
foreach my $probeHashRef (@{$probeListRef}) {
  $probeRecordHash{ $probeHashRef->{'RECID'} } = $probeHashRef;
}
my $probeRecordFile = $config->get('netsaint', 'probeRecordDatabase');
eval { saveStorable(\%probeRecordHash, $probeRecordFile); };
if ($@) {
  addStepStatus(STATUS_ERROR);
  addStepMessage("Failed to save probe record database: $@\n");
  endConfigStep(STATUS_ERROR);
} else {
  endConfigStep(STATUS_OK);
}

# Now do the DB with frozen Probe instances.
startConfigStep('buildProbeDB');

# Next bit sets up building Probe.db in a temp dir
##NOCpulse::Object::SystemIni($config->get('PlugFrame','configFile'));
##Probe->setClassVar('databaseDirectory',$NOCpulse::Object::config->val('scheduleEvents','tempDatabaseDirectory'));
##my $tempDatabaseFilename=Probe->databaseFilename();
my %validProbeIds;      # List of probes that ARE installed.
my %trapReceiverCfg;    # SNMP trap receiver configuration
my $trapReceiverCmdId = # Recid of the SNMP Trap Receiver command
  $config->get('trapReceiver', 'cmd_recid');

ProbeRecord->Map(
  sub {
    $PluginOutput = '';
    my $probeRecord = shift();
    my $evalOutput;
    my $error   = 0;
    my $probeId = $probeRecord->get_RECID;
    $validProbeIds{$probeId} = undef;

    my $commandId     = $probeRecord->get_CHECK_COMMAND;
    my $commandRecord = NOCpulse::Probe::Config::Command->instances($commandId);
    if ($commandRecord) {

      # Check for module name, including mapping via etc/probe_module_map.ini
      my $orig_module = $commandRecord->command_class;
      my $module      =
        NOCpulse::Probe::ModuleMap->instance->module_for($orig_module);

      if ($module =~ /::/) {

        # New probe framework probe
        my $event = NOCpulse::Scheduler::Event::ProbeEvent->new($probeId);
        my $check_seconds = $probeRecord->get_CHECK_INTERVAL * 60;
        $event->time_to_execute(time() + ceil(rand($check_seconds)));
        $event->perl_module($module);
        push(@events, $event);

        if ($probeRecord->get_PROBE_TYPE() eq 'LongLegs') {

          # Long legs needs separate config
          writeLongLegsConfig($config, $probeRecord);

        } elsif ($commandRecord->command_id == $trapReceiverCmdId) {

          # Trap receiver needs separate config
          createTrapReceiverConfig($probeRecord, \%trapReceiverCfg);
        }

        if ($orig_module eq $module) {

          # Locally overridden probes are configured as old and
          # new so that they can be run side-by-side. This is
          # really a new one, so we're done with it.
          return;
        }
      } ## end if ($module =~ /::/)
    } ## end if ($commandRecord)

    # Existing plugin framework probe
##		if (! eval {
##		   my $plugin = ProbeGenerator->newInitialized($probeRecord)->createProbe();
##		   if ($plugin->get_isValid) {
##		      my $event = $plugin->asInitialEvent;
##		      push(@events, $event);
##		   } else {
##		      $error = 1;
##		   }
##		}
##		   ) {
##		   $error = 1;
##		   $evalOutput = $@;
##		}
##		if ($error) {
##		   my $probeTag = '<probe ';
##		   $probeTag .= 'id="'.$probeRecord->get_RECID.'" ';
##                   my $type = $probeRecord->get_PROBE_TYPE;
##                   $type = 'check' if $type eq 'ServiceProbe';
##                   $type = 'host'  if $type eq 'HostProbe';
##                   $type = 'url'   if $type eq 'LongLegs';
##		   $probeTag .= "type=\"$type\"";
##		   $probeTag .= ">\n";
##		   addStepElement($probeTag);
##
    # Plugin output is itself an XML string describing the problem.
##		   addStepElement($PluginOutput);

    # This is other error output from the eval. For now
    # truncate it to avoid breaking the 4K maximum size;
    # this will be fixed when stdout can be arbitrarily long.
##		   if ($evalOutput) {
##		      $evalOutput = substr($evalOutput, 0, 250);
##		      addStepElement("<output>".$evalOutput."</output>\n");
    } ## end sub
##		   addStepStatus(STATUS_ERROR);
##                   addStepElement("</probe>\n");
##		}
##	     }
);
##Probe->database->closeCachedHandle;

# Save off the trap receiver config
my $trapReceiverCfgFile = $config->get('trapReceiver', 'config');
eval {
  open(SNMPC, ">$trapReceiverCfgFile.NEW")
    or die "Couldn't open $trapReceiverCfgFile.NEW: $!";
  print SNMPC NOCpulse::Utils::XML->serialize(\%trapReceiverCfg);
  close(SNMPC);
  unlink($trapReceiverCfgFile);
  rename("$trapReceiverCfgFile.NEW", $trapReceiverCfgFile)
    or die
    "Couldn't rename $trapReceiverCfgFile.NEW -> $trapReceiverCfgFile: $!";
};
if ($@) {

  addStepStatus(STATUS_ERROR);
  addStepMessage("Failed to save SNMP trap receiver config: $@\n");
  endConfigStep(STATUS_ERROR);

} else {

  endConfigStep(STATUS_OK);
}

startConfigStep("generateEvents");

addStepMessage("Generated " . scalar(@events) . " events on satellite");
my $schedulerEvents = freeze(@events);

open(FILE, '>' . $config->get('satellite', 'eventsFile'))
  || errorOut('Unable to open events file');
print FILE $schedulerEvents;
close(FILE);
open(FILE, '>' . $config->get('satellite', 'schedulerConfigFile'))
  || errorOut('Unable to open scheduler config file');
print FILE $netsaintCfg;
close(FILE);

endConfigStep(STATUS_OK);

##startConfigStep("copyProbeDB");

##my $targetDatabaseDir=$NOCpulse::Object::config->val('Probe','databaseDirectory');

# Now copy the temp Probe.db into place. If this satellite has only new-style
# probes, this won't exist.
##$tempDatabaseFilename.= '.'.$NOCpulse::Object::config->val('scheduleEvents','databaseFilenameExtension');
##if ( -f $tempDatabaseFilename) {
##    my $moveOutput = `mv $tempDatabaseFilename $targetDatabaseDir 2>&1`;
##    if ($? >> 8) {
##	fatalErrorOut("Probe database installation failed ($moveOutput)\n");
##    }
##}

##endConfigStep(STATUS_OK);

# Notify the scheduler that it should pick up the new stuff.
startConfigStep("schedulerReload");
open(FILE, '>' . $config->get('satellite', 'schedulerReloadFlagFile'))
  || errorOut('Unable to open schedule reload flag');
print FILE "Nowish would be just fine\n";
close(FILE);

addStepMessage("Scheduler reloaded \n");
endConfigStep(STATUS_OK);

##Probe->setClassVar('databaseDirectory',undef);
$cluster->refreshHAView;
if ($cluster->get_isHA) {
  if ($cluster->get_currentNode->get_isLead) {

    # (It really shouldn't be possible for this script
    # to be called when this isn't true (if we're in an HA cluster))
    startConfigStep("replicateHAConfig");
    ##my $probeDatabaseFilename = Probe->databaseFilename.'.'.$NOCpulse::Object::config->val('scheduleEvents','databaseFilenameExtension');
    my $probeDatabaseFilename = 'foo';
    my @results;
    eval {
      local $SIG{'ALRM'} = sub { die "Mirroring timeout\n" };
      alarm($config->get('netsaint', 'mirrorTimeout'));
      push(@results, $cluster->distributeFile($probeDatabaseFilename))
        if (-f $probeDatabaseFilename);
      push(@results,
           $cluster->distributeFile($config->get('satellite', 'eventsFile')));
      push(
           @results,
           $cluster->distributeFile(
                                $config->get('satellite', 'schedulerConfigFile')
           )
          );
      push(
           @results,
           $cluster->distributeFile(
                            $config->get('netsaint', 'commandParameterDatabase')
           )
          );
      push(
           @results,
           $cluster->distributeFile(
                                 $config->get('netsaint', 'probeRecordDatabase')
           )
          );
      push(
           @results,
           $cluster->runCommand(
                              '/usr/bin/validateCurrentStateFiles.pl')
          );
      alarm(0);
    };
    if ($@) {
      addStepMessage("Timeout mirroring files");
      endConfigStep(STATUS_ERROR);
    } else {
      my $hadErrors = 0;
      map {
        if ($_->get_hadProblem) { $hadErrors = 1 }
      } @results;
      if ($hadErrors) {
        addStepMessage("Problems encountered trying to mirror files:\n");
        map {
          if ($_->get_hadProblem) { addStepMessage($_->asString) }
        } @results;
        endConfigStep(STATUS_ERROR);
      } else {
        addStepMessage("All files mirrored successfully");
        endConfigStep(STATUS_OK);
      }
    } ## end else [ if ($@)
  } ## end if ($cluster->get_currentNode...
} ## end if ($cluster->get_isHA)

##startConfigStep("purgeOldState");

# NOTE: This code lives in /usr/bin/validateCurrentStateFiles.pl as well.
# Purge old current state and probe state files.
##my $inputDbm = $targetDatabaseDir.'/Probe.db'; # WARNING - HARD EXTENSION HERE!
##my $tries = 0;
##my $maxtries = 500;
##
##my %database;
##print "input dbm is $inputDbm\n";
##while (!  tie(%database, 'GDBM_File', $inputDbm, &GDBM_WRCREAT, 0640)) {
##        if ("$!" ne "Resource temporarily unavailable") {
##                $tries = $tries + 1;
##                if ($tries >= $maxtries) {
##                        errorOut("More than $maxtries attempts to open $inputDbm: $!");
##                }
##        }
##        sleep(1);
##}
##my $rmcount = 0;
##my $rmOldStyleCount = 0;
##my $oldId;
##my $curStateDir = '/var/lib/nocpulse/ProbeState';
##print "dir is $curStateDir\n";
##my %dir;
##tie %dir, "IO::Dir", $curStateDir;
##my @oldProbeIds = grep(s/(?:ProbeState|state).(\d+)/$1/,keys(%dir));
##untie %dir;
##foreach $oldId (@oldProbeIds) {
##	print "Checking $oldId\n";
##my $probeStateFile = "$curStateDir/ProbeState.$oldId";
##	my $stateFile = "$curStateDir/state.$oldId";
##        unless (defined(ProbeRecord->Called($oldId))) {
##                # Have to nuke old files
##		print "Removing $oldId\n";
##                if (unlink($probeStateFile) == 0) {
##                    unlink($stateFile);
##                }
##                $rmcount ++;
##
##	} elsif (-r $stateFile && -r $probeStateFile) {
# If the probe has both new and old style state files,
# delete the old one.
##		unlink($probeStateFile);
##                $rmOldStyleCount ++;
##        }
##}
##if ($rmcount) {
##        addStepMessage("Removed $rmcount obsolete probe state(s)\n");
##} else {
##	addStepMessage("No probes were deleted this run\n");
##}
##if ($rmOldStyleCount) {
##        addStepMessage("Removed $rmOldStyleCount old-style probe state file(s)\n");
##}
##untie(%database);

leave(STATUS_OK);
