#!/usr/local/bin/perl

# update-dns.pl
#   Generates a bootptab from the information in the HINFO fields
#     within a DNS database file and a header stub. Also generates
#     all appropriate in-addr.arpa files.
#
#   Expected input:
#     Path to a domain file (d|domain)
#     Path to an ISC-formatted bootptab file (i|isc)
#     Path to a CMU-formatted bootptab file (c|cmu)
#     Path to the bootptab file we'll install
#       CMU: (oc|install-cmu)
#       ISC: (oi|install-isc)
#
#   Expected output:
#     Updated bootptab files (same selection as fed as input)
#     An ISC- or a CMU-formatted bootptab installed
#     All appropriate in-addr.arpa files
#     A list of all free addresses within known ranges
#
#
#   05/03/2000 Version 1.4.2a
#   Use and distribute this script as per the Artistic License
#   Copyright (C) 2001 Igor S. Livshits <mailto:igorl@ayradyss.org>


# Define some global variables
#
$true= (1==1);
$false= (1==0);
$commentDelimiter= "#";
$dnsCommentDelimiter= ";";
$directoryDelimiter= "\/";
$nodeDelimiter= "\.";
$nodeDelimiterMatch= '\.';
$newLine= "\n";
$tab= "\t";
$dnsFieldDelimiter= $tab;
$dnsInformationField= "HINFO";
$space= " ";
$psCommand= "ps -eaf -o pid,user,time,stime,comm |";
$psCommandLinux= "ps ax |";
$iscDHCPConfFile= "/etc/dhcpd.conf";
$iscEthernetAddressDelimiter= ":";
$iscOptionTag= "option";
$iscRoutersTag= "routers";
$iscDomainTag= "domain-name";
$iscMaskTag= "subnet-mask";
$iscListDelimiter= ",";
$iscListTerminator= ";";
$bootptabFile= "/etc/bootptab";
$newFileSuffix= ".new";		# suffix for the generated files
$oldFileSuffix= ".old";		# suffix for preserved copies
$domainFilePrefix= "db.";	# prefix for in-addr.arpa tables
$specialSuffix= ".special";	# for special entries
$spareAddressesSuffix= ".spares"; # for all spare addresses
$serialNumberFileSuffix= ".serial"; # stores previous serial number
$domainHeaderDelimiter= "; General header end";
$bootpHeaderDelimiter= "# Header end";
$serialNumberComment= "; Serial #";
$idTagOther= "Generated by ";
$idTag= "Generated by update-dns.pl ";
$dnsTag= "; DO NOT DELETE THIS LINE";
$dnsProcess= "named";		# name of our DNS daemon
$dhcpdProcess= "dhcpd";		# name of our DHCP daemon
$restartSignal= 'HUP';		# a kill signal to restart a process
$terminateSignal= 'TERM';	# a kill signal to terminate a process
$originARPARoot= ".in-addr.arpa.";
$originTag= "\$ORIGIN ";
$originTagMatch= '\$ORIGIN ';
$authorityTag= "IN\tSOA";
$ethernetAddressLength= 12;	# Ethernet addresses are 12 bytes long
$ethernetAddressIndex= 4;	# The last position of the HINFO line


# Define libraries and modules
#
use Getopt::Long;		# command line options processor


# Initialize
#
GetOptions("d|domain=s" => \$domainFile,
	   "i|isc=s" => \$iscFile,
	   "c|cmu=s" => \$cmuFile,
	   "oc|install-cmu" => \$installCMU,
	   "oi|install-isc" => \$installISC
	   );
&Initialize();
umask(0177);

# Generate our data
#
$newDNS= &Generate();


# Activate new data
#
&Activate();


# Terminate gracefully
#
exit;


#
# Subroutines
#


# Initialize
#
# Learn setting and configure defaults
#
sub Initialize
{
  my($lackingInput)= $false;	# input validity flag

  $lackingInput= $true, print "You did not specify a domain file", $newLine
    unless $domainFile;

  $lackingInput= $true, print "You did not specify any bootptab files",
  $newLine
    unless ($iscFile or $cmuFile);

  $lackingInput= $true,
  print "Cannot install or send an ISC dhcpd.conf file without a template", 
  $newLine
    if ($installISC and !$iscFile);

  $lackingInput= $true,
  print "Cannot install or send a CMU bootptab file without a template", 
  $newLine
    if ($installCMU and !$cmuFile);

  if ($lackingInput)
  {				# exit with a usage message
    print $newLine, "Usage:", $newLine,
    "update-dns.pl -d path/to/domain/file -i path/to/isc-dhcpd.conf -oi",
    $newLine,
    "update-dns.pl -d path/to/domain/file -c path/to/bootptab -oc",
    $newLine;
    exit;
  }

  if ($ENV{OSTYPE} eq "linux")	# tested under Red Hat 6.x
  {				# try using a Linux-flavored ps argument list
    $psCommand= $psCommandLinux;
  }
}


# Generate()
#   Cycles through the DNS entries, generating BootP entries, as appropriate 
# 
sub Generate
{
  local %subnets;		# an array of our subnets and their tags
  local %numbers= ();		# an array of all IP numbers
  my(%addresses)= ();		# an array of all ethernet addresses
  my(%names)= ();		# an array of all IP names
  my($address)= 0;		# temporary buckets for DNS info
  my($name, $formerNumber, $multiHomedName, $dummy, $number, $subnet);
  my($serialNumber, $oldSerialNumber, $validSerialNumber);
  my(@rest);			# temporary storage
  
  # Prepare the ISC dhcpd.conf file if one exists
  #   process subnet information only if we do not have a bootptab file
  %subnets= &SetupDHCP(!$cmuFile) if $iscFile;

  # Prepare and process the bootptab file if one exists
  %subnets= &SetupBootP() if $cmuFile;

  open(DNS, $domainFile) or die "Could not access $domainFile!$newLine";
  print $newLine, "Examining $domainFile...$newLine";
  open(BootPTab, ">>$cmuFile$newFileSuffix") # bootptab
    or die "Could not access $cmuFile$newFileSuffix!$newLine"
      if $cmuFile;
  open(DHCPdConf, ">>$iscFile$newFileSuffix") # dhcpd.conf
    or die "Could not access $iscFile$newFileSuffix!$newLine"
      if $iscFile;
  print BootPTab "$commentDelimiter$newLine" # just a separator
    if $cmuFile;
  print DHCPdConf "$commentDelimiter$newLine" # just a separator
    if $iscFile;
  
  while (<DNS>)
  {				# fast forward to useful info
    if (/$serialNumberComment$/)
    {				# but remember to grab the serial number
      ($serialNumber, @rest)= split();
    }
    last if (/^$dnsTag/);
  }

  $validSerialNumber= &ValidateSerialNumber($serialNumber);
  
  while (<DNS>)
  {
    next if (/^$dnsCommentDelimiter/); # skip comments

    if (/$dnsInformationField/)
    {				# grab the name and the ethernet address
      ($name, @rest)= split();	# the name is always first
      if ($#rest == $ethernetAddressIndex)
      {				# grab the ethernet address
	$address= @rest[$#rest];
	if ((length($address) != $ethernetAddressLength)
	    || ($address=~ /[^a-fA-F0-9]/))
	{
	  print $tab,
	  "Malformed ethernet address [$address] blasted.", $newLine;
	  $address= "";
	}
      }
      else { $address= ""; }

      unless ($name=~ /\w/)
      {				# have to have at least one alphanumeric
	print $tab, "Malformed IP name [$name] blasted.", $newLine;
	$name= "";
      }
    }
    if (/IN\s+A\s+/)
    {				# grab the IP number
      if (/^\s/)
      {				# name preserved from a previous line
	($dummy, $dummy, $number, @rest)= split();
      }
      else
      {				# ignore the redundant (multi-homed?) name
	($multiHomedName, $dummy, $dummy, $number, @rest)= split();
	$multiHomedName= ""	# have to have at least one alphanumeric
	  unless ($multiHomedName=~ /\w/);
      }

      if ($name)
      {				# we have both a name and a number
	if ($numbers{$number})
	{			# a new number, add its name to our list
	  print $tab,
	  "Duplicate IP number [$number] assigned to [$name];", $newLine,
	  $tab, $tab, "preserving original name [$numbers{$number}]...",
	  $newLine;
	}
	else
	{
	  $numbers{$number}= $name;
	  if ($names{$name})
	  {			# check for multi-home entries and report them
	    print $tab, "[$name] previously used for [$names{$name}]",
	    " (multi-homed?).", $newLine;
	  }
	  else
	  {			# remember this new name
	    $names{$name}= $number;
	  }
	  undef $name;		# clear for silence on multi-homed entries
	}
      }
      elsif ($multiHomedName)
      {
	if ($names{$multiHomedName})
	{			# check for multi-home entries and report them
	  print $tab, "[$multiHomedName] previously used for ",
	  "[$names{$multiHomedName}] (multi-homed?).", $newLine;
	}
	else
	{			# remember this new name
	  $names{$multiHomedName}= $number;
	}
	undef $multiHomedName;	# ignore silently for now
      }
      else
      {
	print $tab, "No name defined for [$number]!", $newLine;
      }
      
      if ($address)
      {				# we have all the info, make a bootptab entry
	$number=~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/;
	$subnet= "$1.$2.$3";
	if ($addresses{$address})
	{			# preserve original entry, but warn
	  print $tab, "Ethernet address [$address] used again for ";
	  print "$numbers{$number} " if $numbers{$number};
	  print "[$number];", $newLine, $tab, $tab,
	  "[$address] is already assigned to $addresses{$address}";
	  foreach $formerNumber (keys(%numbers))
	  {			# reverse lookup the IP number from a name
	    if ($numbers{$formerNumber} eq $addresses{$address})
	    {
	      print " [$formerNumber]";
	      last;
	    }
	  }
	  print ".", $newLine;
	}
	else
	{			# a brand new entry
	  $addresses{$address}= $numbers{$number};
	  if ($cmuFile)
	  {			# a bootptab (CMU) compliant entry
	    print BootPTab "$numbers{$number}:",
	    $tab, "ip=$number:",
	    $tab, "tc=$subnets{$subnet}:",
	    $tab, "ha=$address:", $newLine;
	  }
	  if ($iscFile)
	  {			# a dhcpd.conf (ISC) compliant entry
	    $address=~		# insert delimiters into the address
	      /(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)(\w\w)/;
	    $address=		# we've previously sanity-checked the address
	      join($iscEthernetAddressDelimiter, $1, $2, $3, $4, $5, $6);
	    print DHCPdConf "host $numbers{$number}", $newLine,
	    "{", $newLine,
	    $tab, "hardware ethernet $address;", $newLine,
	    $tab, "fixed-address $number;", $newLine,
	    "}", $newLine;
	  }
	  undef $address;	# invalidate for next iteration
	}
      }
    }
  }
  close(BootPTab) if $cmuFile;
  close(DHCPdConf) if $iscFile;
  close(DNS);

  # Preserve a copy of our originals
  #   and replace the originals with new versions
  if ($cmuFile)
  {
    rename($cmuFile, $cmuFile.$oldFileSuffix)
      or die "Could not preserve $cmuFile: $!";
    rename($cmuFile.$newFileSuffix, $cmuFile)
      or die "Could not rename $cmuFile$newFileSuffix: $!$newLine",
      "But continuing with the rest...$newLine";
  }
  if ($iscFile)
  {
    rename("$iscFile", "$iscFile$oldFileSuffix")
      or die "Could not preserve $iscFile: $!";
    rename("$iscFile$newFileSuffix", "$iscFile")
      or die "Could not rename $iscFile$newFileSuffix: $!$newLine",
      "But continuing with the rest...$newLine";
  }

  &GenerateARPA($domainFile, %numbers, %subnets);

  return $validSerialNumber;
}


# ValidateSerialNumber($filePath, $currentSerialNumber)
#   Reads in the previous serial number for a given domain 
#
sub ValidateSerialNumber
{
  my($currentSerialNumber)= shift;
  my($previousSerialNumber);

  return $false unless $currentSerialNumber;
  print "Current serial number:  ", $currentSerialNumber;
  if ($currentSerialNumber=~ /\D/)
  {				# must be an integer!
    print " *invalid*", $newLine;
    return $false;
  }
  else { print $newLine; }

  if (-e $domainFile.$serialNumberFileSuffix)
  {
    open(Serial, $domainFile.$serialNumberFileSuffix)
      or die "Could not access $domainFile$serialNumberFileSuffix$newLine";
    $previousSerialNumber= <Serial>; # read in the previous serial number
    close(Serial);
  }
  if ($previousSerialNumber)
  {
    print "Previous serial number: ", $previousSerialNumber, $newLine;
    $previousSerialNumber= 0 if $previousSerialNumber=~ /\D/;
  }
  else { $previousSerialNumber= 0; }
  open(Serial, ">$domainFile$serialNumberFileSuffix")
    or die "Could not access $domainFile$serialNumberFileSuffix$newLine";
  print Serial $currentSerialNumber; # preserve current serial number
  close(Serial);

  return ($previousSerialNumber < $currentSerialNumber);
}


# SetupDHCP()
#   Reads the header of the current dhcpd.conf seeking subnet information
#   Also preserves the header in the generated copy
#
#   Format compatible with ISC DHCPd
#
sub SetupDHCP
{
  my($parseSubnets)= shift;	# should I parse subnet data or just preserve
  my($filePath)= $iscFile;
  my(%subnets)= ();		# an array of legitimate subnets
  my($domain, $mask, $gateway, $subnet, $byte);
  my($one, $two, $three, $four, $count);
  my($second, $minute, $hour, $day, $month, $year);

  open(DHCPdConfIn, $filePath) or die "Could not access $filePath!$newLine";
  print $newLine, "Examining $filePath...$newLine" if $parseSubnets;
  open(DHCPdConfOut, ">$filePath$newFileSuffix")
    or die "Could not access $filePath$newFileSuffix!$newLine";

  ($second, $minute, $hour, $day, $month, $year)= localtime();
  $month++;			# correct for 0 based count
				# and pad single digits
  $minute= "0".$minute if ($minute < 10);
  $second= "0".$second if ($second < 10);
  $year+= 1900;			# add centuries
  print DHCPdConfOut "# $idTag"; # stamp with our tag
  print DHCPdConfOut "$hour:$minute:$second $month/$day/$year$newLine";

  while (<DHCPdConfIn>)
  {
    next if /^$commentDelimiter $idTag/; # skip previous tag line
    print DHCPdConfOut;
    last if /^$bootpHeaderDelimiter/; # end of useful info

    if ($parseSubnets)
    {				# find information about subnets
      $mask= $1
	if /^\s+$iscOptionTag\s+$iscMaskTag\s+(\d+\.\d+\.\d+\.\d+)/;
      $gateway= $1
	if /^\s+$iscOptionTag\s+$iscRoutersTag\s+(\d+\.\d+\.\d+\.\d+)/;
      $domain= $1
	if /^\s+$iscOptionTag\s+$iscDomainTag\s+\"(.+)\"/;
    }
  }
  close(DHCPdConfIn);
  close(DHCPdConfOut);

  if ($parseSubnets)
  {
    $domain= $true unless $domain;
    if ($mask and $gateway)
    {				# we have all the info to compute our subnets
      ($one, $two, $three, $four)= split(/\./, $mask);
      $gateway=~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/;
      if ($four)
      {				# a partial subnet
	$subnet= "$1.$2.$3";
	if ($subnets{$subnet})
	{
	  print $tab, "Duplicate subnet found! [$subnet]", $newLine,
	  $tab, $tab, "Not replacing...", $newLine;
	}
	else
	{
	  $subnets{$subnet}= $domain;
	  print $tab, "$domain; $subnet", $newLine;
	}
      }
      else
      {
	for ($count= $three, $byte= $3; $count < 256; $count++, $byte++)
	{
	  $subnet= "$1.$2.$byte";
	  if ($subnets{$subnet})
	  {
	    print $tab, "Duplicate subnet found! [$subnet]", $newLine,
	    $tab, $tab, "Not replacing...", $newLine;
	  }
	  else
	  {
	    $subnets{$subnet}= $domain;
	    print $tab, "$domain; $subnet", $newLine;
	  }
	}
      }
    }
    else
    {
      print $tab, "Mask definition missing in <$filePath>.", $newLine
      unless $mask;
      print $tab, "Router definition missing in <$filePath>.", $newLine
      unless $gateway;
    }
  }

  return %subnets;
}


# SetupBootP()
#   Reads the header of the current bootptab seeking subnet information
#   Also preserves the header in the generated copy
#
#   Format compatible with BootP and CMU DHCPd
#
sub SetupBootP
{
  my($filePath)= $cmuFile;
  my(%subnets)= ();		# an array of legitimate subnets
  my($token, $mask, $gateway, $subnet, $byte, $field);
  my($one, $two, $three, $four, $count);
  my($second, $minute, $hour, $day, $month, $year);
  
  open(BootPTabIn, $filePath) or die "Could not access $filePath!$newLine";
  print $newLine, "Examining $filePath...$newLine";
  open(BootPTabOut, ">$filePath$newFileSuffix")
    or die "Could not access $filePath$newFileSuffix!$newLine";

  ($second, $minute, $hour, $day, $month, $year)= localtime();
  $month++;			# correct for 0 based count
				# and pad single digits
  $minute= "0".$minute if ($minute < 10);
  $second= "0".$second if ($second < 10);
  $year+= 1900;			# add centuries
  print BootPTabOut "# $idTag"; # stamp with our tag
  print BootPTabOut "$hour:$minute:$second $month/$day/$year$newLine";

  while (<BootPTabIn>)
  {
    if (/^$commentDelimiter/ or /^$newLine/)
    {				# preserve comments and blank lines
      next if (/^$commentDelimiter $idTag/); # skip previous tag line
      print BootPTabOut;
      last if (/^$bootpHeaderDelimiter/); # end of useful info
      next;
    }

    if (/^\.\w+/)
    {				# found a template definition
      print BootPTabOut;
      ($token)= split(":");
    }

    if (/^\s+:/)
    {				# found a template field or several
      print BootPTabOut;
      foreach $field (split(":"))
      {				# examine each field, looking for the subnet
	if ($field=~ /^sm=/)
	{			# excise the subnet mask IP
	  ($field, $mask)= split("=", $field);
	}
	if ($field=~ /^gw=/)
	{			# excise the gateway IP
	  ($field, $gateway)= split("=", $field);
	  if ($mask and $gateway)
	  {
	    ($one, $two, $three, $four)= split(/\./, $mask);
	    $gateway=~ /(\d+)\.(\d+)\.(\d+)\.(\d+)/;
	    if ($four)
	    {			# a partial subnet
	      $subnet= "$1.$2.$3";
	      if ($subnets{$subnet})
	      {
		print $tab, "Duplicate subnet found! [$subnet]", $newLine,
		$tab, $tab, "Not replacing...", $newLine;
	      }
	      else
	      {
		$subnets{$subnet}= $token;
		print $tab, "$token; $subnet", $newLine;
	      }
	    }
	    else
	    {
	      for ($count= $three, $byte= $3; $count < 256; $count++, $byte++)
	      {
		$subnet= "$1.$2.$byte";
		if ($subnets{$subnet})
		{
		  print $tab, "Duplicate subnet found! [$subnet]", $newLine,
		  $tab, $tab, "Not replacing...", $newLine;
		}
		else
		{
		  $subnets{$subnet}= $token;
		  print $tab, "$token; $subnet", $newLine;
		}
	      }
	    }
	  }
	  else
	  {
	    print $newLine, $tab, "Mask missing in $token's definition.",
	    unless $mask;
	    print $newLine, $tab, "Router missing in $token's definition.",
	    unless $gateway;
	  }
	}
      }
    }
  }
  close(BootPTabOut);
  close(BootPTabIn);
  
  return %subnets;
}


# GenerateARPA($numbers, $subnets)
#   Reads the header of the current domain file seeking standard information
#   and creates appropriate in-addr.arpa files with the same header 
#   
#   $numbers is the array of all IP name and IP address pairs
#   $subnets is the array of all valid subnets
#
sub GenerateARPA
{
  my($numbers)= shift;
  my($subnets)= shift;
  my(@dnsHeader)= ();		# standard DNS header
  my($dnsDirectoryPath);	# where all the DNS files live
  my(%aSubnet);			# an array of values for a given subnet
  my($dummy, $dirty, $domain, $subnet, $number, $name);
  my($line, $domainOrigin, $subnetOrigin, $subnetNumber);
  my(@subnetLeaves, @rest, @spares, @extraLines);

  # Read in the standard header
  open(DNS, $domainFile) or die "Could not access $domainFile!$newLine";
  print $newLine, "Getting DNS header from $domainFile...$newLine";
  while (<DNS>)
  {				# gather the standard header
    next if (/$idTagOther/);	# skip other auto-generation stamps
    push(@dnsHeader, $_);
    last if (/^$domainHeaderDelimiter/);
  }
  close(DNS);
  $domain= shift @dnsHeader;	# grab the domain name
  chop($domain);
  $domain=~ s/^$dnsCommentDelimiter $domainFilePrefix//;    

  # Generate the DNS directory path
  $dummy= $domainFile;
  while ($dummy=~ s/(.+\/)//)
  {				# accummulate directory leaves
    $dnsDirectoryPath.= $1;
  }
  
  print $newLine,
  "Comparing current in-addr.arpa files to $domainFile...", $newLine;
  foreach $subnet (sort keys(%subnets))
  {
    undef %aSubnet;		# clear before accumulating a subnet
    $dirty= $false;		# assume no changes
    @extraLines= ();		# clear any previosuly accumulated lines
    
    if (-e $dnsDirectoryPath.$domainFilePrefix.$subnet.$specialSuffix)
    {				# we have special entries, add them
      open(Special, $dnsDirectoryPath.$domainFilePrefix.$subnet.$specialSuffix)
	or die "Could not access "
	  .  $dnsDirectoryPath.$domainFilePrefix.$subnet.$specialSuffix
	    . "!$newLine";
      while(<Special>)
      {
	next if /^$newLine/;	# skip blank lines
	next if /^$dnsCommentDelimiter/; # skip comments

	if (/\s+PTR\s+/)
	{			# only notice PTR lines
	  ($node, $dummy, $dummy, $name)= split;
	  if ($numbers{"$subnet.$node"})
	  {			# duplicate node assignment
	    print $tab, "Overwriting ", $numbers{"$subnet.$node"},
	    " with reserved $name [$subnet.$node]!", $newLine;
	  }
	  $numbers{"$subnet.$node"}= $name;
	  next;			# skip to next line
	}

	push(@extraLines, $_);	# accumulate extra lines for this subnet
      }
      close(Special);
    }

    if (open(Subnet, $dnsDirectoryPath.$domainFilePrefix.$subnet))
    {				# compare to previous data
      while(<Subnet>)
      {
	if (/\s+PTR\s+/)
	{			# only notice PTR lines
	  ($number, $dummy, $dummy, $name)= split;
	  ($name)= split(/\./, $name) if (/\.$domain\.$/);
	  $aSubnet{$number}= $name;
	}
      }
      close(Subnet);
    }

    foreach $node (0..255)
    {
      if ($aSubnet{$node} ne $numbers{"$subnet.$node"})
      {
	$aSubnet{$node}= $numbers{"$subnet.$node"};
	$dirty= $true;		# mark this subnet file for generation
	last;
      }
    }
    
    if ($dirty)
    {				# generate a new version
      undef @spares;		# clear our list of spare addresses
      open(Subnet, ">$dnsDirectoryPath$domainFilePrefix$subnet$newFileSuffix")
	or die "Could not access "
	  . "$dnsDirectoryPath$domainFilePrefix$subnet$newFileSuffix!$newLine";
      print $tab, "Generating $dnsDirectoryPath$domainFilePrefix$subnet...",
      $newLine;
     
      # stamp with our tag
      print Subnet "$dnsCommentDelimiter $idTag";
      ($second, $minute, $hour, $day, $month, $year)= localtime(time());
      $month++;			# correct for 0 based count
				# and pad single digits
      $minute= "0".$minute if ($minute < 10);
      $second= "0".$second if ($second < 10);
      $year+= 1900;		# add centuries
      print Subnet "$hour:$minute:$second $month/$day/$year$newLine";
      
      print Subnet $dnsCommentDelimiter, " ",
      $domainFilePrefix, $domain, $newLine;
     
      # Fix $ORIGIN tags and prepend the standard header
      @subnetLeaves= split (/$nodeDelimiterMatch/, $subnet);
      @subnetLeaves= reverse(@subnetLeaves);
      $subnetOrigin= join($nodeDelimiter, @subnetLeaves);
      $subnetNumber= shift(@subnetLeaves);
      $domainOrigin= join($nodeDelimiter, @subnetLeaves);
      foreach $line (@dnsHeader)
      {
	if ($line=~ /^$originTagMatch/)
	{			# assign correct domain name
	  print Subnet
	    $originTag, $domainOrigin, $originARPARoot, $newLine;
	}
	elsif ($line=~ /$authorityTag/)
	{			# assign correct subnet number
	  $line=~ s/\w+/$subnetNumber/;
	  print Subnet $line;
	}
	else
	{			# simply preserve this line
	  print Subnet $line;
	}
      }
      print Subnet
	$originTag, $subnetOrigin, $originARPARoot, $newLine;
      print Subnet $newLine, @extraLines;
      
      foreach $node (0..255)
      {
	if ($name= $numbers{"$subnet.$node"})
	{
	  # fully qualify the name unless already so
	  $name.= ".$domain." unless ($name=~ /\.$/);
	  print Subnet $newLine, "$node\tIN\tPTR\t$name";
	}
	else
	{
	  push (@spares, $node.$newLine);
	}
      }
      close(Subnet);

      # preserve a copy and replace the original with the new version
      rename("$dnsDirectoryPath$domainFilePrefix$subnet",
	     "$dnsDirectoryPath$domainFilePrefix$subnet$oldFileSuffix")
	or print $tab, "Could not rename ",
	     "$dnsDirectoryPath$domainFilePrefix$subnet: $!",
	$newLine, $tab, $tab, "But continuing with the rest...", $newLine;
      rename("$dnsDirectoryPath$domainFilePrefix$subnet$newFileSuffix",
	     "$dnsDirectoryPath$domainFilePrefix$subnet")
	or print $tab, "Could not rename ",
	"$dnsDirectoryPath$domainFilePrefix$subnet$newFileSuffix: $!",
	$newLine, $tab, $tab, "But continuing with the rest...", $newLine;

      # update our list of spare addresses
      if (@spares)
      {
	open(Spares,
	     ">$dnsDirectoryPath$domainFilePrefix$subnet$spareAddressesSuffix")
	  or die "Could not access "
	    . "$dnsDirectoryPath$domainFilePrefix$subnet$spareAddressesSuffix!"
	      . $newLine;
	print $tab, "Listing $subnet spares...", $newLine;
	
	# stamp with our tag
	print Spares "$dnsCommentDelimiter $idTag";
	($second, $minute, $hour, $day, $month, $year)= localtime(time());
	$month++;		# correct for 0 based count
				# and pad single digits
	$minute= "0".$minute if ($minute < 10);
	$second= "0".$second if ($second < 10);
	$year+= 1900;		# add centuries
	print Spares "$hour:$minute:$second $month/$day/$year$newLine";
	
	print Spares $dnsCommentDelimiter, " ",
	$domainFilePrefix, $domain, $newLine;
	
	print Spares @spares;

	close(Spares);
      }
      else
      {
	if (-e $dnsDirectoryPath.$domainFilePrefix.$subnet
	    .$spareAddressesSuffix)
	{
	  system("rm ".$dnsDirectoryPath.$domainFilePrefix.$subnet
		 .$spareAddressesSuffix) or "Could not clear old spares.";
	}
      }
    }
  }
}


# Activate()
#  Replace current files with new data and restart daemons as necessary
#
sub Activate
{
  my($result);			# an error condition

  &SignalProcess($dnsProcess, $restartSignal)
    if $newDNS;			# restart the DNS server

  if ($installCMU)
  {				# update the bootptab
    $result= system("cp $cmuFile $bootptabFile");

    print $newLine,
    "Failed to replace <$bootptabFile>.", $newLine
      if $result;
    # CMU DHCP server should notice the new file automatically
  }

  if ($installISC)
  {				# update the dhcpd.conf
     $result= system("cp $iscFile $iscDHCPConfFile");
     print $newLine,
     "Failed to replace <$iscDHCPConfFile>.", $newLine
       if $result;

     # First terminate and then start the DHCP daemon
     #   as the ISC DHCP server cannot handle a restart
     &SignalProcess($dhcpdProcess, $terminateSignal);
     $result= system("$dhcpdProcess -q");
     print $newLine,
     "Failed to start <$dhcpdProcess>.", $newLine
       if $result;
  }
}


# SignalProcess($process, $signal)
#   Finds a designated process and sends it a signal
#
sub SignalProcess
{
  my($process)= shift;		# the name of our target process
  my($signal)= shift;		# the signal to send to said process
  my($candidatePID);		# id of a running DNS process
  my(@candidates)= ();		# a list of possible current DNS processes
  my($result);			# result of the restart signal

  open(Processes, $psCommand)
    or die "Cannot gather a list of current processes.";
  while (<Processes>)
  {
    push(@candidates, $_) if (/\b$process\b/);
  }
  close(Processes);

  if (@candidates)
  {
    die "Multiple instances of $process are running!" if (@candidates > 1);
    $candidates[0]=~ s/^$space+//; # kill leading spaces
    ($candidatePID)= split($space, $candidates[0]);
    $result= kill $signal, $candidatePID;
    print $newLine,
    "$process [$candidatePID] successfully signaled ($signal).", $newLine
      if ($result);
  }
  else
  {
    print $newLine, "$process is not running!$newLine";
    exit;
  }
}


# A helper for the sort function
#
sub numerically { $a <=> $b; }