#!/usr/bin/perl -w

#
# "SystemImager" - Copyright (C) 1999-2001 Brian Elliott Finley <brian@systemimager.org>
#
#   $Id: getimage,v 1.110.4.1 2001/11/30 04:19:00 dannf Exp $
#
#   Others who have contributed to this code (in alphabetical order):
#     Phil Champon <pchampon@valueweb.net>
#     Sean Dague <sean@dague.net>
#     Ian McLeod <ian@valinux.com>
#     James Oakley <joakley@solutioninc.com>
#     Laurence Sherzer <lsherzer@gate.net>
#     Wesley Smith <wessmith@engr.sgi.com>
#     Curtis Zinzilieta <czinzilieta@valinux.com>
#

# set system path for system() calls
$ENV{PATH} = "/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin";

# version
$version_number="2.0.1";

# miscellaneous variables and what not
# NIS domain name will be figured out automatically (if it exists)
$nisdomain="";

# use the long options module to allow us to use, well, long options ;)
use lib "/usr/lib/systemimager/perl";
use lib "/usr/local/lib/systemimager/perl";
use SystemImager::Server;
use SystemImager::Common;
use Getopt::Long;
use AppConfig;

### BEGIN parse the config file ###
my $config = AppConfig->new(
			    'autoinstall_script_dir' => { ARGCOUNT => 1 },
			    'autoinstall_boot_dir' => { ARGCOUNT => 1 },
			    'default_imagedir' => { ARGCOUNT => 1 },
			    'rsyncd_conf' => { ARGCOUNT => 1 },
			    'config_dir' => { ARGCOUNT => 1 },
			    );

$config->file('/etc/systemimager/systemimager.conf');

my $autoinstall_script_dir = $config->autoinstall_script_dir();
my $config_dir = $config->config_dir();
my $rsyncd_conf = $config->rsyncd_conf();
my $default_imagedir = $config->default_imagedir();

if (!$autoinstall_script_dir) {
    die "AUTOINSTALL_SCRIPT_DIR not defined in the config file.";
}
if (!$config_dir) { die "CONFIG_DIR not defined in the config file."; }
if (!$rsyncd_conf) { die "RSYNCD_CONF not defined in the config file."; }
if (!$default_imagedir) {
    die "DEFAULT_IMAGEDIR not defined in the config file.";
}
### END parse the config file

### BEGIN Program ###

# figure out the program name
$0  =~ /(.*)\/+([^\/]*)$/;
$program_name = $2;

$version_info = <<"EOF";
$program_name (part of SystemImager) v$version_number

Copyright (C) 1999-2001 Brian Elliott Finley <brian\@systemimager.org>
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

$get_help = "          Try \"$program_name -help\" for more options.";
$help_info = $version_info . <<"EOF";

Usage: $program_name [OPTION]...  -golden-client HOSTNAME -image IMAGENAME

Options: (options can be presented in any order and may be abbreviated)

 -help                    Display this output.

 -version                 Display version and copyright information.

 -golden-client HOSTNAME  Hostname or IP address of the \"golden\" client.

 -image IMAGENAME         Where IMAGENAME is the name to assign to the 
                          image you are retrieving.  This can be either
                          the name of a new image if you want to create
                          a new image, or the name of an exisiting image
                          if you want to update an image.  

 -ssh-user USERNAME       Username for ssh connection to the client.
                          Only needed if a secure connection is required.

 -log "STRING"		  Quoted string for log file format.  See the
			  rsyncd.conf man page for options.

 -quiet                   Don\'t ask any questions or print any output
                          (other than errors). In this mode, no warning
                          will be given if the image already exists on
                          the server.

 -directory PATH          The full path and directory name where you want
                          this image to be stored.  The directory bearing
                          the image name itself will be placed inside the
                          directory specified here.

 -exclude PATH            Don\'t pull the contents of PATH from the 
                          golden client.  PATH must be absolute (starting 
			  with a "/").  
			  
			  To exclude a single file use:
			   -exclude /directoryname/filename

                          To exclude a directory and it's contents use:
                           -exclude /directoryname/

			  To exclude the contents of a directory, but
			  pull the directory itself use:
			   -exclude "/directoryname/*"

 -exclude-file FILE       Don\'t pull the PATHs specified in FILE from the
                          golden client.

 -update-script [YES|NO]  Update the \$image.master script?  Defaults to 
                          NO if -quiet.  If not specified you will be
                          prompted to confirm an update.


The following options affect the autoinstall client after autoinstalling:

 -ip-assignment METHOD    Where METHOD can be:

                          static_dhcp -- A DHCP server will assign the
			    same static address each time to clients 
			    installed with this image.  Also see the
			    \"mkdhcpstatic\" command.

			  dynamic_dhcp -- A DHCP server will assign IP
			    addresses dynamically to clients installed
			    with this image.  They may be assigned a
			    different address each time.

                          static -- The IP address the client uses
			    during autoinstall will be permanently
			    assigned to that client.

			  replicant -- Don\'t mess with the network
			    settings in this image.  I\'m using it as a
			    backup and quick restore mechanism for a 
			    single machine.

 -post-install ACTION     ACTION can be:

                          beep -- Clients will beep incessantly after 
			    succussful completion of an autoinstall.
			    (This is the default)

			  reboot -- Clients will reboot themselves
			    after successful completion of an 
			    autoinstall.

			  shutdown -- Clients will halt themselves
			    after successful completion of an 
			    autoinstall.


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

$golden_client = "";
$imageserver = "";
$ip_assignment_method = "";
$image = "";
$exclude = "";
$exclude_file = "";
$update_script = "";
$post_install = "beep"; # the default

### BEGIN evaluate options ###
GetOptions( 
    "golden-client=s" => \$golden_client,
    "server=s" => \$imageserver,
    "image=s" => \$image,
    "directory=s" => \$default_imagedir,
    "ip-assignment=s" => \$ip_assignment_method,
    "exclude=s" => \$exclude,
    "exclude-file=s" => \$exclude_file,
    "update-script=s" => \$update_script,
    "post-install=s" => \$post_install,
    "ssh-user=s" => \$ssh_user,
    "log=s" => \$log,
    "help" => \$help,
    "version" => \$version,
    "quiet" => \$quiet
) || die "$help_info";

$update_script = lc $update_script;

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

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

if($imageserver){
  die "\n$program_name: -server is now depricated.  Try cpimage command instead.\n$get_help\n\n";
}

# be sure $golden_client name doesn't start with a hyphen
if ($golden_client) {
  if ($golden_client =~ /^-/) { 
    die "\n$program_name: Golden client name can't start with a hyphen.\n$get_help\n\n";
  }
  $source_host = $golden_client;
}

# both a source host and image must be set.
unless ($source_host and $image) {
    die "\n$program_name: You must specify -golden-client and -image.\n$get_help\n\n";
}


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

# be sure $default_imagedir is an absolute path starting with /
unless ($default_imagedir =~ /^\//) { 
  die "\n$program_name: -directory must be an absolute path starting with \"/\".\n$get_help\n\n";
}

# be sure $exclude is an absolute path starting with /
if($exclude){
  unless($exclude =~ /^\//){ 
    die "\n$program_name: -exclude must be an absolute path starting with \"/\".\n$get_help\n\n";
  }
}

# be sure $update_script was passed a proper option
unless(
       ($update_script eq ""   )
    or ($update_script eq "yes")
    or ($update_script eq "no" )
) { die "\n$program_name: -update-script must be yes or no.\n$get_help\n\n"; }

SystemImager::Server->validate_ip_assignment_option($ip_assignment_method);
SystemImager::Server->validate_post_install_option($post_install);

# only golden client or server may be set
if ($exclude and $exclude_file) {
  die "\n$program_name: Either use -exclude or -exclude-file but not both.\n$get_help\n\n";
}

# if -exclude-file is used, exclude file must exist
if ($exclude_file) {
  unless(-e $exclude_file){
    die "\n$program_name: I can\'t find the exclude file specified by -exclude-file!\n$get_help\n\n";
  }
}
### END evaluate options ###

# be sure program is run by root
SystemImager::Common->check_if_root();

# fill in variables based on options passed
$master_script = $autoinstall_script_dir . "/$image.master";
$final_exclude_file = "/tmp/.exclude.$image";
$imagedir = "$default_imagedir/$image";

$warning =  <<"EOF";
This program will get the \"$image\" system image from \"$source_host\"
making the assumption that all filesystems considered part
of the system image are using ext2, ext3, or reiserfs.

This program will not get /proc, NFS, or other filesystems
not mentioned above.

See \"getimage -help\" for command line options.

EOF

# give warning
if (!$quiet) {
    system("clear");
    print $warning;
    print "Continue? ([y]/n): ";
    $continue = SystemImager::Common->get_response('y');
    ($continue ne "n") or die "$program_name: No files were modified.\n";
    print "\n";
}

if (! -d "$default_imagedir")  {
  mkdir("$default_imagedir", 0750) or die "$program_name: Can't make directory $default_imagedir\n";
}

if (-d "$imagedir") {
  if (!$quiet) {
    print "An image named \"$image\" already exists...\n";
    print "Update exisiting image? ([y]/n): ";
    $continue = SystemImager::Common->get_response('y');
    ($continue ne "n") or die "$program_name: No files were modified.\n";
    print "\n";
  }
} else  {
    mkdir("$imagedir", 0777) || 
	die "$program_name: Can't make directory $imagedir\n";
}

# Set default rsync port number
$port="873";

# If we're using SSH, go ahead and establish port forwarding
if($ssh_user) {
  # Get a random port number (normal rsync port won't work if rsync daemon is running)
  my $port_in_use="yes";
  until ( $port_in_use eq "no" )
  {
    $port_in_use="no";
    $port = rand 60000;
    $port =~ s/\..*//;

    # Be sure port isn't reserved
    $file="/etc/services";
    open (FILE, "<$file") || die ("$0: Couldn't open $file for reading\n");
      while (<FILE>) {
        if (/$port/) { 
          $port_in_use="yes";
          next;
        }
      }
    close FILE;
  
    # Be sure port isn't in use
    open (NETSTAT, "netstat -tn |");
    while (<NETSTAT>) {
      (my $junk, my $port_and_junk) = split (/:/);
      if ($port_and_junk) { 
        (my $netstat_port, my $other_junk) = split (/ +/, $port_and_junk);
        if ($netstat_port = /$port/) { 
          $port_in_use="yes";
          next;
        }
      }
    }
  }

  # Setup the port forwarding
  $command="ssh -f -l $ssh_user -L $port:$source_host:873 $source_host sleep 5";
  $rc = 0xffff & system($command);
  if ($rc != 0) { 
    print "FATAL: Failed to establish secure port forwarding to $source_host!\n";
    die   "       Be sure that you can run \"ssh -l $ssh_user $source_host\" successfully.\n";
  }

  # and change the source host to point to localhost so we can use the port forwarding
  $source_host="127.0.0.1";
}


### BEGIN golden client stuff ###
if($source_host) {
  # get /etc/systemimager/mounted_filesystems from $source_host
  if (!$quiet) {
    print "Retrieving /etc/systemimager/mounted_filesystems from $source_host to check for mounted filesystems...\n";

    my $directory="${imagedir}/etc/";
    unless (-d "$directory") { mkdir("$directory", 0755) || die "$program_name: Can't make directory $directory\n"; }

    $directory="${imagedir}/etc/systemimager/";
    unless (-d "$directory") { mkdir("$directory", 0755) || die "$program_name: Can't make directory $directory\n"; }

    $command = "rsync -av --numeric-ids";
    if ($log) { $command = $command . qq( --log-format="$log"); }
    if ($ssh_user) { $command = $command . " --bwlimit=10000"; }
    $command =  $command . " rsync://${source_host}:${port}/root/etc/systemimager/mounted_filesystems ${imagedir}/etc/systemimager/mounted_filesystems";

    open(RSYNC, "$command|");
    print "------------- $source_host mounted_filesystems RETRIEVAL PROGRESS -------------\n";
    while (<RSYNC>) {
      print;
    }
    print "------------- $source_host mounted_filesystems RETRIEVAL FINISHED -------------\n";
    close(RSYNC);
  } else {

    $directory="${imagedir}/etc/";
    unless (-d "$directory") { mkdir("$directory", 0755) || die "$program_name: Can't make directory $directory\n"; }

    $directory="${imagedir}/etc/systemimager/";
    unless (-d "$directory") { mkdir("$directory", 0755) || die "$program_name: Can't make directory $directory\n"; }

    $command = "rsync -a --numeric-ids";
    if ($log) { $command = $command . qq( --log-format="$log"); }
    if ($ssh_user) { $command = $command . " --bwlimit=10000"; }
    $command = $command . " rsync://${source_host}:${port}/root/etc/systemimager/mounted_filesystems ${imagedir}/etc/systemimager/mounted_filesystems";

    system($command);
  }
  
  # $? is the return value from rsync
  if ($?) {
    print "Failed to retrieve /etc/systemimager/mounted_filesystems from $source_host.\n";
    print "$program_name: Have you run \"prepareclient\" on $source_host?\n";
    print "          If you see the message \"unrecognised option\" above, check\n";
    print "          http://systemimager.org/download/ to be sure that you are running\n";
    die   "          the recommended version of rsync.\n";
  }
  
  # create list of filesystems to *not* get
  $file="${imagedir}/etc/systemimager/mounted_filesystems";
  open (FILE, "<$file") || die ("$program_name: Couldn't open $file for reading!\n");
    @mounted_filesystems = <FILE>;
  close FILE;

  $file="$final_exclude_file";
  open (FINAL_EXCLUDE_FILE, ">$file") || die "$program_name: Couldn't open $file for writing!\n";
    @mounted_filesystems = grep (!/\s+ext2\s+/, @mounted_filesystems);
    @mounted_filesystems = grep (!/\s+ext3\s+/, @mounted_filesystems);
    @mounted_filesystems = grep (!/\s+reiserfs\s+/, @mounted_filesystems);
    print FINAL_EXCLUDE_FILE "\n# Automatic exclusions made by SystemImager.\n";
    foreach (@mounted_filesystems) {
      /\S+\s+\S+\s+(\S+)\s+/;
      my $mount_point=$1;
      print FINAL_EXCLUDE_FILE "$mount_point/*\n";
    }
    if($exclude) { 
      print FINAL_EXCLUDE_FILE "\n# Exclusions from -exclude on the command line.\n";
      print FINAL_EXCLUDE_FILE "$exclude\n";
    }
    if($exclude_file) { 
      print FINAL_EXCLUDE_FILE "\n# Exclusions from -exclude-file.\n";
      $file="$exclude_file";
      open (EXCLUDE_FILE, "<$file") || die "$program_name: Couldn't open $file for reading!\n";
        while(<EXCLUDE_FILE>){
          print FINAL_EXCLUDE_FILE "$_\n";
	}
      close EXCLUDE_FILE;
    }
  close FINAL_EXCLUDE_FILE;

  # compile rsync options
  $options = "--delete --delete-excluded --exclude-from=$final_exclude_file";
  if ($log) { $options = $options . qq( --log-format="$log"); }
  if ($ssh_user) { $options = $options . " --bwlimit=10000"; }
  $options = $options . " rsync://${source_host}:${port}/root/ $imagedir";
}
### END golden client stuff ###

### BEGIN generic image retrieval stuff ###
# Get the image from the specified host
if (!$quiet) {
    print "\n\nRetrieving image $image from $source_host\n";
    open (RSYNC, "rsync -av --numeric-ids $options |");
    print "------------- $image IMAGE RETRIEVAL PROGRESS -------------\n";
    while (<RSYNC>) {
        print $_;
    }
    print "------------- $image IMAGE RETRIEVAL FINISHED -------------\n";
    close (RSYNC);
} else {
    $command = qq(rsync -a --numeric-ids $options);
    system($command);
}

# $? is the return value from rsync
if ($?) {
    die "$program_name: Failed to retrieve image $image from $source_host.\n";
}


if(!$quiet) {
    print "\nPress <Enter> to continue...";
    <STDIN>;
}

unlink $final_exclude_file || die "$program_name: Removal of file $final_exclude_file failed\n";

# Add entry to image server's rsyncd.conf if necessary

SystemImager::Server->add2rsyncd($rsyncd_conf,$image,$imagedir) or 
  die "$program_name: Cannot add $image entry to $rsyncd_conf";

### END generic image retrieval stuff ###


### BEGIN Overwrite $image.master? ###
if( (!$quiet) and (!$update_script) and (-e $autoinstall_script_dir . "/$image.master") ) {
    system("clear");
    print "Update Autoinstall Script?\n";
    print "--------------------------\n\n";
    print "An autoinstall script for this image already exists.  It is recommended\n";
    print "that you update this autoinstall script, unless you have customized it.\n";
    print "(You will know if you have customized it.)\n\n";
    print "Would you like to update the autoinstall script for this image? ([y]/n): ";

    $update_script = SystemImager::Common->get_response('yes');
    $update_script = lc $update_script;

    # if user actually hits "y", deal with it
    if($update_script eq "y") { $update_script = "yes"; }
}

# If it just ain't there yet, make it and don't even ask about it.
elsif(! -e $autoinstall_script_dir . "/$image.master") {
    $update_script="yes";
}

# if we don't need to update the master script, then exit -- we're done
unless($update_script eq "yes") { exit 0; }

### END Overwrite $image.master? ###


### BEGIN Got NIS? ###
$file="$imagedir/etc/sysconfig/network";
if(open (NETWORK, "< $file")) {
    while (<NETWORK>) {
        if (/^NISDOMAIN\s*\=\s*(\S+)/) {
            $nisdomain = $1;
        }
    }
    close (NETWORK);
}
### END Got NIS? ###


### BEGIN IP Assignment Method ###
if((!$quiet) and (!$ip_assignment_method))
{
    $satisfied="n";
    $ip_assignment_method="1";
    until($satisfied eq "y")
    {
        system("clear");
        print << 'EOF';
IP Address Assignment
---------------------

There are four ways to assign IP addresses to the client systems on an
ongoing basis:

1) static_dhcp -- A DHCP server will assign the
     same static address each time to clients 
     installed with this image.  Also see the
     "mkdhcpstatic" command.

2) dynamic_dhcp -- A DHCP server will assign IP
     addresses dynamically to clients installed
     with this image.  They may be assigned a
     different address each time.

3) static -- The IP address the client uses
     during autoinstall will be permanently
     assigned to that client.

4) replicant -- Don't mess with the network
     settings in this image.  I'm using it as a
     backup and quick restore mechanism for a 
     single machine.

EOF

        print "Which method do you prefer? [$ip_assignment_method]: ";
        $ip_assignment_method=SystemImager::Common->get_response($ip_assignment_method);
        print "You have chosen method $ip_assignment_method for assigning IP addresses.\n";
        print "\nAre you satisfied? ([y]/n): ";
        $satisfied=SystemImager::Common->get_response('y');
	unless(
	           ($ip_assignment_method == "1")
                or ($ip_assignment_method == "2")
                or ($ip_assignment_method == "3")
                or ($ip_assignment_method == "4")
	      )
              {
                 # reset to default
                 $ip_assignment_method = "1";
                 # send 'em back through the wringer
                 $satisfied="n" ;
              }
    }

# turn number values into useable string values
if   ($ip_assignment_method == "1") { $ip_assignment_method = "static_dhcp" ; }
elsif($ip_assignment_method == "2") { $ip_assignment_method = "dynamic_dhcp"      ; }
elsif($ip_assignment_method == "3") { $ip_assignment_method = "static"; }
elsif($ip_assignment_method == "4") { $ip_assignment_method = "replicant"   ; }
}
### END IP Assignment Method ###

### BEGIN create a fresh $autoinstall_script_dir/$image.master ###
SystemImager::Server->create_autoinstall_script(
	$master_script,
	$config_dir,
	$image,
	$imagedir,
	$ip_assignment_method,
	$post_install
);
### END create a fresh $autoinstall_script_dir/$image.master ###

# prompt to run mkdhcpstatic after autoinstalling clients if using static_dhcp
if((!$quiet) and ($ip_assignment_method eq "static_dhcp")) {
    system("clear");
    print << 'EOF';
Static DHCP Instructions
------------------------

You have chosen to use "Static DHCP" for your IP addressing.  Be sure to
run "mkdhcpstatic" after autoinstalling your client systems.  You will
need to autoinstall your client systems in the order that you want them
to be assigned their IP addresses.  When you run "mkdhcpstatic", it 
will re-write the DHCP server configuration file to permanently assign
to each client the same IP address that client received during it's 
autoinstall process.

EOF

    # and then give 'em a chance to read the message
    print "\nPress <Enter> to continue...";
    <STDIN>;
    system("clear");
}

# prompt to run "addclients"
if (!$quiet) {
    print "Would you like to run the \"addclients\" utility now? (y/[n]): ";
    $continue = SystemImager::Common->get_response('n');
    ($continue ne "n") or exit 0;
    exec("addclients");
}
