bjensen34 has asked for the wisdom of the Perl Monks concerning the following question:

Hello oh wise ones

I am sure that this has been done a million times before, however I can't seem to find it by searching here or googling. I have a situation where I have 3 DNS servers running BIND9 and a total of 18 zone files all in /var/named/zone. In the event of a failure I need to be able to do the following:

1. Stop named on the Primary DNS server (Already solved/written).

2. Globally find and replace all instances of the first 3 octets of an IP address with a different 3, for exaple: 192.168.10.x with 10.2.5.x leaving the last octet in place. I have partially done this and tested and although dirty it works:

`perl -pi -e 's/192.168.10/10.2.5/g' $ROOT_DIR/*`;

Open to a suggestion for a cleaner way to do that within the script.

3. This one is what is giving me the real issue. I need to be able to change serial numbers in all of the 18 zone files so the slaves will update properly. I can do it in a fairly dirty method one file at a time using this:

my $zonefile = DNS::ZoneParse->new("$ROOT_DIR/zone.com", $origin);
print "Changing $zonefile serial number\n";
$zonefile->new_serial();
open NEWZONE, ">$ROOT_DIR1/zone.com" or die "ERROR - $zonefile open failed";
print NEWZONE $zonefile->output();
close NEWZONE;

However I find I have issue with it as it blows away my $ORIGIN and CNAME lines.

Would like to find a simpler solution to read in the entire directory (it's all zone files btw) put them into a list and then do an edit on each maybe something along these lines(beware I know my syntax is lacking here and thus my request for help):

opendir $ROOT_DIR, ".";
@contents = grep !/^\.\.?$/, readdir $ROOT_DIR;
while (<>) {

?????

Here is the part I need help with, either being able to continue to use the DNS::ZoneParse Module and not have it clobber my $ORIGIN and CNAME lines or some other method of cleanly giving each file a new serial. Then of course:

} closedir ROOT_DIR;

4. Then finally restart the service (solved), send an email letting staff know this was run and what mode it is now in (partially solved), log the entire happenings within the script to a file for debug and time stamping. I have looked at a couple of things but seems there has to be an easy way to dump all out to a log like in shell script.(clueless).

Of course there are other admin things in the background, making backups of files and moving them around, etc., but have that solved as well.

Any direction you can provide would be enormously appreciated. Thanks to all in advance. Any questions do not hesitate.

Thanks -
Brad Jensen

Replies are listed 'Best First'.
Re: DNS Failover
by moritz (Cardinal) on Oct 29, 2007 at 22:19 UTC
    perl -pi -e 's/192.168.10/10.2.5/g' $ROOT_DIR/*

    You should escape the dots . in your regex: s/192\.168\.10/10.2.5/g. Otherwise the . stands for any character, which is probably not what you want.

    There is a different solution, that may clean up your process of rewriting the configuration files: you could store all the real data in a template file, and write a placeholder instead of the parts that are changed on failover (i.e [%root_ip%].23 instead of 192.168.10.23 or 10.2.5.23).

    Then every time you'd normally change the config files directly you now modify only the templates.

    And you need a script that processes the templates, and substitutes the variables based on whether you're in failover mode or normal operation.

    CPAN has many templating modules ready for you, like Template and Text::Template.

      Thanks for the first one, missed that

      Thank you also for the suggestion on using templates. I have followed the links and done some reading and find myself a bit confused.

      Would you not then have to maintain not only 18 zone files but 18 templates as well?? So if I added another 20 A records to each of the 18 zones I would in effect have to edit 18 files x2?

      As far as the script goes I am still faced with the same logic problem I believe as well in that I still need to figure out within the script which is doing other things how to pull up each templ file and pass the variables and still output new zone files?

      Perhaps I am not seeing the forest through the trees on this one. If you can help guide me a bit I would greatly appreciate it.

      Thanks -

      Brad Jensen

        Would you not then have to maintain not only 18 zone files but 18 templates as well?

        No, you wouldn't. You'd just maintain the templates, and generate the "real" config files automatically.

        Introducing Templates is effectively changing your problem from "read the config files, search and replace in them, and write them back" to "Fill the templates with variables, and write the output into the config files".

        If that confuses you, your best bet is to stay with your original solution, and wait until somebody has a better suggestion on how to handle it ;-)

Re: DNS Failover
by dwm042 (Priest) on Oct 29, 2007 at 22:42 UTC
    When I've done mass changes of DNS serial numbers, I've tended to use an in place replacement such as:

    `perl -pi -e 's/2007\d\d\d\d\d\d/2007102901/' *.com`
    and repeat it for the other zone types. The serial number in 'date format' is unique enough that it's just not found anywhere else in the zone files I manage.

    Otherwise, moritz's suggestion of a template seems to fit your needs quite well.

    Update: To check for an appropriate serial number, something like the code below could be useful:

    #!/usr/bin/perl use warnings; use strict; # # warning - untested code. # Let's find the largest serial number of the lot. # my $patt = qr{200\d{7}}; my $dns_zones = '/etc/named/primary/'; # chdir $dns_zones or die "Cannot enter zone file directory.\n"; my @zones = split /\n/, `ls -1`; my $curr_time = get_serial_number(); for my $z (@zones) { open (ZONE, "<", $z) or die("Cannot open zone file $z.\n"); while(<ZONE>) { # # I habitually follow the serial # with a space, a semicolon, a space # and the word 'serial' # if ( /($patt)\s+\;\s+serial/ ) { my $serial = $1; $curr_time = ++$serial if ( $serial > $curr_time ); last; } } close(ZONE); } print "Final time = $curr_time\n"; # # replace in place here. # sub get_serial_number { my @time = localtime(); my $day = substr("0" . $time[3], -2 ); my $month = $time[4] + 1; my $year = 1900 + $time[5]; return $year . $month . $day . "01"; }
      Thanks! This is helpful as well. Kind of under the gun on this one short term so going to have to do it the dirty way for now and come back and clean it up perhaps using templates later.

      You mentioned using perl -pi -e to do this mass update yesterday and I tried from cmd line and it works great, however when I try to put it in my script it does nothing.

      Here is what I am trying to do:

      # USAGE $0 {failover|failback|status} $1 {serial}
      $VARIABLE = $ARGV[0];
      $Serial = $ARGV1;
      `perl -pi -e 's/2007\d\d\d\d\d\d/$Serial/' $ROOT_DIR/*`;

      So I pass in the serial as an arg to the script and then pass it down to the perl -pi command. What am I missing here? $ROOT_DIR is defined as well pointing to /var/named/zone.

      Sure it is something obvious and silly but any help you can provide would be greatly appreciated.

      Thank you -

      Brad Jensen