#!/usr/bin/perl -w

##############################################
#
# aims2oldhostsremoval.pl - Tool for cleaning up 
# old or dead hosts that are considered stale
#
# 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 aims2server::pxe;
use aims2server::host;
use DBI;
use DBD::Oracle;
use File::stat;
use Time::Local;


# Temp logging for https://its.cern.ch/jira/browse/LOS-688
use DBI::Log;
use DBI::Log file => "/tmp/aims2oldhostsremoval.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            = "aims2oldhostsremoval";
my $DBSTATUS           = "/var/lock/subsys/aims.DBDOWN";
my $LOCK_PXECLEANUP    = "/var/lock/subsys/aims2oldhostsremoval.RUNNING";
my $PIDFILE_PXECLEANUP = "/var/run/aims2oldhostsremoval.pid";

##############################################
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( "lost contact with database." );

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

##############################################
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( "caught @_, exiting." );
    printf STDOUT "caught @_ exiting\n";
    notify_server_admins("aims2oldhostsremoval stopped abruptly","aims2oldhostsremoval cronjob stopped abruptly due to a caught @_.");
    die "Exiting.\n";
}

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

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

##############################################
sub alog {
##############################################
    my ( $status ) = @_;
    # Log directly so we can then use Kibana to
    # check them across all nodes. 
    # Use program: aims2oldhostsremoval as filter
    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: aims2oldhostsremoval - $state
$message
End_of_Mail
    close(SENDMAIL);
    alog( "Notified server administrators: $state, $message" );
}

# Start the old/stale host conf cleanup
##############################################
sub init
##############################################
{

    openlog( "aims2oldhostsremoval", "ndelay,nofatal", "local3" );
    system( "touch", "$LOCK_PXECLEANUP" ) == 0
      or die "cannot lock $LOCK_PXECLEANUP ($?)\n";
    alog( "Starting $PROGRAM nightly cleanup" );
    system("echo $$ > $PIDFILE_PXECLEANUP") == 0
      or die "cannot write $PIDFILE_PXECLEANUP ($?)\n";

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


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

    ##############################################
    # The connection must have been restablished.
    ##############################################
    if ( -e $DBSTATUS ) {
        alog( "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();

    ##############################################
    # Cleanup pxe configurations.
    ##############################################
    # Since we are always removing old hosts we can afford autocommiting now
    $dbh->{AutoCommit} = 1;
    # host and pxe DB interfaces
    my $host = aims2host->new( $dbh, undef, undef );
    my $pxe = aims2pxe->new( $dbh, undef, undef );
    ################################################
    # Set to localboot if host is set to expire
    # and it has been enabled for more than 24 hours
    ################################################
    foreach my $interface ( keys %$configs ) {
        if ( ( defined( $configs->{$interface}{STATUS} )
            && $configs->{$interface}{STATUS} != 2 )
            && ( defined( $configs->{$interface}{NOEXPIRY} )
            && $configs->{$interface}{NOEXPIRY} != 1 ) )
        {
            my $hostname = $configs->{$interface}{HOSTNAME};
            my $ondate = str2time( $configs->{$interface}{ON} );
            my $expire = $ondate + 86400;
            if ( !defined($ondate) || ( $nowtime > $expire ) ) {

                # This will remove the dnsmasq workaround
                $pxe->disablelgcy($hostname);

                # This will also remove all config files that correspond to the host
                $pxe->disable($hostname);
                alog("Nightly host removal set $hostname to localboot after 24 hours");
            }
        }
    }

    ##############################################
    # Remove hosts and its configs after 60 days
    # if they are not set as non expiring
    ##############################################
    foreach my $interface ( keys %$configs ) {
        if ( ( defined( $configs->{$interface}{NOEXPIRY} )
            && $configs->{$interface}{NOEXPIRY} != 1 )
            || !defined( $configs->{$interface}{NOEXPIRY} )
        )
        {
            my $ondate = str2time( $configs->{$interface}{REG} );
            my $expire = $ondate + 5184000;
            if ( !defined($ondate) || ( $nowtime > $expire ) ) {
                # This may try to delete hosts multiple times if they have more than one
                # interface, but it is acceptable
                my $hostname = $configs->{$interface}{HOSTNAME};

                # This will remove the dnsmasq workaround
                $pxe->disablelgcy($hostname);

                # This will also remove all config files that correspond to the host
                $pxe->disable($hostname);

                # We only remove the entry once we got rid of configs!
                $host->remove($hostname);
                alog("Nightly host removal deleted $hostname after 60 days");
            }
        }
    }
}

##############################################
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( "Query failed: $@" );
        }
    }
    \%configs;
}

##############################################
sub dbupdate_host_status
##############################################
{
 my ($dbh, $config) = @_;

 my $sql="UPDATE pxehosts SET status = 2, disabled=CURRENT_TIMESTAMP where hostname = ?";

 eval {
  my $prepare = $dbh->prepare($sql);

  $prepare->execute($config->{HOSTNAME})
 };
 if($@){
     if($@ =~ $dbh->err){
        die "Database Error: Failed to set localboot status for host: $config->{HOSTNAME} $@.\n";
     }
  }
}



init();
