#!/usr/bin/perl -w

##############################################
#
# aims2cleanup.pl - Tool for cleaning up old or dead hosts
# and restarting dnsmasq if we find new conf files for
# lgcy BIOS workaround
#
# Author: Daniel Juarez Gonzalez <djuarezg@cern.ch>
#
##############################################

use strict;
use POSIX qw(setsid strftime);
use Sys::Syslog qw(:standard :macros);
use File::Path;
use IO::File;
use Digest::MD5;
use Date::Manip;
use Date::Parse;
use aims2server::conf;
use DBI;
use DBD::Oracle;
use File::Find::Rule;
use File::stat;
use Time::Local;
use File::Copy;

# Temp logging for https://its.cern.ch/jira/browse/LOS-688
use DBI::Log;
use DBI::Log file => "/tmp/aims2cleanup.sql";
use DBI::Log trace => 1;
use DBI::Log timing => 1;

$| = 0;

##############################################
# Catch icky-things.
##############################################
$SIG{'INT'}   = 'interrupt';
$SIG{'HUP'}   = 'interrupt';
$SIG{'ABRT'}  = 'interrupt';
$SIG{'QUIT'}  = 'interrupt';
$SIG{'TRAP'}  = 'interrupt';
$SIG{'STOP'}  = 'interrupt';
$SIG{'TERM'}  = 'interrupt';
$SIG{__DIE__} = 'interrupt';

##############################################
my $SERVER_ADMIN = "aims-admins\@cern.ch";

##############################################

my $PROGRAM            = "aims2cleanup";
my $DELAY              = 30;
my $LOG                = "/var/log/aims/aims2cleanup.log";
my $DBSTATUS           = "/var/lock/subsys/aims.DBDOWN";
my $LOCK_PXECLEANUP    = "/var/lock/subsys/aims2cleanup.RUNNING";
my $PIDFILE_PXECLEANUP = "/var/run/aims2cleanup.pid";
my $TFTP_ROOT          = "/aims_share/tftpboot/aims";
my $TFTP_CONF_BIOS     = "/config/bios";
my $TFTP_CONF_LGCY     = "/config/lgcy";
my $TFTP_CONF_UEFI     = "/config/uefi";
my $TFTP_CONF_ARM64    = "/config/arm64";
my $DNSMASQ_CONF_DIR   = "/aims_share/dnsmasq";

##############################################
sub db_connect
##############################################
{
    # Connect to the database. Once connected, return handle.
    # Note, we have to do are own error checking.
    my $dbconn =
        $aims2conf::db_conn_string
      ? $aims2conf::db_conn_string
      : die "Error: Database server undefined.\n";
    my $dbuser =
        $aims2conf::db_user
      ? $aims2conf::db_user
      : die "Error: Database user undefined.\n";
    my $dbpass =
        $aims2conf::db_pass
      ? $aims2conf::db_pass
      : die "Error: Database password undefined.\n";

    # FIXME: Can we use db.pm? Not at the moment.
    my $dbh;
    until (
        $dbh = DBI->connect(
            $dbconn, $dbuser, $dbpass,
            {
                PrintError  => 0,
                RaiseError  => 0,
                LongTruncOk => 0,
                LongReadLen => 1_000_000_000
            }
        )
      )
    {
        db_sleep();
    }
    return $dbh;
}

##############################################
sub db_sleep
##############################################
{
    # Go to sleep for a little bit.
    unless ( -e $DBSTATUS ) {
        system( "touch", "$DBSTATUS" ) == 0
          or groan("cannot create $DBSTATUS: $!");
        alog( $LOG, "lost contact with database." );

        notify_server_admins("Database connection failed.", "Database connection failed, sleeping until next retry.");
    }
    sleep $DELAY;
}

##############################################
sub interrupt {
##############################################
    # Handle things nicely when we croak.
    if ( -e $DBSTATUS ) {
        unlink $DBSTATUS
          or groan("daemon killed but cannot remove $DBSTATUS : $!");
    }
    if ( -e $LOCK_PXECLEANUP ) {
        unlink $LOCK_PXECLEANUP
          or groan("daemon killed but cannot remove $LOCK_PXECLEANUP : $!");
    }
    alog( $LOG, "caught @_, exiting." );
    printf STDOUT "caught @_ exiting\n";
    notify_server_admins("aims2cleanup stopped abruptly","Maintenance cleanup daemon stopped abruptly due to a caught @_. Next Puppet run will run it again.");
    die "Exiting.\n";
}

##############################################
sub groan
##############################################
{
    my $groan = shift;
    alog( $LOG, "error: $groan" );

    notify_server_admins("Unexpected error", "$groan");
}

##############################################
sub insert0 {
##############################################
    # 7th July =~ 07th July :)
    my ($date) = shift;
    if ( $date < 10 ) {
        return "0$date";
    }
    return $date;
}

##############################################
sub getdate {
##############################################
    my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $iddst ) =
      localtime(time);
    my $datestring;
    $year += 1900;
    $mon++;
    $mon        = insert0($mon);
    $mday       = insert0($mday);
    $min        = insert0($min);
    $datestring = "$year-$mon-$mday $hour:$min";
    return ($datestring);
}

##############################################
sub alog {
##############################################
    my ( $logfile, $status ) = @_;

    # FIXME: This is the date it was logged, not executed.. naughty.
    my $date = getdate();
    if ( open( FILE, ">>$logfile" ) ) {
        print FILE ("$date $status\n");
        close FILE;
    }

    syslog( "info", $status . "\n" );
}

##############################################
sub notify_server_admins
##############################################
{
    my ( $state, $message ) = @_;
    open( SENDMAIL, "| /usr/sbin/sendmail -t -oi " );
    print SENDMAIL <<End_of_Mail;
To: $SERVER_ADMIN
Reply-to: $SERVER_ADMIN
Subject: aims2cleanup - $state
$message
End_of_Mail
    close(SENDMAIL);
    alog( $LOG, "Notified server administrators: $state, $message" );
}

#---------------------------------------------
# Start the daemon conf cleanup
#---------------------------------------------

##############################################
unless ( my $pid = fork() ) {
##############################################
    exit if $pid;
    setsid;

    openlog( "aims2cleanup", "ndelay,nofatal", "local3" );

    system( "touch", "$LOCK_PXECLEANUP" ) == 0
      or die "cannot lock $LOCK_PXECLEANUP ($?)\n";
    alog( $LOG, "Starting $PROGRAM pxeconf" );
    system("echo $$ > $PIDFILE_PXECLEANUP") == 0
      or die "cannot write $PIDFILE_PXECLEANUP ($?)\n";

    ##############################################
    # Connect to the database.
    ##############################################
    my $dbh = db_connect();

    while (1) {

        my $lgcy = 0;

        ##############################################
        # Check for a database connecton.
        ##############################################
        if ( !$dbh->ping ) {
            alog( $LOG, "ping db failed... trying to reconnect" );
            until ( $dbh = db_connect ) {
                db_sleep();
            }
        }

        ##############################################
        # The connection must have been restablished.
        ##############################################
        if ( -e $DBSTATUS ) {
            alog( $LOG, "reconnected to database." );
            unlink $DBSTATUS or groan("could not remove $DBSTATUS");

            notify_server_admins("Database connection restored.", "The connection to the database has been restored");
        }

        ##############################################
        # Get a list of pxehosts
        ##############################################
        my $configs = get_pxehosts($dbh);
        my $nowtime = strftime "%s", localtime();

        # First clear the directory of junk.
        # This removes pxe conf files that do not
        # correspond to the current host bootmode
        remove_dead_hosts($configs);

        ##############################################
        # Check for dnsmasq conf files changes
        ##############################################
        my $today         = time;
        my $modtime       = ( stat($DNSMASQ_CONF_DIR)->mtime );
        my $limit_refresh = $today - $DELAY;

        if ( $modtime >= $limit_refresh ) {
            $lgcy++;
        }

        # End of processing.
        # Now sleep.

        if ($lgcy) {

            # restart (NOT reload !) dnsmasq
            # dnsmasq needs to restart to take conf files into account
            # as it happens just before switch from root to nobody user
            system("/bin/systemctl restart dnsmasq");
        }

        sleep $DELAY;

    }

    # END.
    notify_server_admins("aims2cleanup stopped abruptly","Maintenance cleanup daemon stopped abruptly. Next Puppet run will run it again.");
    die "aims2cleanup stopped abruptly";
}

##############################################
sub remove_dead_hosts
##############################################
{
    my ($hosts) = @_;
    remove_dead_hosts_2( $hosts, $TFTP_ROOT . $TFTP_CONF_BIOS,  "bios",  0 );
    remove_dead_hosts_2( $hosts, $TFTP_ROOT . $TFTP_CONF_UEFI,  "uefi",  1 );
    remove_dead_hosts_2( $hosts, $TFTP_ROOT . $TFTP_CONF_ARM64, "arm64", 2 );
    remove_dead_hosts_2( $hosts, $TFTP_ROOT . $TFTP_CONF_LGCY,  "lgcy",  0 );
    return remove_dead_hosts_2( $hosts, $DNSMASQ_CONF_DIR, "lgcy", 3 );
}

##############################################
sub remove_dead_hosts_2
##############################################
{
    my ( $hosts, $dir, $what, $type ) = @_;
    my @configs;

    my $pref = "01-";
    my $suff = "";

    if ( $type == 3 ) {
        $suff = ".lgcy";
    }
    elsif ( $type == 1 || $type == 2 ) {
        $pref = "grub.cfg-01-";
    }
    foreach my $hw ( keys %$hosts ) {
        $hw =~ tr/[A-Z]/[a-z]/;
        push( @configs, $pref . $hw . $suff );
    }

    opendir( *pxedir, $dir )
      or groan("cannot read contents of $dir directory: $!");
    my @contents;
    if ( $type == 1 || $type == 2 ) {
        @contents = grep m/^(grub.cfg-01-)+?/ig, readdir *pxedir;
    }
    else {
        @contents = grep m/^(01-)+?/ig, readdir *pxedir;
    }
    closedir(*pxedir);
    my %removals = map { $_, 1 } @configs;
    my $i        = 0;
    my @removal  = grep { !$removals{$_} } @contents;

    foreach (@removal) {
        alog( $LOG, "REM deadhost $_ [$what]" );
        my $file = "$dir/$_";
        $i++;
        unlink $file or groan("failed to remove $file: $!");
    }

    return $i;
}

##############################################
sub remove_config
##############################################
{
    my ( $interface, $config, $what ) = @_;
    my $cfgfile;
    my $intf;
    $intf    = "01-" . lc($interface);
    $cfgfile = $TFTP_ROOT . $TFTP_CONF_LGCY . "/" . $intf;
    my $intf2          = "grub.cfg-" . $intf;
    my $need_to_update = 0;

    if ( $what != 3 ) {
        $need_to_update +=
          remove_config_2( $cfgfile, $config->{HOSTNAME}, $intf, "lgcy" );
    }
    if ( $what != 0 ) {
        $cfgfile = "$TFTP_ROOT$TFTP_CONF_BIOS/$intf";
        $need_to_update +=
          remove_config_2( $cfgfile, $config->{HOSTNAME}, $intf, "bios" );
    }
    if ( $what != 1 ) {
        $cfgfile = "$TFTP_ROOT$TFTP_CONF_UEFI/$intf2";
        $need_to_update +=
          remove_config_2( $cfgfile, $config->{HOSTNAME}, $intf, "uefi" );
    }
    if ( $what != 2 ) {
        $cfgfile = "$TFTP_ROOT$TFTP_CONF_ARM64/$intf2";
        $need_to_update +=
          remove_config_2( $cfgfile, $config->{HOSTNAME}, $intf, "arm64" );
    }

    return $need_to_update;
}

##############################################
sub remove_config_2
##############################################
{
    my ( $cfgfile, $hostname, $intf, $what ) = @_;

    if ( -e $cfgfile ) {
        alog( $LOG, "REM pxe conf for $intf ($hostname) [$what]" );
        unlink $cfgfile or groan("cannot remove $cfgfile: $!");
        return 1;
    }
    return 0;
}

##############################################
sub get_pxehosts {
##############################################

    my $dbh = shift;

    my %configs = ();

    # Define the SQL query.
    my $sql = <<EOSQL;
SELECT
  i.hw, h.hostname, h.status, t.arch, t.name, h.kopts, t.kopts, t.vmlinuz_source, t.initrd_source,
  to_char(h.registered, 'yyyy/mm/dd hh24:mi:ss'), to_char(h.enabled, 'yyyy/mm/dd hh24:mi:ss'),
  to_char(h.disabled, 'yyyy/mm/dd hh24:mi:ss'), h.noexpiry, h.type, h.lgcy
FROM
  hardware i,
  pxehosts h
LEFT OUTER JOIN
  pxeboot t
ON
  h.target = t.name
WHERE (h.hostname = i.hostname)
EOSQL

    # Prepare to talk to the database.
    # Return ref(%config);
    eval {
        my $prepare = $dbh->prepare($sql)
          or groan( "warning: failed to prepare host query." . $dbh->errstr );
        $prepare->execute();

        #$prepare->bind_columns(undef {});
        while ( my ( $hw, @config ) = $prepare->fetchrow_array ) {
            (
                $configs{$hw}{HOSTNAME},  $configs{$hw}{STATUS},
                $configs{$hw}{ARCH},      $configs{$hw}{TARGET},
                $configs{$hw}{HOSTKOPTS}, $configs{$hw}{TARGETKOPTS},
                $configs{$hw}{VMLINUZ},   $configs{$hw}{INITRD},
                $configs{$hw}{REG},       $configs{$hw}{ON},
                $configs{$hw}{OFF},       $configs{$hw}{NOEXPIRY},
                $configs{$hw}{TYPE},      $configs{$hw}{LGCY},
            ) = @config;
        }
    };
    if ($@) {
        if ( $@ =~ $dbh->err ) {
            groan("warning: failed to query for host changes. see logs.");
            alog( $LOG, "Query failed: $@" );
        }
    }
    \%configs;
}
