#!/usr/bin/perl -w

#
# "SystemImager" - Copyright (C) 1999-2001 Brian Elliott Finley <brian@systemimager.org>
#
#   Others who have contributed to this code (in alphabetical order):
#     Adam L. Lambert <alambert@epicrealm.com> (credit for the -autoinstall option goes to Adam)
#
# This file is: updateclient
#

# set version_number
$version_number="2.0.1";

# declare modules
use Getopt::Long;
use Socket;
use Sys::Hostname;


### BEGIN Subroutines ###
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_bin {
    (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 join('', $a_bin, $b_bin, $c_bin, $d_bin);
}

sub ip_bin2ip_quad {
    my $ip_bin = $_[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);
}
### END Subroutines ###


# set version information
$version_info = <<"EOF";
updateclient (part of SystemImager) version $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

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

Usage: updateclient [OPTION]... -server HOSTNAME -image IMAGENAME
  or   updateclient -server HOSTNAME -listing

Options: (options can be presented in any order)
 -help                    Display this output.

 -version                 Display version and copyright information.

 -server HOSTNAME         Hostname or IP address of the imageserver.

 -image IMAGENAME         Image from which the client should be updated.

 -directory DIRECTORY     Absolute path of directory to be updated.
                          (defaults to "/")

 -nolilo                  Don't run lilo after update completes.
                          (lilo is always run unless specified)

 -autoinstall             Autoinstall this client the next time it
                          reboots.  (can't be run with -nolilo)

 -configure-from DEVICE   Only used with -autoinstall.  Stores the
                          network configuration for DEVICE in the
                          /local.cfg file so that the same settings
                          will be used during the autoinstall process.

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

 -listing                 List avaliable images on the imageserver.
                          (supercedes and ignores all options other
			   than -server)

 -reboot                  Reboot client after update completes.
    
 -dry-run                  Only shows what would have been updated. 

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

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

# interpret command line options
GetOptions( "listing" => \$listing,
            "configure-from=s" => \$DEVICE,
            "help" => \$help,
            "version" => \$version,
            "reboot" => \$reboot,
            "nolilo" => \$nolilo,
            "autoinstall" => \$autoinstall,
            "dry-run" => \$dry_run,
            "ssh-user=s" => \$ssh_user,
            "server=s" => \$imageserver,
            "image=s" => \$image,
            "directory=s" => \$directory,
	    "log=s" => \$log,
) 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 "Imageserver name can\'t start with a hyphen.\n\n$help_info"; }
}

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

# if name usage is bad, print help information
unless( ($imageserver and $image) or ($imageserver and $listing) or ($imageserver and $autoinstall) ) { 
  die qq($help_info);
}

# -autoinstall and -nolilo conflict
if($autoinstall and $nolilo) { 
  die qq($help_info);
}

# -autoinstall and -dry-run conflict
if($dry_run and $autoinstall) { 
  print qq($help_info);
  die "\nautoinstall and dry-run don't like each other\n\n";
}

# -nolilo and -dry-run: latter one implies the first one
if(($dry_run) and !($nolilo)) { 
	$nolilo=1; 
}
	
# -configure-from requires -autoinstall
if($DEVICE) {
  unless ($autoinstall) { 
    die "$help_info"; 
  }
}

# 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:$imageserver:873 $imageserver sleep 5";
  $rc = 0xffff & system($command);
  if ($rc != 0) { die "FATAL: Failed to establish secure port forwarding to $imageserver!\n"; }  

  # and change imageserver to point to localhost
  $imageserver="127.0.0.1";
}

# if listing, get listing
if($listing) {
  print "Available image(s):\n";
  $command = "rsync rsync://${imageserver}:${port}/";
  $rc = 0xffff & system($command);
  exit $rc >> 8;
}

# if not run as root, updateclient will surely fail
if($< != 0) { die "Must be run as root!\n"; }

### BEGIN update image section ###
if ($autoinstall) {
  # suck down kernel 
  print "Retrieving SystemImager kernel...\n";
  # if the write fails for some reason (read only filesystem, full filesystem, etc.) rsync will 
  # not return a proper exit code -- a bug report has been submitted <brian@systemimager.org>
  $command = "rsync -a --numeric-ids rsync://${imageserver}:${port}/tftpboot/kernel /boot";
  $rc = 0xffff & system($command);
  if ($rc != 0) { die "Failed to copy SystemImager kernel to /boot on client!!!\n"; }  

  # and initial ram disk image
  print "Retrieving SystemImager initial ramdisk...\n";

  # if the write fails for some reason (read only filesystem, full filesystem, etc.) rsync will 
  # not return a proper exit code -- a bug report has been submitted <brian@systemimager.org>
  $command = "rsync -a --numeric-ids rsync://${imageserver}:${port}/tftpboot/initrd.gz /boot";
  $rc = 0xffff & system($command);
  if ($rc != 0) { die "Failed to copy SystemImager initrd.gz file to /boot on client!!!\n"; }  

  # find out on which partition the current root file system resides
  $file="/etc/mtab";
  open(MTAB, "< $file") or die "Couldn't open $file for reading: $!\n";
  while (<MTAB>) {
    # turn tabs into spaces (not a normal space --> <ctrl>+<v> then <tab> )
    $_ =~ s/	/ /g;
    # turn multiple spaces into single spaces
    $_ =~ s/ +/ /g;
    if(/ \/ /){
      my @fields = split(/ /, $_);
      $last_root=$fields[0];
    }
  }
  close(MTAB);

  ### BEGIN lilo.conf file stuff ###
  $file = "/etc/lilo.conf";

  # test to see if an entry already exists
  open (LILO_CONF, "< $file") or die "Couldn't open $file for reading: $!\n";
    while(<LILO_CONF>) {
      chomp;
      if( /label=systemimager/ ) { 
        print "Warning: SystemImager entry already exists.  Using existing entry...\n";
        $lilo_entry_exists="true";
      };
    };
  close (LILO_CONF);

  # if not, add one
  unless ($lilo_entry_exists) {
    print "Adding SystemImager entry in $file...\n";
    open (LILO_CONF, ">> $file") or die "Couldn't open $file for writing: $!\n";
    print LILO_CONF << "EOF";

image=/boot/kernel
      label=systemimager
      initrd=/boot/initrd.gz
      read-write
      # appending root=/dev/ram0 ensures that the kernel will 
      # not try to run the non-existent /linuxrc script
      append="LAST_ROOT=$last_root root=/dev/ram0 load_ramdisk=1 prompt_ramdisk=0"
EOF
    close (LILO_CONF);
  }
 
  ### END lilo.conf file stuff ###

  # and tell it to use the systemimager kernel on the next boot
  my $lilo_command = "/sbin/lilo -d 50 -D systemimager";
  print "running $lilo_command ...\n";
  $rc = 0xffff & system("$lilo_command");
  if ($rc != 0) {print "$lilo_command failed!!!\n"; exit $rc >> 8;}  
      
} else {
  # go ahead and do that update thing you do
  # set directory to default if not specified on command line
  if(!$directory) {
    $directory = "/";
  } else {
    # in case directory doesn't already have one, add a trailing slash
    # (We could easily test to see if it does, but we've got to normalize multiple
    #  slashes into one down below anyway...  Same number of functions to execute.)
    $directory = $directory . "/";

    # turn multiple slashes "///" into a single slash "/"
    # (So maybe we don't really have to do this -- things will work with multiple
    #  slashes.  This is actually done to avoid user consternation when reading
    #  any output that might include the multiple slashes.)
    $directory =~ s/\/+/\//g;
  }

  # start with base command
  $command = "rsync -av --numeric-ids";
  if ($dry_run) {
      $command = $command . qq( --dry-run) ;
  }

  if ($log) { $command = $command . qq( --log-format="$log"); }

  # get exclusions from /etc/systemimager/updateclient.local.exclude
  $systemimager_exclude = "/etc/systemimager/updateclient.local.exclude";
  open(SYSTEMIMAGER_EXCLUDE, "< $systemimager_exclude") or die "Couldn't open $systemimager_exclude for reading: $!\n";
  while (<SYSTEMIMAGER_EXCLUDE>) {
      if (/^\s*\//) {  # match non commented explicit path
	chomp;
	$command = $command . qq( --exclude=$_);
      }
  }
  close(SYSTEMIMAGER_EXCLUDE);

  # append currently mounted non ext2 filesystems to exclusions list
  open(ETC_MTAB, "< /etc/mtab") or die "Couldn't open /etc/mtab for reading: $!\n";
  while (<ETC_MTAB>) {
      chomp;
      @fields = split(/ /, $_);
      unless( ($fields[2] eq "ext2") or ($fields[1] eq '/proc') or ($fields[1] eq '/dev/pts') ) {
	$command = $command . qq( --exclude=$fields[1]);
      }
  }
  close(ETC_MTAB);

  # finalize command
  $command = $command . " --delete rsync://${imageserver}:${port}/$image$directory $directory";

  # execute command
  if ($dry_run) {
  	print qq(dryrun against image...\n); }
  else {
  	print qq(Updating image...\n);
  }
  $rc = 0xffff & system($command); 
  if ($rc != 0) { exit $rc >> 8; }  

  # lilo
  unless($nolilo) { 
      print qq(running lilo...\n);
      $rc = 0xffff & system("/sbin/lilo");
      if ($rc != 0) { exit $rc >> 8; }  
  }
} ### END update image section ###


### BEGIN configure-from section ###
if($DEVICE){
  ### BEGIN get ip address, netmask, and broadcast from the ifconfig command ###
  $command = "ifconfig $DEVICE";
  open(COMMAND, "$command|");
  while (<COMMAND>) {
  if (/inet addr/) { 
  s/[A-Za-z:]//g;
  s/^ +//g;
  chomp;
  ($IPADDR, $BROADCAST, $NETMASK) = split(/ +/);
  }
  }
  close(COMMAND);
  ### END get ip address, netmask, and broadcast from the ifconfig command ###


  ### BEGIN get interface and gateway from the route command ###
  $GATEWAYDEV=$DEVICE;
  $command = "route -n";
  open(COMMAND, "$command|");
  while (<COMMAND>) {
  if ((/$GATEWAYDEV/) and (/UG/)) { 
  chomp;
  (my $destination, my $gateway, my $genmask, my $flags, my $metric, my $ref, my $use, my $iface) = split(/ +/);
  $GATEWAY=$gateway;
  }
  }
  close(COMMAND);
  ### END get interface and gateway from the route command ###


  ### BEGIN get hostname ###
  $HOSTNAME = hostname();
  my @fields = split(/\./, $HOSTNAME);
  $HOSTNAME = $fields[0];
  ### END get hostname ###


  ### BEGIN get ip address of imageserver if necessary ###
  if($imageserver =~ /[A-Za-z]/) {
  $IMAGESERVER = inet_ntoa(scalar gethostbyname("$imageserver"));
  } else {
  $IMAGESERVER = $imageserver;
  }
  ### BEGIN get ip address of imageserver if necessary ###


  ### BEGIN get domainname ###
  $command = "dnsdomainname";
  open(COMMAND, "$command|");
  while (<COMMAND>) {
  chomp;
  $DOMAINNAME=$_;
  }
  close(COMMAND);

  # if dnsdomainname didn't work, try grokking /etc/resolv.conf
  unless($DOMAINNAME) {
  $file="/etc/resolv.conf";
  open(FILE, "< $file") or die "Couldn't open $file for reading: $!\n";
  while(<FILE>) {
  chomp;
  # turn all tabs into single spaces -- Note: <ctrl-v><tab>
  s/	/ /g;
  if(/domain/) {
  (my $junk, $DOMAINNAME) = (split / +/);
  } elsif(( ! $DOMAINNAME) and (/search/)) {
  (my $junk, $DOMAINNAME) = (split / +/);
  }
  }
  close(FILE);
  }
  ### END get domainname ###


  ### BEGIN Calculate network number ###
  $IPADDR_BIN  = ip_quad2ip_bin($IPADDR);
  $NETMASK_BIN = ip_quad2ip_bin($NETMASK);
  $NETWORK_BIN = $IPADDR_BIN & $NETMASK_BIN;
  $NETWORK     = ip_bin2ip_quad($NETWORK_BIN);
  ### END Calculate network number ###


  ### BEGIN Create local.cfg ###
  $file="/local.cfg";
  open(FILE, "> $file") or die "Couldn't open $file for writing: $!\n";
  print FILE "#\n";
  print FILE "# \"SystemImager\" - Copyright (C) 1999-2001 Brian Elliott Finley <brian\@systemimager.org>\n";
  print FILE "#\n";
  print FILE "# This file is: /local.cfg\n";
  print FILE "#\n";

  print FILE "HOSTNAME=";
  if ($HOSTNAME) { print FILE    "$HOSTNAME\n"; } else { print FILE "\n"; }

  print FILE "DOMAINNAME=";
  if ($DOMAINNAME) { print FILE  "$DOMAINNAME\n"; } else { print FILE "\n"; }

  print FILE "DEVICE=";
  if ($DEVICE) { print FILE      "$DEVICE\n"; } else { print FILE "\n"; }

  print FILE "IPADDR=";
  if ($IPADDR) { print FILE      "$IPADDR\n"; } else { print FILE "\n"; }

  print FILE "NETMASK=";
  if ($NETMASK) { print FILE     "$NETMASK\n"; } else { print FILE "\n"; }

  print FILE "NETWORK=";
  if ($NETWORK) { print FILE     "$NETWORK\n"; } else { print FILE "\n"; }

  print FILE "BROADCAST=";
  if ($BROADCAST) { print FILE   "$BROADCAST\n"; } else { print FILE "\n"; }

  print FILE "GATEWAY=";
  if ($GATEWAY) { print FILE     "$GATEWAY\n"; } else { print FILE "\n"; }

  print FILE "GATEWAYDEV=";
  if ($GATEWAYDEV) { print FILE  "$GATEWAYDEV\n"; } else { print FILE "\n"; }

  print FILE "IMAGESERVER=";
  if ($IMAGESERVER) { print FILE "$IMAGESERVER\n"; } else { print FILE "\n"; }

  if ($ssh_user) {
    print FILE "SSH_USER=$ssh_user\n";
  }

  close(FILE);
  ### END Create local.cfg ###


  ### BEGIN show file to user ###
  print "\n";
  print "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n";
  print "Below are the contents of your /local.cfg file.  Make sure that all\n";
  print "the variables are filled in and that they contain the proper values.\n";
  print "You may edit the file directly if you need to change any of the values.\n";
  print "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n";
  print "\n";

  $file="/local.cfg";
  open(FILE, "< $file") or die "Couldn't open $file for reading: $!\n";
    while (<FILE>) { print; }
  close(FILE);
  ### END show file to user ###
}
### END configure-from section ### 

### BEGIN graffiti ###
if($image)
{
  $file="/etc/systemimager/IMAGE_LAST_SYNCED_TO";
  open (FILE, "> $file") or die "Couldn't open $file for writing: $!\n";
    print FILE "$image\n";
  close (FILE);
}
### BEGIN graffiti ###


# reboot
if($reboot) { 
    print qq(rebooting...\n);
    $rc = 0xffff & system("/bin/sleep 10s; /sbin/init 6");
    if ($rc != 0) { exit $rc >> 8; }  
}

exit 0;
