Article Listing 1 oct2005.tar

Listing 1 autoconfig.pl

#!/usr/bin/perl
# prepares TFTP directory, *simple* DHCP, and *simple* DNS for server # install.
# stdin (or file argument) needs to have space-delimited file:
#  hostname MAC IP TFTP-server-IP pxeconfig-profile-name
# use as "perl autoconfig.pl --help"
use strict;
use warnings;
use Getopt::Long;
use File::Copy qw(mv cp);
use File::Path qw(mkpath);

$ENV{PATH}="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin";
our $DNS_DIR = "/var/named";
our $DNS_SERVER = "dns.example.com";
our $DNS_SERVER_IP = "10.1.1.1";
our $TFTP_ROOT = "/tftpboot";
our $MASTER_INSTALL_DIR = "$TFTP_ROOT/install";
our @INSTALL = ();
our %subnets;
our $COUNT = 1;
our $NAMED_CONF = "/etc/named.conf";
our $DHCPD_CONF = "/etc/dhcpd.conf";
our $CFENGINE_KEYS = "/var/cfengine/ppkeys";

GetOptions( "dns-dir=s" => \$DNS_DIR,
            "dns-server=s" => \$DNS_SERVER,
            "install=s" => \@INSTALL,
            "usage" => \&usage,
            "help" => \&usage,
          ) or exit 1;

our %INSTALL = map { $_, 1 } @INSTALL;

# MAIN

initialize_dhcp();
initialize_dns();

while( my $machine = read_machine() ) {
  add_hashinfo( $machine );
  do_dhcp_config( $machine);
  do_dns_config( $machine);
  do_install( $machine ) if install_p( $machine );
}

finalize_dhcp();
finalize_dns();

# END MAIN

sub soa {
  my $zone = shift;
  <<"_SOA_";
@ IN SOA $DNS_SERVER root.example.com ( 1 3600 1800 604800 86400 )
@ NS $DNS_SERVER.
_SOA_
}

sub install_p {
  my $machine = shift;

  if ( $INSTALL{ $machine->{name} }
       or $INSTALL{ $machine->{short_name} }
       or $INSTALL{ $machine->{ip} }
       or $INSTALL{ $machine->{mac} } ) {
    return 1;
  } else {
    return 0;
  }
}

sub ip_to_hex {
  return join '',
    map { uc sprintf("%02x", $_ ) }
      split /\./, shift;
}

sub do_install {
  my $machine = shift;
  my $ip = $machine->{ip};
  my $hex_ip = ip_to_hex( $ip )
    or die "Couldn't calculate hex IP from '$ip'\n";

  print "installing $ip ($hex_ip)\n";
  my $in_file = "$MASTER_INSTALL_DIR/$machine->{pxe}";
  die "PXE type '$machine->{pxe}' does not have a corresponding file \
    '$in_file'!\n" unless -e $in_file;
  my $out_file = "$TFTP_ROOT/pxelinux.cfg/$hex_ip";
  cp ( $in_file, $out_file )
    or die "Couldn't copy: $!\n";
  chmod 0666, $out_file;

  my $cfengine_key = "$CFENGINE_KEYS/root-$ip.pub";
  unlink $cfengine_key if ( -e $cfengine_key);

  unless ( -s $in_file == -s $out_file ) {
    die "Copy from '$in_file' to '$out_file' seems to have failed\n";
  }
}

sub read_machine {
  while (<>) {
    s/#.*//; s/^\s+//; s/\s+$//;
    next unless $_;
    my @machine = split /\s+/;
    return { name => $machine[0],
             mac => $machine[1],
             ip => $machine[2],
             dhcp => $machine[3],
             pxe => $machine[4],
             disk => $machine[5],
           };
  }
  return;
}

sub add_hashinfo {
  my $machine = shift;
  ( my $short_host = $machine->{name} ) =~ s|\..*||;
  $machine->{short_name} = $short_host;
}

# DHCP
sub initialize_dhcp {
  open *main::DHCP, "> $DHCPD_CONF"
    or die "Can't open '$DHCPD_CONF': $!\n";
  print main::DHCP "ddns-update-style none;\n\n";
}


sub do_dhcp_config {
  my $machine = shift;

  (my $subnet = $machine->{ip} ) =~ s/\.\d+$//;
  unless ( $subnets{$subnet}++ ) {
    print main::DHCP "subnet $subnet.0 netmask 255.255.255.0 {}\n\n";
  }

  ( my $router = $machine->{ip} ) =~ s/\d+$/254/;

  print main::DHCP << "_END_MACHINE_";
host $COUNT {
    option routers $router;
    option host-name "$machine->{name}";
    option domain-name-servers $DNS_SERVER_IP;
    hardware ethernet $machine->{mac};
    fixed-address $machine->{ip};
    filename "/pxelinux.0";
    next-server $machine->{dhcp};
}
_END_MACHINE_
  $COUNT++
}

sub finalize_dhcp {
  system("service dhcpd restart");
}


# DNS
sub initialize_dns {
  opendir D, $DNS_DIR
    or die "Can't open '$DNS_DIR': $!\n";
  while( my $f = readdir D ) {
    my $path = "$DNS_DIR/$f";
    next unless -f $path;
    unlink $path;
  }
}

sub write_to_zone {
  my ($zone, $data) = @_;

  open F, ">> $DNS_DIR/$zone" or die "Can't open '$DNS_DIR/$zone': $!\n";
  print F $data;
}

sub do_dns_config {
  my $machine = shift;
  my @rev_ip = reverse split /\./, $machine->{ip};
  my $last_octet = shift @rev_ip;
  my $zone = join '.', @rev_ip, qw(in-addr arpa);

  write_to_zone( $zone, "$last_octet IN PTR $machine->{name}.\n" );

  my @rev_name = split /\./, $machine->{name};
  my $last_name = shift @rev_name;
  $zone = join '.', @rev_name;

  write_to_zone( $zone, "$last_name IN A $machine->{ip}\n" );

}

sub finalize_dns {
  opendir D, $DNS_DIR
    or die "Can't open '$DNS_DIR': $!\n";
  my @zones;

  while( my $zone = readdir D ) {
    my $path = "$DNS_DIR/$zone";
    next unless -f $path;
    push @zones, $zone;
    my $tmp_path = $path . ".tmp";

    mv ($path, $tmp_path);

    open F, "> $path" or die "Can't open '$path': $!\n";
    print F soa( $zone );
    open TMP, "$tmp_path" or die "Can't open '$tmp_path': $!\n";
    print F $_ while (<TMP>);
    close F;
    close TMP;
    unlink $tmp_path;
  }

  open CONF, "> $NAMED_CONF" or die "Can't open '$NAMED_CONF': $!\n";
  print CONF qq{options { directory "$DNS_DIR"; };\n\n};
  foreach my $zone ( @zones ) {
    print CONF <<"_ZONE_";
zone "$zone" in {
  type master;
  file "$zone";
};

_ZONE_
  }
  system("service named restart");
}

sub usage {
  die <<"_USAGE_";
usage: $0
          [ --dns-dir=/path/to/dns ]
          [ --dns-server=x.y.z.example.com ]
          [ --install=server.example.com
          [ --install=server2.example.com ... ] ]
          hosts.conf
          | --help | --usage
_USAGE_
}