#!/usr/bin/perl # vim: cink=0{,0},0),!^F,o,O,e autoindent ts=4 sw=4 cindent expandtab use strict; # I'm dyslexic so it's always a good idea to include this asap # ============================================================================= # # $Id: flatter.pl,v 1.2 2005/04/26 06:57:23 xaphod Exp $ # # 2004-05-19 16:09 # # ============================================================================= # debugging my $DEBUG = 1; my $COOKIE_DEBUG=0; # Load the modules that should already be available use Data::Dumper; #use Getopt::Long; use File::Copy qw(cp mv); # Load the required modules in a pretty manner, giving me the info # I need to correct the problem my %modules = ( 'DBI' => '/usr/ports/databases/p5-DBI', ); foreach my $module (keys %modules) { eval "require $module"; die "Can't load '$module', install the port from $modules{$module}\n" if $@; } # ============================================================================= ## Config print STDERR "Dumping config\n" if $DEBUG; my %options = ( 'master_ip' => '10.9.8.7', # IP of the master, # for generation of secondary config # if needed ## UNUSED ##'secondary' => 0, ## # Generate secondary? #'ndc' => '/bin/echo /usr/sbin/rndc', 'ndc' => '/usr/local/sbin/rndc', # path to ndc 'reload' => 1, # ndc reload zones? 'verbose' => 1, # Raise verbosity 'path-create' => 1, # create the top level dirs? 'prefix' => '/etc/namedb/', # trailing slash ); print STDERR " options: " . Dumper(\%options) if $DEBUG; ## Get options! ## TO DO ## Database connection parameters. my %db = ( 'host' => 'localhost', 'name' => 'flatter', 'user' => 'flatter', 'pass' => 'flatter' ); print STDERR " db: " . Dumper(\%db) if $DEBUG; ## Locations (add trailing slash to paths:) my %paths = ( 'zone' => $options{prefix} . 'flat-zones/', 'temp' => $options{prefix} . 'flat-temp/', 'conf' => $options{prefix} . 'flat-conf/', ); print STDERR " paths: " . Dumper(\%paths) if $DEBUG; my %files = ( 'COOKIE' => $paths{conf} . 'COOKIE', 'MASTER' => $paths{conf} . 'named.master.conf', 'SLAVE' => $paths{conf} . 'named.slave.conf', 'UPDATE' => $paths{conf} . 'UPDATE', ); print STDERR " files: " . Dumper(\%files) if $DEBUG; ## SQL Statements my %sql = ( 'defaults' => q{ SELECT variable, value FROM `_defaults_` WHERE subsystem = 'dns' }, 'cookie' => q{ SELECT MAX( timestamp ) AS timestamp, COUNT( * ) AS count FROM dns_zone WHERE active <> 'N' }, 'zones' => q{ SELECT zone, zone_id, timestamp FROM dns_zone WHERE active <> 'N' }, 'zone' => q{ SELECT * FROM dns_zone WHERE zone_id = ? }, 'rr' => q{ SELECT * FROM dns_entry WHERE zone_id = ? ORDER BY LEVEL , entry_id }, ); # ============================================================================= ## Check paths print STDERR "\nChecking paths:\n" if $DEBUG; &dir_check($options{prefix}); foreach (keys %paths) { $paths{$_} =~ s/\/$//; &dir_check($paths{$_}); } ## Connect to the DB print STDERR "\nConnecting to the DB\n" if $DEBUG; my $dbh = DBI->connect("dbi:mysql:host=$db{host};database=$db{name}", "$db{user}", "$db{pass}", { PrintError => ($DEBUG?1:0) }); die "ERROR: " . $DBI::errstr if ($DBI::err); ## Check the cookie print STDERR "\nChecking the cookie\n" if $DEBUG; my $COOKIE = cookie->new($files{COOKIE}, $dbh, $sql{cookie}); if ($COOKIE->skip()) { print STDERR "\nNothing to do, exiting!\n" if $DEBUG; exit 0; } ## Get a list of zones print STDERR "\nGrabbing zone list\n" if $DEBUG && $options{verbose}; my %zones; my $sth; # Query the DB $sth = $dbh->prepare($sql{zones}); $sth->execute(); # load them into our hash while ( my $data = $sth->fetchrow_hashref() ) { my $z = lc $data->{zone}; # get the zone in lower case $z =~ s/\.$//; # strip any trailing dot unless (defined $zones{$z}) { foreach my $k (keys %{$data}) { $zones{$z}{$k} = $data->{$k}; } $zones{$z}{zone} = $z; } else { print "WARNING: $data->{zone} (#$data->{zone_id}) is a duplicate\n"; } } print STDERR " Got the zones\n" if $DEBUG; print STDERR " zones: " . Dumper(\%zones) if $DEBUG && $options{verbose}; # Finish $sth->finish; ## Generate the conf and zonefiles print STDERR "\nGenerating files\n" if $DEBUG; # Objects my $MASTER = masterconfig->new($files{MASTER}); my $SLAVE = slaveconfig->new($files{SLAVE}, $options{master_ip}); my $ZONE = zonefile->new($dbh, \%sql); ## Process the zones foreach my $zone (sort (keys %zones)) { # calculate the zonefile dir hash, and create the required dirs my ($h1, $h2) = $zone =~ /(.)(.)/; dir_make("$paths{zone}/$h1"); dir_make("$paths{zone}/$h1/$h2"); my $hash = "$h1/$h2/"; # Debug print STDERR " ", $zone if $DEBUG && $options{verbose}; # Write the config $MASTER->add("$paths{zone}/$hash$zone", \$zones{$zone}); $SLAVE->add("$paths{zone}/$zone", \$zones{$zone}); # Generate zonefile if needs be if ($zones{$zone}{timestamp} > $COOKIE->last()) { print STDERR " -- generating new zonefile" if $DEBUG && $options{verbose}; $ZONE->export("$paths{zone}/$hash$zone", \%{$zones{$zone}}); } else { print STDERR " -- no change" if $DEBUG && $options{verbose}; } # print STDERR "\n" if $DEBUG && $options{verbose}; } ## Reload or reconfig if ($options{reload}) { print STDERR "\nReload requirements\n" if $DEBUG; if ($COOKIE->reconfig()) { print STDERR " Configuration has changed\n" if $DEBUG; my $resp = `$options{ndc} reload`; print "Reload server - $resp\n" if $options{verbose}; open FH, ">$files{UPDATE}"; print FH time(); close FH; } else { print STDERR " Only zone data changed\n" if $DEBUG; foreach my $zone (sort (@{$ZONE->export_lst()}) ){ print STDERR " ++ reload $zone\n" if $DEBUG && $options{verbose}; my $resp = `$options{ndc} reload $zone`; print "$zone - $resp\n" if $options{verbose}; } } } else { print STDERR "\nReload disabled in config\n" if $DEBUG; } ## And finally print STDERR "\nDestructors:\n" if $DEBUG; exit 0; # ============================================================================= # ============================================================================= # Helper functions sub dir_check { my $dir = shift; print STDERR " Checking for $dir\n" if $DEBUG; if (-d $dir) { # check we can write to the dir.... open FH, ">$dir/.placeholder" or die "Directory $dir permission denied!\n"; print FH "placeholder"; close FH; } else { if ($options{'path-create'}) { &dir_make($dir); } else { die "Directory $dir does not exist\n"; } } } sub dir_make { my $dir = shift; if (! -e "$dir") { print STDERR " ++ Creating dir $dir\n" if $DEBUG; mkdir "$dir", 0777 or die "Failed to create directory $dir\n;"; } } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## PACKAGE - cookie package cookie; # new - constructor (Get's value from the cookie & Gets MAX from DB) # 1. filename (the file which has the cookie) # 2. DB Handle # 3. SQL Statement to grab the MAX # # DESTROY - descructor (backup old cookie, creates the new); # # last - the value # # skip - do we need to do owt? # # ============================================================================= sub new { my $proto = shift; my $class = ref($proto) || $proto; my $filename = shift; my $dhh = shift; my $sql = shift; my $self = { FILENAME => $filename, LAST => 0, COUNT => 0, SAVED_LAST => 0, SAVED_COUNT => 0 }; # GET the current values my $sth = $dbh->prepare($sql); $sth->execute(); my $row = $sth->fetchrow_hashref(); $self->{LAST} = $row->{timestamp}; $self->{COUNT} = $row->{count}; $sth->finish; print STDERR " Got '$self->{LAST}', '$self->{COUNT}' from the DB\n" if $DEBUG ; # GET the LAST if (-r $filename) { open FH, "< $filename"; $self->{SAVED_LAST} = ; chomp $self->{SAVED_LAST}; $self->{SAVED_COUNT} = ; chomp $self->{SAVED_COUNT}; close FH; print STDERR " Got '$self->{SAVED_LAST}', '$self->{SAVED_COUNT}' from the cookie\n" if $DEBUG; } else { print STDERR " NO COOKIE! Using 0,0\n" if $DEBUG; } bless ($self, $class); return $self; } # ============================================================================= sub DESTROY { my $self = shift; return if $self->skip(); if ($COOKIE_DEBUG) { print STDERR " COOKIE_DEBUG is on, not saving the cookie\n"; return; } my $filename = $self->{FILENAME}; # Save the last open FH, "> $filename"; print FH $self->{LAST}, "\n", $self->{COUNT}; close FH; print STDERR " Saving the cookie\n" if $DEBUG; } # ============================================================================= sub last { # my $self = shift; return @_[0]->{SAVED_LAST}; } # ============================================================================= sub skip { my $self = shift; return ( ($self->{LAST} == $self->{SAVED_LAST}) && ($self->{COUNT} == $self->{SAVED_COUNT}) ); } # ============================================================================= sub reconfig { my $self = shift; return !($self->{COUNT} == $self->{SAVED_COUNT}); } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## PACKAGE - masterconfig package masterconfig; # new - constructor (opens the config file) # 1. filename # # DESTROY - Destructor (closes the config file) # # add - Add a zone record to the config file # 1. filename # 2. Referance to a hash containing the current zone record # ============================================================================= sub new { my $proto = shift; my $class = ref($proto) || $proto; my $filename = shift; open FH, "> $filename"; print FH "//\n// Named Master Configuration File\n//\n//\n"; my $self = { FH => *FH }; bless ($self, $class); return $self; } # ============================================================================= sub DESTROY { my $self = shift; close $self->{FH}; } # ============================================================================= sub add { my $self = shift; my $filename = shift; my $zone = shift; my $FH = $self->{FH}; print $FH "\n// zone_id: ", $$zone->{zone_id}, "\n"; print $FH "zone \"", $$zone->{zone}, "\" {\n type master;\n file \"$filename\";\n};\n"; } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## PACKAGE - slaveconfig package slaveconfig; # new - constructor (opens the config file) # 1. filename # 2. the IP of the master # # DESTROY - Destructor (closes the config file) # # add - Add a zone record to the config file # 1. filename # 2. Referance to a hash containing the current zone record # ============================================================================= sub new { my $proto = shift; my $class = ref($proto) || $proto; my $filename = shift; open FH, "> $filename"; print FH "//\n// Named Slave Configuration File\n//\n//\n"; my $self = { FH => *FH, MASTER => shift }; bless ($self, $class); return $self; } # ============================================================================= sub DESTROY { my $self = shift; close $self->{FH}; } # ============================================================================= sub add { my $self = shift; my $filename = shift; my $zone = shift; my $FH = $self->{FH}; print $FH "\n// zone_id: ", $$zone->{zone_id}, "\n"; print $FH "zone \"", $$zone->{zone}, "\" {\n type slave;\n masters {\n"; print $FH " ", $self->{MASTER}, ";\n"; print $FH " };\n file \"", $filename, "\";\n};\n"; } # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ## PACKAGE - zonefile package zonefile; use Data::Dumper; # new - constructor (prepare the statement handle) # 1. DB Handle # 2. Ref to SQL Statements # # DESTROY - destructor (releases the statement handle) # # export - dump a record hash to disk # 1. Filename to use # 2. Referance to a hash containing the current zone record # ============================================================================= sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = { DBH => shift, SQL => shift, ZONE => undef, EXPORTS => (), }; die "zonegen requires a DB Handle, and an SQL statement - fool: $!" if ( !defined($self->{DBH}) or !defined($self->{SQL}) ); # Prepare our queries. $self->{STH}{zone} = $self->{DBH}->prepare($self->{SQL}{zone}); $self->{STH}{rr} = $self->{DBH}->prepare($self->{SQL}{rr}); bless ($self, $class); return $self; } # ============================================================================= sub DESTROY { my $self = shift; } # ============================================================================= sub export_lst { my $self = shift; return $self->{EXPORTS}; } # ============================================================================= sub export { my $self = shift; my $location = shift; my $zone = shift; my $lastlev = 0; # for prettyness only. $self->_query_zone($zone->{zone_id}); $self->_get_defaults unless (exists $self->{DEFAULTS}); open FH, "> $location" or die "ERROR: can't open destination file '$location'\n"; print FH ";;\n;; $location\n;; zone_id: ", $self->{ZONE}{zone_id}, "\n;;\n\n"; print FH ";; THIS FILE IS AUTOGENERATED\n;; MANUAL EDITS WILL BE LOST!\n\n"; print FH $self->_origin(); print FH $self->_ttl(); print FH $self->_soa(); print FH "@ IN TXT \"Zone generated by flatter.pl - see http://lizard.org.uk./weblog/freebsd/dns/zsql.html\"\n"; $self->{STH}{rr}->execute($self->{ZONE}{zone_id}); while (my $rr = $self->{STH}{rr}->fetchrow_hashref()) { print FH "\n" if ($rr->{level} != $lastlev); $lastlev = $rr->{level}; my $owner = sprintf("%-31s ", $rr->{owner}); my $ttl = $rr->{ttl} ? sprintf ("%-6i", $rr->{ttl}) : ' '; print FH "$owner $ttl $rr->{class} $rr->{type}\t$rr->{rdata}\n"; } close FH; push @{$self->{EXPORTS}}, $self->{ZONE}{zone} } # ============================================================================= sub _query_zone { my $self = shift; my $zone_id = shift; $self->{STH}{zone}->execute($zone_id); $self->{ZONE} = $self->{STH}{zone}->fetchrow_hashref(); } # ============================================================================= sub _get_defaults { my $self = shift; print STDERR "\n ++ loading defaults\n" if $DEBUG && $options{verbose}; $sth = $dbh->prepare($self->{SQL}{defaults}); $sth->execute(); # Now, for each setting while ( my $data = $sth->fetchrow_hashref() ) { $self->{DEFAULTS}{$data->{variable}} = $data->{value}; } print STDERR " defaults: " . Dumper($self) if $DEBUG && $options{verbose}; # Finish $sth->finish; print STDERR "\n" if $DEBUG && $options{verbose}; } # ============================================================================= sub _def_or_val { my $self = shift; my $what = shift; return $self->{ZONE}{"$what"} ? $self->{ZONE}{"$what"} : $self->{DEFAULTS}{"$what"}; } # ============================================================================= sub _origin { my $self = shift; my $zone = lc($self->{ZONE}{zone}); $zone =~ s/\.*$/\./; return "\$ORIGIN $zone\n\n"; } # ============================================================================= sub _ttl { my $self = shift; my $ttl = $self->_def_or_val('default_TTL'); return "\$TTL $ttl\n\n"; } # ============================================================================= sub _soa { my $self = shift; my $mname = $self->_def_or_val('soa_MNAME'); my $rname = $self->_def_or_val('soa_RNAME'); my $serial = sprintf("%10s", $self->{ZONE}{timestamp}); my $refresh = sprintf("%10s", $self->_def_or_val('soa_REFRESH')); my $retry = sprintf("%10s", $self->_def_or_val('soa_RETRY')); my $expire = sprintf("%10s", $self->_def_or_val('soa_EXPIRE')); my $minimum = sprintf("%10s", $self->_def_or_val('soa_MINIMUM')); return << "END-SOA"; \@\t\t\tIN SOA\t$mname $rname ( \t\t\t$serial ; serial (unix epoch!) \t\t\t$refresh ; refresh \t\t\t$retry ; retry \t\t\t$expire ; expire \t\t\t$minimum ) ; minimum END-SOA } # ============================================================================= __END__