#!/usr/bin/perl -w

#
#   "SystemImager"
#
#   Copyright (C) 1999-2004  Brian Elliott Finley
#
#   $Id: si_addclients 2872 2004-09-29 18:36:18Z brianfinley $
#
#   2004.09.28  Brian Elliott Finley
#   - change addclients to si_addclients
#

# XXX Someday, we should add IP address, hostname, and domain name validation routines. -BEF-

use lib "USR_PREFIX/lib/systemimager/perl";
use strict;
use Getopt::Long;
use SystemImager::Config;
use vars qw($config $VERSION);



### BEGIN Program ###
# set version information
my $VERSION = "SYSTEMIMAGER_VERSION_STRING";
my $program_name = "si_addclients";
my $version_info = <<"EOF";
$program_name (part of SystemImager) v$VERSION

Copyright (C) 1999-2001 Brian Elliott Finley <brian.finley\@baldguysoftware.com>
Copyright (C) 2002 Bald Guy Software <brian.finley\@baldguysoftware.com>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF

# set help information
my $help_info = $version_info . <<"EOF";

Usage: $program_name [OPTION]... REQUIRED_ARGUMENTS
   or: $program_name

Options: (options can be presented in any order)

 -help               Display this output.
                     
 -version            Display version and copyright information.
                     
 -host NAME          Base host name.
                     
 -domainname NAME    Domain name.
                     
 -host-range N-N     Range of hosts involved.  Examples: 1-99 or 01-99.
                     
 -script NAME        Master autoinstall script name.  Don't include
                     the path or the .master extension.  Example: my_image
                     
 -ip-range IP-IP     Range of IP addresses for clients.  Used to create an
                     optional hosts file that clients can use for IP address
                     to hostname resolution during install.  Not necessary if
                     DNS reverse resolution is configured for the IP addresses
                     in question.  If both DNS and a hosts file are used, 
                     information in the hosts file will supercede the 
                     information in DNS.  Example: 10.0.0.1-10.0.0.99.

 -interactive YES/NO This program will go interactive by default if domainname,
                     host, host-range, and script are all specified.  
                     
                     If you specify YES here, then it will go interactive, even
                     if all of these values are specified.  
                     
                     If you specify NO here, then it will not go interactive,
                     even if it is missing some of the required values.

Download, report bugs, and make suggestions at:
http://systemimager.org/
EOF

# defaults
my $script;
my $domain_name = "";
my $base_host_name = "";
my $starting_number = "";
my $ending_number = "";
my $starting_ip = "";
my $ending_ip = "";
my $interactive = "";


# interpret command line options
GetOptions( 
  "help"            => \my $help,
  "version"         => \my $version,
  "domainname=s"    => \$domain_name,
  "host=s"          => \$base_host_name,
  "host-range=s"    => \my $host_range,
  "script=s"        => \$script,
  "ip-range=s"      => \my $ip_range,
  "interactive=s"   => \$interactive,
) 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;
}

unless($< == 0) { die "Must be run as root!\n"; }

if($interactive) {
    $interactive = lc $interactive;
    unless( ($interactive eq "yes") or
            ($interactive eq "no")
        ) {
        print qq(FATAL: If -interactive is used, it must be set to YES or NO.\n);
        print qq(See "$program_name -help" for more information.\n);
        exit 1;
    }
}

if ($script) {
    if($script =~ m|/|) {
        print qq(FATAL: -script must not be a path or a file.\n);
        print qq(See "$program_name -help" for more information.\n);
        exit 1;
    } elsif($script =~ m|\.master$|) {
        print qq(FATAL: -script must not include the .master extension.\n);
        print qq(See "$program_name -help" for more information.\n);
        exit 1;
    }
}

if ($host_range) {
    ($starting_number, $ending_number) = split(/-/, $host_range);
    unless ( ($starting_number ne "") and ($ending_number ne "") ) {
        print qq(FATAL:  for "-host-range X-Y", both X and Y must be specified!\n);
        print qq(See "$program_name -help" for more information.\n);
        exit 1;
    }

    unless ( ($starting_number + 0) and ($ending_number + 0) ) {
        print qq(FATAL:  for "-host-range X-Y", both X and Y must be numeric!\n);
        print qq(See "$program_name -help" for more information.\n);
        exit 1;
    }
}

if ($ip_range) {
    ($starting_ip, $ending_ip) = split(/-/, $ip_range);
}

$domain_name = lc $domain_name;
$base_host_name = lc $base_host_name;


my $all_parameters_set;
if (($base_host_name) and 
    ($script) and 
    ($starting_number ne "") and 
    ($ending_number ne "")) {

    $all_parameters_set = 1;

}

if( ! $all_parameters_set) {

    if($interactive eq "no") {
        print qq(FATAL: Not all required parameters were specified.  Exiting with status of 1.\n);
        print qq(See "$program_name -help" for more information.\n);
        exit 1;
    } else {
        $interactive = "yes";
    }
}


my $autoinstall_script_dir = $config->autoinstall_script_dir();

if($interactive eq "yes") {
    system("clear");
    print << "EOF";
Welcome to the SystemImager "si_addclients" utility  
--------------------------------------------------------------------------------

This utility has 3 sections.  


"Section 1" will ask you for your hostname information.


"Section 2" will allow you to create softlinks from each client hostname to
your "master" script in the "$autoinstall_script_dir" directory.  

  Example: www297.sh -> web_server_image_v1.master


"Section 3" will ask you for IP address information that will be combined 
with the hostname information provided in Section 1 to create entries in 
"/etc/hosts" for each of these same clients.  New entries will be appended 
to the end of "/etc/hosts".  If you specify new hostnames for existing IP 
addresses, those entries will be re-written in place to reflect the new 
host names.


EOF
}

if($interactive eq "yes") {
    print "Continue? ([y]/n): ";
    my $continue=<STDIN>;
    chomp $continue;
    $continue = lc $continue;
    ($continue ne "n") or die "\nsi_addclients: No files were modified.\n";
}

my $satisfied;

if($interactive eq "yes") {
    ### BEGIN main questionnaire ###
    $satisfied = "n";
    while ($satisfied ne "y") {
        system("clear");
        print <<EOF;
si_addclients -- Section 1 (hostname information)
--------------------------------------------------------------------------------

The next series of questions will be used to create a range of hostnames.  
You will be asked for your domain name, the base host name, a beginning 
number, and an ending number.

For example, if you answer:
  domain name     = systemimager.org
  base host name  = www
  starting number = 7
  ending number   = 11

Then the result will be a series of hostnames that looks like this:
  www7.systemimager.org
  www8.systemimager.org
  www9.systemimager.org
  www10.systemimager.org
  www11.systemimager.org


EOF

        print "What is your domain name? [$domain_name]: ";
        $domain_name = get_response($domain_name);
       
        print "What is the base host name that you want me to use? [$base_host_name]: ";
        $base_host_name = get_response($base_host_name);
       
        print "What number should I begin with? [$starting_number]: ";
        $starting_number = get_response($starting_number); 
       
        print "What number should I end with? [$ending_number]: ";
        $ending_number=get_response($ending_number);
       
        $domain_name = lc $domain_name;
        $base_host_name = lc $base_host_name;

        print "\n\n";
        print "I will work with hostnames:  $base_host_name$starting_number through $base_host_name$ending_number\n"; 
        print "             in the domain:  $domain_name\n";
        print "\nAre you satisfied? (y/[n]): ";
        chomp($satisfied=<STDIN>);
    } # while ($satisfied ne "y")
    ### END main questionnaire ###


    ### BEGIN links questionaire ###
    system("clear");
    print <<"EOF";
si_addclients -- Section 2 (soft links to master script)
--------------------------------------------------------------------------------

Would you like me to create soft links to a "master" script so that hosts:

  $base_host_name$starting_number through $base_host_name$ending_number

EOF

    print "can be autoinstalled with that image? ([y]/n): ";
    my $createlinks=<STDIN>;
    chomp $createlinks;
    $createlinks = lc $createlinks;

    unless($createlinks eq "n") {
        $satisfied="n";
        while ($satisfied eq "n") {
        
            # gather a list of available images
            my (@files, $newest_script);
            # ------------------------------------> sort by timestamp -- oldest to newest
            my $cmd = "cd $autoinstall_script_dir && ls -1tr *.master 2>&1";
            open(LS, "$cmd |");
            while (<LS>) {
                chomp;
                s/\.master//;
                push(@files, $_);
                # make the newest image the default
                $newest_script = $_;
            }
            close(LS);
          
            unless($script) { $script = $newest_script; }
          
            # display the list of available images
            unless(@files) { die "There are no available autoinstall scripts.  Please use si_getimage to retrieve an\nimage first.   -The Mgmt\n"; }
            print qq(\nHere is a list of available autoinstall scripts:\n);
            print "\n";
            foreach(@files) {
                print "$_ \n";
            }
            print "\n";
          
            print "Which script would you like these hosts to be installed with?\n";
            print "[$script]: ";
            $script=get_response($script);
            if ( -f "$autoinstall_script_dir/$script.master" ) {
          
                create_links($starting_number, $ending_number, $base_host_name, $script, $autoinstall_script_dir);
          
                print "\nYour soft links have been created.\n";
                print "\nPress <Enter> to continue...";
                $satisfied="y";
                <STDIN>;
                system("clear");
          
            } else {
          
                print qq(\nMaster script \"$script\" does not exist...\n);
                print qq(Let's try again, shall we?\n);
                print qq(\nPress <Enter> to continue...);
                $satisfied="n";
                <STDIN>;
                system("clear");
            }
        }

    } else {
        print "\nNo links will be created.\n";
        print "\nPress <Enter> to continue...";
        <STDIN>;
    }
    ### END links questionaire ###


    ### BEGIN hosts questionaire ###
    system("clear");
    print <<EOF;
si_addclients -- Section 3 (adding or modifying /etc/hosts entries)
--------------------------------------------------------------------------------

Your target machines need to be able to determine their host names from their
IP addresses, unless their host name is specified in a local.cfg file.  

The preferred method for doing this is with DNS.  If you have a working DNS 
that has IP address to hostname resolution properly configured for your 
target machines, then answer "n" here.

If you don't have a working DNS, or you want to override the information in
DNS, then answer "y" here to add entries to the "/etc/hosts" file on your
image server.  After adding these entries, the /etc/hosts file will be 
copied to "$autoinstall_script_dir" where it can be retrieved by your 
target machines.

I will ask you for your clients' IP addresses one subnet at a time.


EOF

    print "Would you like me to continue? (y/[n]): ";

    my $etc_hosts=<STDIN>;
    chomp $etc_hosts;
    $etc_hosts = lc $etc_hosts;
    if ($etc_hosts ne "y") { $etc_hosts = "n"; }

    my $node_number = $starting_number;

    if ($etc_hosts eq "y") {

        my $subnet_count = "0";
        
        ### BEGIN One Subnet at a time ###
        #
        my $satisfied;
        
        while ( $node_number <= $ending_number ) {
            $subnet_count = $subnet_count + 1;
        
            ### get IP information ###
            # ne "y" is used instead of eq "n" because the dissatisfied response may be something other than "n".
            $satisfied="n";
            while ($satisfied ne "y") {
                system("clear");
                print "si_addclients -- Section 3 (adding or modifying /etc/hosts entries -- continued...)\n";
                print "--------------------------------------------------------------------------------\n";
                print "subnet $subnet_count\n\n";
                print "The first host in subnet $subnet_count will be: $base_host_name$node_number\n";
                print "What is the starting IP address for subnet $subnet_count? [$starting_ip]: ";
                $starting_ip = get_response($starting_ip);
                
                print "What is the ending IP address? [$ending_ip]: ";
                $ending_ip = get_response($ending_ip);
                
                print "I will work with IP addresses:  $starting_ip through $ending_ip\n";
                print "\nAre you satisfied? (y/[n]): ";
                chomp($satisfied=<STDIN>);
            }
            ### get IP information ###
        
            ($starting_ip, $ending_ip, $node_number) = add_hosts_entries($starting_ip, $ending_ip, $starting_number, $ending_number, $base_host_name, $domain_name);
        
        }
        ### END One Subnet at a time ###
        
        
        if ( $etc_hosts eq "y" ) { print "\nThese entries have been added to /etc/hosts.\n"; }
        print "\nPress <Enter> to continue...";
        <STDIN>;

    } else {

        print "\nNo entries will be added to your /etc/hosts file.\n";

    }
    ### END hosts questionaire ###

} else {

    create_links($starting_number, $ending_number, $base_host_name, $script, $autoinstall_script_dir);

    if( ($base_host_name) and
        ($starting_ip) and
        ($ending_ip) and
        ($starting_number ne "") ) { 

        add_hosts_entries($starting_ip, $ending_ip, $starting_number,  $ending_number, $base_host_name, $domain_name);
    }
    
}




### BEGIN Subroutines ###
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));
}

# Usage: my $ip_quad = ip_dec2ip_quad($ip_dec);
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);
}

sub numerically { 
    $a <=> $b;
}


# Usage: create_links($starting_number, $ending_number, $base_host_name, $script, $autoinstall_script_dir);
sub create_links {

    my ($starting_number, $ending_number, $base_host_name, $script, $autoinstall_script_dir) = @_;

    $base_host_name = lc $base_host_name;

    foreach my $node_number ($starting_number .. $ending_number) {
        my $cmd = "cd $autoinstall_script_dir && ln -sf $script.master $base_host_name$node_number.sh";
        !system($cmd) or die "Can't $cmd!";
    }
}


# Usage: add_hosts_entries($starting_ip, $ending_ip, $node_number, $ending_number, $base_host_name, $domain_name);
# Usage: ($starting_ip, $ending_ip, $node_number) = add_hosts_entries($starting_ip, $ending_ip, $starting_number, $ending_number, $base_host_name, $domain_name);
sub add_hosts_entries { 

    my ($starting_ip, $ending_ip, $node_number, $ending_number, $base_host_name, $domain_name) = @_;

    $base_host_name = lc $base_host_name;
    $domain_name = lc $domain_name;

    my $starting_ip_dec = ip_quad2ip_dec($starting_ip);
    my $ending_ip_dec = ip_quad2ip_dec($ending_ip);

    ### BEGIN test to be sure /etc/hosts exists and create if it doesn't ###
    my $file = "/etc/hosts";
    if ( ! -f "$file" ) {
        open(ETC_HOSTS, ">> $file") or die "Couldn't open $file for writing: $!\n";
        print ETC_HOSTS "127.0.0.1  localhost\n";
        close(ETC_HOSTS);
        system('chmod 644 /etc/hosts');
    }
    ### END test to be sure /etc/hosts exists and create if it doesn't ###

    ### BEGIN read in /etc/hosts and create a hash of lines by ip address and a hash of lines by number
    my %etc_hosts_lines_by_ip = ();
    my %etc_hosts_lines_by_number = ();
    my $line_number = "1";

    open(ETC_HOSTS, "< /etc/hosts") or die "Couldn't open /etc/hosts for reading: $!\n";
    while (<ETC_HOSTS>) {
        chomp;
        my @fields = split;
        my $ip_quad = $fields[0];
        my $line = $_;
        if ($ip_quad) {
            $etc_hosts_lines_by_ip{$ip_quad} = $line;
        }
        $etc_hosts_lines_by_number{$line_number} = $line;
        $line_number = $line_number + 1;
    }
    close(ETC_HOSTS);
    ### END read in /etc/hosts and create a hash of lines by ip address and a hash of lines by number
    
    ### create a hash of new hostnames by ip address 
    my $desired_length;
    unless (length($starting_number) == length($starting_number + 0)) {
        $desired_length = length($ending_number);
    }

    my %new_hostnames_by_ip;
    my $ip_dec = $starting_ip_dec;
    until ( $ip_dec > $ending_ip_dec ) {
        my $bitwise = $ip_dec & 255;
        if ( $bitwise == "255" ) { $ip_dec = $ip_dec + 2; }
        my $ip_quad = ip_dec2ip_quad($ip_dec);
        $new_hostnames_by_ip{$ip_quad} = "$base_host_name$node_number";
        $ip_dec = $ip_dec + 1;
        $node_number = $node_number + 1;

        if ($desired_length) { $node_number = pad_with_zeros($node_number, $desired_length); }
        if ( $node_number > $ending_number ) { last; }
    }
    ### create a hash of new hostnames by ip address 

    ### munge new ips and hostname info into %etc_hosts_lines_by_ip
    my @new_ip_addresses = sort (keys %new_hostnames_by_ip);
    foreach my $new_ip_address (@new_ip_addresses) {
        if ($domain_name) {
            $etc_hosts_lines_by_ip{$new_ip_address} = "$new_ip_address    $new_hostnames_by_ip{$new_ip_address}.$domain_name  $new_hostnames_by_ip{$new_ip_address}";
        } else {
            $etc_hosts_lines_by_ip{$new_ip_address} = "$new_ip_address    $new_hostnames_by_ip{$new_ip_address}";
        }
    }
    ### munge new ips and hostname info into %etc_hosts_lines_by_ip

    ### BEGIN open temporary /etc/hosts for writing
    my $temp_file = "/tmp/.hosts.systemimager";
    open(NEW_ETC_HOSTS, "> $temp_file") or die "Couldn't open $temp_file for writing: $!\n";
    ### END open temporary /etc/hosts for writing

    ### BEGIN replace entries as necessary in numbered /etc/hosts lines and print numbered lines
    foreach my $line_number ( sort numerically ( keys %etc_hosts_lines_by_number )) {
        $_ = $etc_hosts_lines_by_number{$line_number};
        my @words = split;
        my $ip_quad = $words[0];
        if ($ip_quad) {
            $etc_hosts_lines_by_number{$line_number} = $etc_hosts_lines_by_ip{$ip_quad};
            delete $etc_hosts_lines_by_ip{$ip_quad};
        }
        # print numbered hosts entries
        print NEW_ETC_HOSTS "$etc_hosts_lines_by_number{$line_number}\n";
    }
    ### END replace entries as necessary in numbered /etc/hosts lines and print numbered lines

    ### create hash of entries by decimal ip (for sorting purposes)
    my %etc_hosts_lines_by_ip_decimal;
    my $ip_decimal;
    foreach my $ip_quad ( keys %etc_hosts_lines_by_ip ) {
        $ip_decimal = ip_quad2ip_dec($ip_quad);
        $etc_hosts_lines_by_ip_decimal{$ip_decimal} = $etc_hosts_lines_by_ip{$ip_quad};
    }
    ### create hash of entries by decimal ip (for sorting purposes)

    ### print remaining entries
    foreach my $ip_decimal ( sort( keys %etc_hosts_lines_by_ip_decimal )) {
        print NEW_ETC_HOSTS "$etc_hosts_lines_by_ip_decimal{$ip_decimal}\n";
    }
    ### print remaining entries
    
    ### close temporary /etc/hosts after writing
    close(NEW_ETC_HOSTS);
    ### close temporary /etc/hosts after writing
    
    ### move new hosts file in to place
    system('mv', '-f', $temp_file, '/etc/hosts');
    if($? != 0) { die "Couldn't move $temp_file to /etc/hosts!\n", "Is the filesystem that contains /etc/ full?"; }
    ### move new hosts file in to place
    
    my $cmd = "cp -f /etc/hosts $autoinstall_script_dir";
    !system($cmd) or die "Couldn't $cmd!";

    $starting_ip = ip_dec2ip_quad($ip_dec);
    $ending_ip = "";
    return ($starting_ip, $ending_ip, $node_number);

}

# Usage: my $result = pad_with_zeros($number, $desired_length);
sub pad_with_zeros {

    my ($number, $desired_length) = @_;

    until (length($number) == $desired_length) {
        $number = "0" . "$number";
    }
    
    return $number;
}
### END Subroutines ###
