#! /usr/bin/perl

# $Id: fai-chboot 3412 2006-04-15 21:37:24Z lange $
#*********************************************************************
#
# fai-chboot -- manage configuration for network boot
#
# This script is part of FAI (Fully Automatic Installation)
# Copyright (C) 2003-2006 Thomas Lange, lange@informatik.uni-koeln.de
# Universitaet zu Koeln
#
#*********************************************************************
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# A copy of the GNU General Public License is available as
# '/usr/share/common-licences/GPL' in the Debian GNU/Linux distribution
# or on the World Wide Web at http://www.gnu.org/copyleft/gpl.html.  You
# can also obtain it by writing to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#*********************************************************************

# variable needed: $nfsroot
$version="version 2.1.2 15-april-2006";

use Socket;
use Net::hostent;
use Getopt::Std;
use File::Copy;

$Getopt::Std::STANDARD_HELP_VERSION=1;

$0=~ s#.+/##; # remove path from program name
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub ip2hex {

  my $ipadr = shift;
  my $hex = sprintf("%02X%02X%02X%02X", split(/\./,$ipadr));
  return ($ipadr,$hex);
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub host2hex {

  my $host = shift;

  if ($host =~ /^\d+\.\d+\.\d+\.\d+$/) {
    # hostname is already an IP address
    return ip2hex($host);
  }
  return ('no IP','default') if ($host =~ /^default/);

  my $h = gethost($host);
  die "$0: unknown host: $host\n" unless $h;

  if ( @{$h->addr_list} > 1 ) {
    my $i;
    for my $addr ( @{$h->addr_list} ) {
      $ipadr = inet_ntoa($addr);
      printf "$host \taddr #%d is [%s]\n", $i++, $ipadr if $debug;
    }
  } else {
    $ipadr = inet_ntoa($h->addr);
    printf "$host \taddress is [%s]\n", $ipadr if $debug;
  }
  ip2hex($ipadr);
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub readpxedir {

  # read all files in pxedir and add them to different arrays

  opendir(DIR, $pxedir) || die "can't opendir $pxedir: $!";
  foreach (readdir(DIR)) {
    next if /^\./;
    if (/^default/)    { push @default,  $_ ; next};
    if (/^[0-9A-F]+$/) { push @enabled,  $_ ; next};
    if (/\.disable$/)  { push @disabled, $_ ; next};
    if (/\.tmpl$/)     { push @templates, $_ ; next};
    push @other, $_;
  }
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub lsdir {

# -lr list only disabled hosts
# -le list only enabled hosts
# -lt list only templates
# -lo list only "other", i.e that does not match templates or hosts 
# -lg list by goups (enabled, disabled, templates, others)
# -l[retog] <pattern> list matching pattern

  my (@n,$host,$ip,$iaddr,$hex,$type);

  @patterns = @_; # a global variable

  readpxedir();

  # create list which entries we want to list
  @allfiles = (@default,@enabled,@disabled,@templates,@other);
  $opt_r and @allfiles = @disabled;
  $opt_e and @allfiles = @enabled;
  $opt_t and @allfiles = @templates;
  $opt_o and @allfiles = @other;
  $opt_g and @allfiles = (@default,@enabled,@disabled,@templates,@other);

  # map all entries (in HEX) to hostname or IP
  foreach $hex (@allfiles) {
    undef $type;
    # hex to ip address
    @n = $hex =~ m/[0-9A-F][0-9A-F]/g;
    $ip = join ('.', map {$_ = hex $_} @n);

    if ($hex =~ /^default$/) {
      $host = $hex;
      $type = '[DEFAULT]';
    } elsif ($hex =~ /\.tmpl$/) {
      $host = "$hex";
      $type = '[Template]';
    } elsif ($#n > 0 && $#n < 3) {
      # Don't fail if not a complete IP
      $host = "Subnet: $ip";
    } else {
      # ip to hostname
      $iaddr = inet_aton($ip);
      if ($h = gethostbyaddr($iaddr, AF_INET)) {
	$host = $h->name;
	$host =~ s/^([^.]+).*/$1/; # strip domain from FQDN so we have short hostnames
      } else {
        $host = $hex;
        $type = '[Other]';
      }
    }
    $hexname{$host} = $hex;
    $hname{$hex}= $host;
    $type{$hex} = $type if $type;
  }

  if ($opt_g) { # print in group, sorted inside each group
    prtsorted(@default);
    prtsorted(@enabled);
    prtsorted(@disabled);
    prtsorted(@templates);
    prtsorted(@other);
    exit 0;
  }

  prtsorted(keys %hname);
  exit 0;
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub prtsorted {

  my @list = @_;

  @list = sort map {$hname{$_}} @list;
  foreach (@list) {
    printpxe ($_,$hexname{$_}, $type{$_});
  }
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub printpxe {

  my ($host,$hex, $type) = @_;
  my ($kernelname,$append);

  $match = (@patterns) ? 0: 1; # set match to 1 if no pattern is given
  foreach (@patterns) {
     $match = 1 if $host =~ /$_/;
  }
  return unless $match; # do not print entries if no pattern matches

  # read pxe config file for a host
  undef $kernelname;
  open (CFG,"$pxedir/$hex") || die "$! $@\n";
  while (<CFG>) {
    /\bkernel\s+(\S+)/ and $kernelname = $1;
    /\b(localboot.+)/ and $kernelname = $1;
    /\bappend\s+(.+)/ and $append = $1;
  }
  close (CFG);

  if ($opt_l && ! $opt_L) {
    $append =~ /FAI_ACTION=(\S+)/;
    $append = $1;
#    printf "%-16.16s $append $kernelname %-8s\n",$host,$hex;
    printf "%-16.16s  %-20.20s $append $kernelname\n",$type?$type:$host,$hex;
  } else {
    printf "%s %s $kernelname $append\n",$host,$hex;
  }

  undef $append;
  undef $kernelname;
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub usage {

  &VERSION_MESSAGE;
  &HELP_MESSAGE;
  exit 0;
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub VERSION_MESSAGE {

  print "$0 $version\n";
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub HELP_MESSAGE {

  print << "EOM";
 Please read the manual pages fai-chboot(8).
EOM
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub mkpxecfg {

  my ($host,$kernel,$rootfs) = @_;
  my ($ipadr,$hex) = host2hex($host);

  warn "$host has $ipadr in hex $hex\n" if $verbose;
  warn "Writing file $pxedir/$hex for $host\n" if $verbose;
  return if $opt_n;

  if ($opt_p && -e "$pxedir/$hex") {
    warn "WARNING: $pxedir/$hex already exists. Skipping file creation. ";
    return;
  }
  open (FILE,"> $pxedir/$hex") or warn "$0 $@ $!";
  print FILE << "EOM";
# generated by fai-chboot for host $host with IP $ipadr
default fai-generated

label fai-generated
$kernel
EOM

  $append="append $bootprot root=$rootfs $opt_k $flags\n";
  print FILE $append if $rootfs;
  close FILE;
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub disable {

  # rename network config file
  my ($host) = shift;
  my ($ipadr,$hex) = host2hex($host);
  print "disable pxe config for $host in hex $hex\n" if $verbose;
  return if $opt_n;
  rename "$pxedir/$hex","$pxedir/$hex.disable" or $error .= "\nRename for $hex failed. $! $@";
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub enable {

  # rename network config file
  my ($host) = shift;
  my ($ipadr,$hex) = host2hex($host);

  -e "$pxedir/$hex" and print "$host ($hex) is already enabled\n" and return;
  if (! -e "$pxedir/$hex.disable") {
    print "$host ($hex) is not disabled\n";
    return;
  }

  print "reenable pxe config for $host in hex $hex\n" if $verbose;
  return if $opt_n;
  rename "$pxedir/$hex.disable","$pxedir/$hex" or $error .= "\nRename for $hex failed. $! $@";
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub tcopy {

  my ($srchost,$desthost) = @_;
  my ($ipadr,$srcfile,$srchex,$desthex);

  if (gethost($srchost)) {
    ($ipadr,$srchex) = host2hex($srchost);

    if (-e "$pxedir/$srchex") {
      $srcfile = "$pxedir/$srchex";
    } elsif (-e "$pxedir/$srchex.disable") {
      $srcfile = "$pxedir/$srchex.disable";
    } elsif (-e "$pxedir/$srchost" ) {
      $srcfile = "$pxedir/$srchost";
    } elsif (-e "$pxedir/$srchost.tmpl" ) {
      $srcfile = "$pxedir/$srchost";
    } else {
      warn "Source file for $srchost ($srchex) not available\n";
      return;
    }
  } elsif ( -e "$pxedir/$srchost") {
      $srcfile = "$pxedir/$srchost";
  } elsif ( -e "$pxedir/$srchost.tmpl") {
      $srcfile = "$pxedir/$srchost";
  } else {
      warn "Source file for $srchost not available\n";
      return;
  }
  if ($desthost =~ /\.tmpl$/) {
    if (-e "$pxedir/$desthost") {
      warn "Template $desthost already exist. Copying aborted.\n";
      return;
    }
    print "copy pxe config from $srchost to template $desthost\n" if $verbose;
    copy("$srcfile","$pxedir/$desthost") or $error .= 
        "\nCopy of $srchost ($srchex) to template $desthost failed.$! $@";
  } else {
    ($ipadr,$desthex) = host2hex($desthost);
    if (-e "$pxedir/$desthex.disable") {
       warn "Config for $desthost is currently disabled. Copying aborted.\n";
       return;
    }
    print "copy pxe config from $srchost to $desthost ($desthex)\n" if $verbose;
    copy("$srcfile","$pxedir/$desthex") or $error .= 
        "\nCopy of $srchost ($srchex) to $desthost ($desthex) failed.$! $@";
  }
}
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
getopts('gBc:d:ehnvlLiIp:f:Frk:Sto');
$opt_h and usage;
defined @ARGV or usage;

$opt_n and $opt_v = 1;
$opt_v and $verbose = 1;
$pxedir = $opt_d || '/boot/fai/pxelinux.cfg';
$opt_L and $opt_l = 1;
$opt_l and lsdir(@ARGV);
($opt_B and $opt_F) && die "ERROR: use only one option out of -B and -F\n";
($opt_S and $opt_I) && die "ERROR: use only one option out of -I and -S\n";
#TODO: also -e, -r and -c can't be used together

if ($opt_r) {
  die "Missing host name(s). Can't disable network booting.\n" unless @ARGV;
  foreach (@ARGV) {
    disable("$_");
  }
  $error and die "$0: $error\n";
  exit 0;
}

if ($opt_c) {
  die "Missing destination host name(s). Can't copy.\n" unless @ARGV;
  # copy a template config to multiple hosts
  foreach (@ARGV) {
    tcopy($opt_c,$_);
  }
  $error and die "$0: $error\n";
  exit 0;
}

if ($opt_e) {
  die "Missing host name(s). Can't reenable network booting.\n" unless @ARGV;
  foreach (@ARGV) {
    enable("$_");
  }
  $error and die "$0: $error\n";
  exit 0;
}

if ($opt_S) {
  $opt_i = 1;
  $action="FAI_ACTION=sysinfo";
}
if ($opt_I) {
  $opt_i = 1;
  $action="FAI_ACTION=install";
}

# read the nfsroot variable; a little bit ugly, but it works
$nfsroot = `. /etc/fai/fai.conf 2>/dev/null; echo \$NFSROOT`;
chomp $nfsroot;
$nfsroot = '/usr/lib/fai/nfsroot' unless $nfsroot;

if ($opt_i) {
  # create config so host will boot the install kernel
  $kernelname = 'kernel vmlinuz-install';
  $rootfs     = "/dev/nfs nfsroot=$nfsroot,v2,rsize=32768,wsize=32768";
  $bootprot   = "ip=dhcp devfs=nomount $action";
} elsif ($opt_o) {
  $kernelname = 'localboot 0';
  $rootfs   = '';
  $bootprot = '';
  $flags    = '';
} else {
  $kernelname = shift;
  $kernelname = "kernel $kernelname";
  $rootfs = shift;
}

$opt_F and $opt_f="verbose,sshd,createvt";
$opt_B and $opt_f="verbose,sshd,reboot";
$opt_f and $flags="FAI_FLAGS=$opt_f";
warn "append parameters: $bootprot $opt_k $flags\n" if $verbose;
warn "Booting $kernelname\nrootfs is $rootfs\n" if $verbose;
$opt_k && print "Kernel parameters: $opt_k\n";

die "No host specified.\n" unless @ARGV;
foreach (@ARGV) { mkpxecfg("$_",$kernelname,$rootfs); }
