#!/usr/bin/perl -w
#
#   "SystemImager" 
#
#   Copyright (C) 1999-2003 Brian Elliott Finley <brian@bgsw.net>
#
#   $Id: pushupdate,v 1.11 2003/06/25 19:37:12 brianfinley Exp $
#
#   Others who have contributed to this code (in alphabetical order):
#       Curtis Zinzilieta <czinzilieta@valinux.com>
#

use lib "USR_PREFIX/lib/systemimager/perl";

use Socket;
use IO::Handle;
use FileHandle;
use Time::Local;
use Getopt::Long;
use SystemImager::Options;
use POSIX qw(strftime);

### BEGIN Parameters to be read from /etc/systemimager/systemimager.conf
$si_logdir = "/var/log/systemimager";
$si_log_format = "%t %o %f";
### END Parameters ###

$program_name="pushupdate";



### BEGIN Program ###
# set version information
$version_number = "SYSTEMIMAGER_VERSION_STRING";
$version_info = <<"EOF";
pushupdate (part of SystemImager) version $version_number

EOF

$version_info .= SystemImager::Options->copyright();


# set help information
$get_help = "   Try \"pushupdate -help\" for more options.";
$help_info = $version_info .  SystemImager::Options->pushupdate_options_header();
$help_info = $help_info .  SystemImager::Options->generic_options_help_version();
$help_info = $help_info .  SystemImager::Options->pushupdate_options_body();
$help_info = $help_info .  SystemImager::Options->updateclient_options_body();
$help_info = $help_info .  SystemImager::Options->generic_footer();

# setup generic logging info message for detail client logs if needed
$logging_blurb = <<"EOF";

Check out the -log option with the command pushupdate -help
to get information for capturing detailed file transfer logging
from each client pushupdate targets.
EOF

#
# default values
#
my $concurrent=1;

# interpret command line options
GetOptions(

    "help"                      => \my $help,
    "version"                   => \my $version,
    "client=s"                  => \my $base_host_name,
    "range=s"                   => \my $range,
    "domain=s"                  => \my $domain_name,
    "clients-file=s"            => \my $clients_file,
    "concurrent-processes=s"    => \$concurrent,
    "continue-install"          => \my $continue_install,
    "ssh-user=s"                => \my $ssh_user,
    "log=s"                     => \my $log_format,
    "updateclient-options=s"    => \my $updateclient_options,

) or die qq($help_info);

# if requested, print help information
if($help) {
    print qq($help_info);
    exit 0;
}

# if requested, print version and copyright information
if($version) {
    print qq($version_info);
    exit 0;
}

# be sure $imageserver name doesn't start with a hyphen
if($imageserver) {
    $_ = $imageserver;
    if(/^-/) { die "\n$program_name: Server name can\'t start with a hyphen.\n$get_help\n\n"; }
}

# must have some specifier for processing files
if((!$clients_file) and (!$base_host_name)) {
    die "\n$program_name: Must specify -client or -clients-file.\n$get_help\n\n";
}

# if name usage is bad, print help information
if ((!$imageserver) and (!$continue_install)) {
    print "\n$program_name: Must specify -server if doing an update.\n";
    die     "            Must specify -continue-install if doing an install.\n$get_help\n\n";
}

# -clients-file doesn't exist
if(($clients_file) and ( ! -e $clients_file )) {
    die "\n$program_name: Unable to find $hostfile.\n$get_help\n\n";
}

# -clients-file and -client conflict
if($clients_file and $base_host_name) {
    die "\n$program_name: Must select either -clients-file or -client.\n$get_help\n\n";
}

# if -client, must have -image
if($base_host_name and !$image) {
    die "\n$program_name: Must also specify -image.\n$get_help\n\n";
}

# if -clients-file, cannot have -image
if($clients_file and $image) {
    print "\n$program_name: -clients-file and -image conflict.";
    die   "\n               Images should be specified in the clients file.\n$get_help\n\n";
}

# setup @hostname array for processing
if (-e $clients_file) {
    open (FH, "< $clients_file") or die "\n$program_name: Unable to open $clients_file: $!\n";
    @hostnames_and_images = <FH>;
    close (FH) or die "\n$program_name: Unable to close $hostfile: $!\n";
    my (@fields, $testline, $i);
    for ($i = 0; $i <= $#hostnames; $i++) {
        # attempt to parse each line, just to make sure there is an image or script specified
        chomp($hostnames[$i]);
        @fields = split(" ", $hostnames[$i]);
        if(!$fields[1]) {
            die "No image/script defined for $fields[0] on line $i of file $clients_file\n";
        }
        
        # if we are running as autoinstall, find and verify the autoinstall script exists
        # assume that the entry lists only a filename...add the
        # /var/lib/systemimager/scripts and ".master" in the path/filename.
        if ($continue_install) {
            my $installscript = "/var/lib/systemimager/scripts/" . $fields[1] . ".master";
            if (! -e $installscript) {
        die "Master install script not found for $fields[0] on line $i of file $clients_file\n";
            }
        }
    }
} else {
    # prepare needed variables
    if ($domain_name) { $domain_name = lc $domain_name; } 
    
    # verify the script to install, if relevant
    if ($continue_install) {
        my $installscript = "/var/lib/systemimager/scripts/" . $image . ".master";
        if (! -e $installscript) {
            die "\n$program_name: $installscript not found!\n";
        }
    }
    
    # must have a hostname then...put it into the array for processing
    if ($range) {
        # decide if there is a range, and extract it
        my ($starting_number, $ending_number);
        $range =~ /\-/;
        $starting_number = trim($`);
        $ending_number = trim($');
        if ((!starting_number) || (!ending_number)) {
            die "Invalid range specifier.\n$get_help\n\n";
        }
        
        # given a good range, build the hostnames array
        my $node_number;
        my $count = 0;
        foreach $node_number ($starting_number .. $ending_number) {
            $hostnames[$count] = $base_host_name . $node_number;
            if ($domain_name) {
                $hostnames[$count] = "$hostnames[$count]." .  $domain_name .  " $image";
            }
            $count++;
        }
    } else {
        my $hostname = $base_host_name;
        if ($domain_name) {
            $hostname = "$hostname." . $domain_name;
        }
        $hostname = $hostname . " $image";
        @hostnames_and_images = $hostname;
    }
}


# Begin main program loop, processing each push as needed
$CONCURRENT_RUNNING_PROCESSES = 0;
my $element;
foreach $element (@hostnames_and_images) {
    if ($CONCURRENT_RUNNING_PROCESSES >= $concurrent) {
        wait;
        $CONCURRENT_RUNNING_PROCESSES--;
    }

    # fork a new process with the command to execute
    if ($pid = fork) {
        $CONCURRENT_RUNNING_PROCESSES++;
        sleep 1;
    } elsif (defined $pid) {
        # this is the newly forked process
        if ($continue_install) {
	        processautoinstall($element);
        } else {
            processupdate($element);
        }
        exit(0);
    } else {
        die "error forking: $!\n";
    }
}

# wait for children to finish
while (wait != -1) { ; } ;

exit 0;



################################################################################
#
#   Subroutines
#
sub check_if_root{
    unless($< == 0) { die "$program_name: Must be run as root!\n"; }
}


sub get_response {
    my $garbage_out=$_[0];
    my $garbage_in=<STDIN>;
    chomp $garbage_in;
    unless($garbage_in eq "") { $garbage_out = $garbage_in; }
    return $garbage_out;
}


sub dec2bin {
    my $str = unpack("B32", pack("N", shift));
    return $str;
}


sub dec2bin8bit {
    my $str = unpack("B32", pack("N", shift));
    $str = substr($str, -8); # 32bit number -- get last 8 bits (the relevant ones)
    return $str;
}


sub bin2dec {
    return unpack("N", pack("B32", substr("0" x 32 . shift, -32))); # get all 32bits
}


sub ip_quad2ip_dec {
    (my $a, my $b, my $c, my $d) = split(/\./, $_[0]);
    my $a_bin=dec2bin8bit($a);
    my $b_bin=dec2bin8bit($b);
    my $c_bin=dec2bin8bit($c);
    my $d_bin=dec2bin8bit($d);
    return bin2dec(join('', $a_bin, $b_bin, $c_bin, $d_bin));
}


sub ip_dec2ip_quad {
    my $ip_bin = dec2bin($_[0]);
    my $a_dec = bin2dec(substr($ip_bin, 0, 8));
    my $b_dec = bin2dec(substr($ip_bin, 8, 8));
    my $c_dec = bin2dec(substr($ip_bin, 16, 8));
    my $d_dec = bin2dec(substr($ip_bin, 24, 8));
    return join('.', $a_dec, $b_dec, $c_dec, $d_dec);
}


# trim leading/trailing spaces and return result
sub trim {
  my @converts = @_;
  for (@converts) {
    s/^\s+//;
    s/\s+$//;
  }
  return wantarray ? @converts : $converts[0];
}


# write a record to pushupdate command log
sub updatelogfile {
  my ($logmessage) = @_;
  my $fh=0;
  open ($fh,">> $si_logdir/pushupdate");
  my $datetime = strftime("%Y/%m/%d %H:%M:%S", localtime(time()));
  print $fh "$datetime  $logmessage\n";
  close ($fh);
}

# start a new install from within each fork
sub processautoinstall {
    my ($param_input) = @_;
    my ($logfile, $local_log_format, $status, $rc, $command);
    
    # if -pre is specified, run it here, burying all results
    if ($pre) {
        $rc = `$pre > /dev/null 2>&1`;
        if ($rc) { 
            die "unusual error code from -pre script $pre of:$rc $!\n";
        }
    }
    
    # split param-target, to pickup the target address & image/script
    chomp($param_input);
    my @fields = split(" ", $param_input);
    my $param_target = trim($fields[0]);
    my $param_image  = trim($fields[1]);
    
    # output the command to the log
    updatelogfile("pushupdate autoinstall started for $param_target");
    
    # extract ip address and lookup client hostname
    # if ip address given, just returns same value back
    my $client = gethostbyaddr(inet_aton($param_target), AF_INET) 
        or die "Cant resolve $param_target: $!\n";
    
    # set the machine specific logfile name
    $logfile = "$si_logdir/pushupdate.$param_target";
    if (defined($log_format)) {
        if ($log_format) {
            $local_log_format = $log_format;
        } else {
            $local_log_format = $si_log_format;
        }
    } else {
        my $datetime = strftime("%Y/%m/%d %H:%M:%S", localtime(time()));
        my $fh=0;
        open ($fh,"> $logfile");
            print $fh "client $param_target was autoinstalled $datetime.\n";
            print $fh $logging_blurb;
        close ($fh);
        $logfile = "/dev/null";
    }
    
    # copy install script out to the client
    $command = "ssh -o \"StrictHostKeyChecking no\" -l root -R8730:127.0.0.1:873 $client \"rsync -avL rsync://127.0.0.1:8730/scripts/$param_image.master /tmp/\"";
    $rc = 0xffff & system($command);
    if ($rc != 0) { 
        # get username from /etc/passwd as derived from the real user id "$>"
        my $username = getpwuid $>;
        my $message="FATAL: Could not copy /var/lib/systemimager/scripts/systemimager/$param_image.master to $client!\n";
        $message = $message . "       Be sure that you have ${username}'s .ssh/identity.pub key in ssh_files/authorized_keys.\n";
        updatelogfile($message);
        die $message;
    }
    
    # install script is ready to run, so open ssh tunnel and execute script
    $command = "ssh -o \"StrictHostKeyChecking no\" -l root -R873:127.0.0.1:873 $client sh /tmp/$param_image.master > $logfile 2>&1";
    $rc = 0xffff & system($command);
    if ($rc != 0) { 
        my $message="FATAL: Could not connect via ssh and run autoinstall script on $client!\n";
        updatelogfile($message);
        die $message;
    } else {
        my $message="Completed pushupdate autoinstall for $client from $imageserver.\n";
        updatelogfile($message);
    }
    
    # if -post is specified, run it here, burying all results
    if ($post) {
      $rc = `$post > /dev/null 2>&1`;
      if ($rc) {
        die "unusual error code from -post script $post of:$rc $!\n";
      }
    }
}


# run updateclient from within each fork
sub processupdate {
    my ($param_input) = @_;
    my ($logfile, $local_log_format, $status, $rc, $command);
    
    # if -pre is specified, run it here, burying all results
    if ($pre) {
      $command = "$pre > /dev/null 2>&1";
      $rc = 0xffff & system($command);
      if ($rc) { 
        die "unusual error code from -pre script $pre of:$rc $!\n";
      }
    }
    
    # split param-target, to pickup the target address & image
    chomp($param_input);
    my @fields = split(" ", $param_input);
    my $param_target = trim($fields[0]);
    my $param_image  = trim($fields[1]);
    
    # output the command to the log
    updatelogfile("pushupdate started for $param_target");
    
    # extract ip address and lookup client hostname
    # if ip address given, just returns same value back
    my $client = gethostbyaddr(inet_aton($param_target), AF_INET) 
        or die "Cant resolve $param_target: $!\n";
    
    # set the machine specific logfile name
    $logfile = "$si_logdir/pushupdate.$param_target";
    if (defined($log_format)) {
        if ($log_format) {
            $local_log_format = $log_format;
        } else {
            $local_log_format = $si_log_format;
        }
    } else {
        my $datetime = strftime("%Y/%m/%d %H:%M:%S", localtime(time()));
        my $fh=0;
        open ($fh,"> $logfile");
        print $fh "client $param_target was last updated $datetime.\n";
        print $fh $logging_blurb;
        close ($fh);
        $logfile = "/dev/null";
    }
    
    #
    # build command line
    #
    $command = "ssh -l $ssh_user $client sudo updateclient $updateclient_options --image $param_image";
    
    # run command to start updateclient on the remote workstation
    $rc = 0xffff & system($command);
    if (!$rc) {
        updatelogfile("completed pushupdate for $client from $imageserver");
    } else {
        updatelogfile("unsuccessful pushupdate for $client from $imageserver");
    }
    
    # if -post is specified, run it here, burying all results
    if ($post) {
        $command = "$post > /dev/null 2>&1";
        $rc = 0xffff & system($command);
        if ($rc) {
            die "unusual error code from -post script $post of:$rc $!\n";
        }
    }
}
