#!/usr/bin/perl -w
# vim:et:ts=2:sw=2:

#*********************************************************************
#
# fai-monitor -- monitor daemon which collects client status info
#
# This script is part of FAI (Fully Automatic Installation)
# (c) 2003-2015 by Thomas Lange, lange@cs.uni-koeln.de
# Universitaet zu Koeln
#
#*********************************************************************

use strict;
use Socket;
use Getopt::Std;
use POSIX qw(strftime);

$| = 1;
my ($port, $timeout, $daemon, $timestamp, $timestamp_format, $localtime);
my $pidfile = '/var/run/fai-monitor.pid';
my $logfile = '-';
my $daemonlogfile = '/var/log/fai-monitor.log';
my $useip;

our %variables;

our ($opt_b,$opt_h,$opt_p,$opt_l,$opt_t,$opt_d,$opt_P,$opt_T,$opt_i,$opt_Z,$opt_f);
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub logline(@) {
  open(LOGFILE, $logfile) or return 0;

  if ($timestamp) {
    if ($localtime) {
      print LOGFILE (strftime "$timestamp_format - ", gmtime());
    } else {
      print LOGFILE (strftime "$timestamp_format - ", localtime());
    }
  }
  print LOGFILE @_ or return 0;
  close(LOGFILE);
  return 1;
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub signal_die(@) {
  logline(@_);
  unlink($pidfile);
  exit(1);
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub signal_warn(@) {
  logline(@_) or die "log: $!";
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub signal_deadly(@) {
  # Use the die-handler
  signal_die('Caught deadly signal ' . shift() . "\n");
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub server_init() {
  logline("FAI monitoring daemon starting..\n") or die "log: $!";

  # Init signals
  $SIG{INT} = \&signal_deadly;
  $SIG{QUIT} = \&signal_deadly;
  $SIG{TERM} = \&signal_deadly;
  $SIG{__DIE__} = \&signal_die;
  $SIG{__WARN__} = \&signal_warn;
  # HUP is usually used to reopen log files. This is not a problem
  # in this design.
  $SIG{HUP} = 'IGNORE';

  if ($daemon) {
    if (-e $pidfile) {
    # Pid file already exists. Check if it's a valid pid.
      open(PIDFILE, '<', "$pidfile") or die "open $pidfile: $!";
      my $pid = <PIDFILE>;
      chomp($pid);
      if ($pid ne '') {
      # Kill -0 exits with value 0 if pid is alive
        system("kill -0 $pid 2> /dev/null");
        if ($? == 0) {
          logline("Pidfile $pidfile exists and contains an existing pid. Exiting.\n");
          exit(1);
        }
      }
      close(PIDFILE);
    }
    eval "Proc::Daemon::Init";
    umask 022;

    open(PIDFILE, '>', "$pidfile") or die "open $pidfile: $!";
    print PIDFILE $$ or die "print $pidfile: $!";
    close(PIDFILE);
  }

  # Listen
  my $proto = getprotobyname('tcp');
  socket(SERVER, PF_INET, SOCK_STREAM, $proto) or die "socket: $!";
  setsockopt(SERVER, SOL_SOCKET, SO_REUSEADDR, 1) or die "setsockopt: $!";

  my $paddr = sockaddr_in($port, INADDR_ANY);

  bind(SERVER, $paddr) or die "bind: $!";
  listen(SERVER, SOMAXCONN) or die "listen: $!";
  logline("FAI monitoring daemon started on port $port with pid $$\n") or die "log: $!";
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub big_loop() {

  # accept a connection, print message received and close
  my ($client_addr);
  while ($client_addr = accept(CLIENT, SERVER)) {
    my ($port, $iaddr) = sockaddr_in($client_addr);
    my $ip = inet_ntoa($iaddr);

    my $inp = '';

    eval {
      local $SIG{__DIE__};
      local $SIG{__WARN__};
      local $SIG{'ALRM'} = sub { die("Timeout"); };

      alarm($timeout);
      $inp = <CLIENT>;
      alarm(0);
      defined($inp) && $inp =~ /^VARIABLE / && reply_to_client($inp)
    };

    close CLIENT;

    if (!defined($inp) || $inp eq '') {
      # Client did not send anything, or alarm went off
      logline("$ip:$port: No data or timeout.\n") or die "log: $!";
      next;
    }

    if ($inp =~ /^([^\s;]+)\s+TASKEND install 0/ && $opt_b) {
      my $cname = $1;
      if ($useip) {
        $cname = $ip;
      }
      system('fai-chboot', '-d', $cname);
      logline("$ip:$port: Disabling pxelinux configuration for $cname\n") or die "log: $!";
    }
    logline("$inp") or die "log: $!";
  }
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub read_list_of_variables {

  # read a file which contains a list of variables
  # format:  key=value
  # value may contain blanks
  # generate a hash

  my $varfile = "/var/log/fai/variables";
  return unless -f $varfile;

  open(VARFILE, '<', "$varfile") or die "open $varfile: $!";
  while (<VARFILE>) {
    next if /^#/;
    if (/^(\w+)=(.*)/) {
      $variables{$1} = $2;
    }
  }
  close(VARFILE);
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub reply_to_client {

  my $name = shift;

  chomp $name;
  $name =~ s/^VARIABLE //;

  unless (exists $variables{$name}) {
    print CLIENT "UNKNOWN $name\n";
    return;
  }
  print CLIENT "OK $variables{$name}\n";
  return;
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub usage {

  print << "EOF";
fai-monitor, FAI monitor daemon.

    Copyright (C) 2003-2015 by Thomas Lange

Usage: fai-monitor [OPTIONS]

    -b                  Call fai-chboot to change boot parameter.
    -p PORT             Set port to listen to. Default is 4711.
    -l FILE             Logfile. Default is standard out and
                        '$daemonlogfile' in daemon mode.
    -t TIMEOUT          Timeout for bad clients. 0 to disable.
    -d                  Daemon mode.
    -P FILE             PID-file. Default is '$pidfile'.
                        Used only if starting in daemon mode.
    -T                  Print timestamps in the log.
    -f FORMAT           Use strftime(FORMAT) for timestamp formatting.
    -Z                  Use gmtime/UTC instead of local time.
    -i                  When using -b: send IP of client to fai-choot
                        instead of the hostname the host reports.

EOF
  exit 0;
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

getopts('bhZTf:p:l:t:dP:i') || usage;
$opt_h && usage;
$port = $opt_p || 4711;
$timeout = $opt_t || 5;
$localtime = $opt_Z || 0;
$daemon = $opt_d || 0;
$timestamp = $opt_T || 0;
$useip = $opt_i || 0;

if (defined($opt_P)) {
  $pidfile = $opt_P;
}

if (defined($opt_f)) {
    $timestamp_format = $opt_f
} else {
    $timestamp_format = "%a %b %e %H:%M:%S %Y";
}

if (defined($opt_d)) {
  (eval "require Proc::Daemon") or
    die "Daemon mode not available, Proc::Daemon not found. Please install libproc-daemon-perl\n";
  # If in daemon mode, use standard daemon log file
  $logfile = $daemonlogfile;
}

if (defined($opt_l)) {
  $logfile = $opt_l;
}

# Constuct a $logfile that open can take as an argument
if ($logfile eq '-') {
  $logfile = ">&STDOUT";
}
else {
  $logfile = ">>$logfile";
}


read_list_of_variables;
server_init;
big_loop;
