master_watcher
changeset 9 6f5ef6fb479d
parent 8 098af5defd01
child 10 8f7ba479860c
equal deleted inserted replaced
8:098af5defd01 9:6f5ef6fb479d
     1 #! /usr/bin/perl -w
       
     2 # $Id$
       
     3 my $USAGE = <<'#';
       
     4 Usage: $ME [options]
       
     5        -l --logfile=s   Name of the logfile we've to read [$opt_logfile]
       
     6        -z --zonesdir=s  Where the named.conf's are expected [$opt_zonesdir]
       
     7        -u --[no]update  Update the \"masters\"-entries [$opt_update] 
       
     8        -f --[no]follow  Follow the end of the logfile [$opt_follow]
       
     9        -d --[no]debug   extra debug output [$opt_debug]
       
    10        -h --help        This text [$opt_help]
       
    11           --daemon      go into background [$opt_daemon]
       
    12        -p --pidfile=s   file to store the pid [$opt_pidfile]
       
    13        -v --version     print version [$opt_version]
       
    14 #
       
    15 # Es wird ein Verzeichnis geben, in diesem Verzeichnis liegt für 
       
    16 # *jede* Zone eine eigene Konfigurations-Datei.
       
    17 # Diese ganzen Konfigurationsdateien werden dann zusammengefaßt
       
    18 # und diese zusammengefaßte wird dem bind per "include" mitgeteilt.
       
    19 #
       
    20 # Wir durchsuchen den Syslog nach
       
    21 # NOTIFY von Master-Servern.
       
    22 #   o Wenn der Master nicht authorisiert ist -> ENDE GELÄNDE
       
    23 #   o Andernfalls merken wir uns diesen Master für diese Domain
       
    24 # Wenn dann mal zu lesen ist "not of our zones" und wir uns diesen
       
    25 # als einen unserer Master gemerkt haben, dann vermerken wir uns, daß
       
    26 # für diese Zone das Konfig-File fehlt.
       
    27 #
       
    28 # Sollte irgendwann mal ein "slave zone...loaded" auftauchen, ist das Konfig-File
       
    29 # inzwischen vorhanden und kein Grund zur Panik.  Wir entfernen es aus der Liste
       
    30 # der fehlenden Files.
       
    31 #
       
    32 # Sollte dieser Text ausbleiben, müssen wir ein File anlegen (wahrscheinlich).
       
    33 # -> Sollte trotzdem schon eins da sein, dann konnten wir aus irgendwelchen
       
    34 # Gründen nix laden (deshalb fehlt ja der "...loaded"-Text).  Das kann z.B. sein, weil ein 
       
    35 # Master mal keinen Bock hat oder nicht authoritativ ist.
       
    36 #
       
    37 # Etwas anders sieht's im update-Modus aus.  Hier wird *jeder* Master für jede Domain gemerkt und
       
    38 # geprüft, ob der in dem entsprechenden Konfig-File enthalten ist.
       
    39 #
       
    40 # Im täglichen Einsatz sollte es ohne Update-Modus ganz gut funktionieren.
       
    41 
       
    42 use strict;
       
    43 use File::Basename;
       
    44 use IO::Handle;
       
    45 use Fcntl qw/:flock/;
       
    46 use File::Path;
       
    47 use Getopt::Long;
       
    48 use Unix::Syslog qw/:macros :subs/;
       
    49 
       
    50 my $ME = basename $0;
       
    51 
       
    52 
       
    53 my %auth = (
       
    54     "212.80.235.130" => "pu.schlittermann.de",
       
    55     "212.80.235.132" => "mango.compot.com",
       
    56     "145.253.160.50" => "bastion.actech.de",
       
    57     "62.144.175.34" => "ns.add-on.de",	    
       
    58     "195.145.19.34" => "ns.datom.de",
       
    59     "62.157.194.1" => "ns.mueritzcomp.de",
       
    60     "212.80.235.137" => "ns.flaemingnet.de omni.flaemingnet.de",
       
    61     "212.80.235.152" => "www.proton24.de",
       
    62     # "194.162.141.17" => "dalx1.nacamar.de",
       
    63 );
       
    64 
       
    65 $SIG{__DIE__} = sub { syslog(LOG_ERR, $_[0]); exit -1; };
       
    66 $SIG{__WARN__} = sub { syslog(LOG_WARNING, $_[0]); };
       
    67 $SIG{TERM} = $SIG{INT} = sub { exit 0; };
       
    68 
       
    69 my %seen;
       
    70 
       
    71 my $opt_help = 0;
       
    72 my $opt_pidfile = "/var/run/$ME.pid";
       
    73 my $opt_logfile = "/var/log/syslog";
       
    74 my $opt_zonesdir = "/etc/bind/zones.d";
       
    75 my $opt_follow = 0;
       
    76 my $opt_update = 0;
       
    77 my $opt_debug = 0;
       
    78 my $opt_daemon = 1;
       
    79 my $opt_version = 0;
       
    80 
       
    81 my $naptime = 60;
       
    82 
       
    83 
       
    84 sub updateFile($$$);
       
    85 sub debug($;@) { syslog(LOG_DEBUG, "DEBUG " . shift @_, @_) if $opt_debug; }
       
    86 
       
    87 END {
       
    88     open(PID, $opt_pidfile);
       
    89     my $pid = <PID>;
       
    90     close(PID);
       
    91     unlink $opt_pidfile if $$ == $pid;
       
    92 }
       
    93 
       
    94 MAIN: {
       
    95 
       
    96 
       
    97     openlog($ME, LOG_PID | LOG_PERROR, LOG_DAEMON);
       
    98     syslog(LOG_NOTICE, "starting");
       
    99 
       
   100     GetOptions(
       
   101 	"help" => \$opt_help,
       
   102 	"logfile=s" => \$opt_logfile,
       
   103 	"follow!" => \$opt_follow,
       
   104 	"update!" => \$opt_update,
       
   105 	"debug!" => \$opt_debug,
       
   106 	"daemon!" => \$opt_daemon,
       
   107 	"pidfile=s" => \$opt_pidfile,
       
   108 	"version!" => \$opt_version,
       
   109 	"zonesdir=s" => \$opt_zonesdir)
       
   110     or die "$ME: Bad Usage\n";
       
   111 
       
   112     if ($opt_help) {
       
   113 	print eval "\"$USAGE\"";
       
   114 	exit 0;
       
   115     }
       
   116 
       
   117     if ($opt_version) {
       
   118 	print "$ME Version: ", '$Id$', "\n";
       
   119 	exit 0;
       
   120     }
       
   121 
       
   122 
       
   123     # Create the PID-File
       
   124     {
       
   125 	open(PID, $_ = ">$opt_pidfile.$$") or die "Can't open $opt_pidfile: $!\n";
       
   126 	print PID "$$\n";
       
   127 	close(PID);
       
   128 
       
   129 	if (!link($_ = "$opt_pidfile.$$", $opt_pidfile)) {
       
   130 	    unlink "$opt_pidfile.$$";
       
   131 	    die "There's another $ME running.  Bad. Stop.";
       
   132 	}
       
   133 	unlink "$opt_pidfile.$$";
       
   134     }
       
   135 
       
   136     if ($opt_daemon) {
       
   137 	my $pid = fork();
       
   138 
       
   139 	if ($pid < 0) {
       
   140 	    die "Can't fork: $!\n";
       
   141 	} 
       
   142 
       
   143 	if ($pid) {
       
   144 	    open(PID, $_ = ">$opt_pidfile") or die "Can't open $_: $!\n";
       
   145 	    print PID "$pid\n";
       
   146 	    close(PID);
       
   147 	    exit 0;
       
   148 	}
       
   149 
       
   150 	close(STDIN); close(STDOUT); close(STDERR);
       
   151     }
       
   152 
       
   153 
       
   154     open (LOGFILE, $_ = "<$opt_logfile") or die "Can't open $_: $!\n";
       
   155 
       
   156     for (;;) {
       
   157 	my (%masters, %missing, %nomasters);
       
   158 	while (<LOGFILE>) {
       
   159 	    
       
   160 	    my ($domain, $ip);
       
   161 	    
       
   162 	    # NOTIFY-Zeilen
       
   163 	    ($domain, $ip) = /NOTIFY.*?\((\S+?),.*?\[([\d.]+)\]/ and do {
       
   164 		if (not exists $auth{$ip}) {
       
   165 		    warn "notify for $domain from unauthorized ip $ip\n";
       
   166 		    next;
       
   167 		};
       
   168 		# also in die Liste (hier als Key eines Hashes wegen der möglichen
       
   169 		# Dopplungen) der Master für diese Domain aufnehmen.
       
   170 		debug("Master für $domain: $ip\n");
       
   171 		$masters{$domain}->{$ip} = 1;
       
   172 		next;
       
   173 	    };
       
   174 
       
   175 	    # Das müssen wir doch garnicht machen, da wir ja sowieso nach 
       
   176 	    # dem Master-Files gucken...
       
   177 	    # NOTIFY for vergessene 
       
   178 	    /NOTIFY for "(\S+?)".*not one of our zones/ and do {
       
   179 		my $domain = $1;
       
   180 		if (not exists $masters{$domain}) {
       
   181 		    debug "skipping $domain (not authorized)\n";
       
   182 		    next;
       
   183 		}
       
   184 		# Also in die Liste der vergessenen Domains (für die wir garkeine
       
   185 		# Konfigurations-Datei haben)
       
   186 		next if exists $missing{$domain};	# schon erledigt
       
   187 
       
   188 		debug("Missing file for $domain\n");
       
   189 		$missing{$domain} = 1;
       
   190 		next;
       
   191 	    };
       
   192 
       
   193 	    # Wenn wir ein "... loaded" finden, dann fehlt das File nicht gänzlich!
       
   194 	    /slave zone "(\S+?)" .*loaded/ and do {
       
   195 		my $domain = $1;
       
   196 		next if not exists $missing{$domain};	# ist noch nicht vermißt worden
       
   197 
       
   198 		debug("Missing file for $domain is not longer missing\n");
       
   199 		delete $missing{$domain};
       
   200 		next;
       
   201 	    };
       
   202 
       
   203 	    /\[([\d.]+)\] not authoritative for (\S+), SOA/ and do {
       
   204 		my ($master, $domain) = ($1, $2);
       
   205 		next if exists $nomasters{$domain}->{$master};
       
   206 
       
   207 		debug "$master isn't a auth. master for $domain\n";
       
   208 		$masters{$domain}->{$master} = 1;	# sieht blöd aus, wird aber gebraucht,
       
   209 							# weil wir nur die bearbeiten, die einen
       
   210 							# master haben
       
   211 		$nomasters{$domain}->{$master} = 1;
       
   212 		next;
       
   213 	    };
       
   214 	}
       
   215 
       
   216 	# Jetzt sind wir erstmal durch und verarbeiten alles
       
   217 	my $changed = 0;
       
   218 	foreach my $domain (sort ($opt_update ? keys %masters : keys %missing)) {
       
   219 	    $changed += updateFile($domain, [keys %{$masters{$domain}}], [keys %{$nomasters{$domain}}]);
       
   220 	    delete $masters{$domain};
       
   221 	    delete $missing{$domain} if exists $missing{$domain};
       
   222 	    delete $nomasters{$domain} if exists $nomasters{$domain};
       
   223 	}
       
   224 
       
   225 	debug "$changed changes."; 
       
   226 	if ($changed) {
       
   227 	    debug("bind reload required\n");
       
   228 	    open(ALL, $_ = ">/etc/bind/zones.all") or die "Can't open $_: $!\n";
       
   229 	    foreach (</etc/bind/zones.d/*>) {
       
   230 		open(IN, $_) or die "Can't open $_: $!\n";
       
   231 		print ALL <IN>;
       
   232 		close(IN);
       
   233 	    }
       
   234 	    system qw(ndc reload);
       
   235 	}
       
   236 
       
   237 	last if !$opt_follow;
       
   238 	syslog LOG_INFO, "Sleeping for $naptime seconds\n";
       
   239 	sleep $naptime;
       
   240 	if ((LOGFILE->stat())[1] != (stat($opt_logfile))[1]) {
       
   241 	    # new file to follow
       
   242 	    syslog(LOG_NOTICE, "Logfile changed, re-open it.\n");
       
   243 	    open(LOGFILE, $_ = "<$opt_logfile") 
       
   244 		or die "Can't open $_: $!\n";
       
   245 	} else {
       
   246 	    LOGFILE->clearerr();
       
   247 	}
       
   248     }
       
   249 }
       
   250 
       
   251 sub updateFile($$$)
       
   252 {
       
   253     local $_;
       
   254     my $domain = $_[0];
       
   255     my @new_masters = @{$_[1]};
       
   256     my @no_masters = @{$_[2]};
       
   257 
       
   258     my %masters = ();
       
   259     my $masters;
       
   260 
       
   261     my $file = "$opt_zonesdir/$domain";
       
   262 
       
   263     debug "updateFile: $domain, @new_masters, @no_masters\n";
       
   264 
       
   265     if (-f $file) {
       
   266 	# Das File ist also schon da, wir müssen nur mal gucken, ob die Master,
       
   267 	# von denen wir ein NOTIFY erhalten haben, auch in unserer Datei stehen.
       
   268 	#
       
   269 	open (F, $_ = "+<$file") or die "Can't open $_: $!\n";
       
   270 	flock(F, LOCK_EX); seek(F, 0, 0);
       
   271 
       
   272 	$_ = join "", <F>;
       
   273 
       
   274 	# Liste der Master raussuchen, darus noch die löschen, die uns
       
   275 	# die Mitarbeit verweigert haben..
       
   276 	/^(\s*masters\s*{\s*)(.*?);(\s*}\s*;)(\s*\/\/.*?\n)/ims;
       
   277 	%masters = map { $_, 1 } split/\s*;\s*/, $2;
       
   278 	$masters = %masters;	# für den späteren Vergleich
       
   279 
       
   280 	# noch unsere neuen hinzufügen...
       
   281 	@masters{@new_masters} = map { 1 } @new_masters;
       
   282 
       
   283 	# nun die weg, die sich nicht zuständig fühlen
       
   284 	delete @masters{@no_masters};
       
   285 
       
   286 	# Wenn sich nach alldem nichts verändert hat, haben wir fertig.
       
   287 	if ($masters eq %masters) {
       
   288 	    debug("File is up-to-date for $domain\n");
       
   289 	    syslog(LOG_NOTICE, "No changes made for $domain (no \"loaded\" seen, defective master?)\n")
       
   290 		unless $opt_update;
       
   291 	    close(F);
       
   292 	    return 0;
       
   293 	}
       
   294 
       
   295 	if (not %masters) {
       
   296 	    syslog LOG_NOTICE, "REMOVING $file (empty masters list)\n";
       
   297 	    close F;
       
   298 	    unlink $file;
       
   299 	    return 1;
       
   300 	}
       
   301 
       
   302 	$masters = join ";", keys %masters;
       
   303 	syslog(LOG_NOTICE, "Updated masters ($masters) list for $domain\n");
       
   304 	s/^(\s*masters\s*{\s*)(.*?);(\s*}\s*;)/$1$masters;$3/ims;
       
   305 
       
   306 	truncate(F, 0);
       
   307 	seek(F, 0, 0);
       
   308 	print F;
       
   309 	close F;
       
   310 
       
   311 	return 1;
       
   312     } 
       
   313 
       
   314 
       
   315     my $date = localtime();
       
   316     my %new_masters = map { $_, 1 } @new_masters;
       
   317     delete @new_masters{@no_masters};
       
   318 
       
   319     if (not %new_masters) {
       
   320 	syslog LOG_INFO, "not creating $file (empty masters list)\n";
       
   321 	return 0;
       
   322     }
       
   323 
       
   324     $masters = join "; ", @new_masters;
       
   325 
       
   326     -d $opt_zonesdir or mkpath($opt_zonesdir, 0, 0755);
       
   327 
       
   328     syslog(LOG_NOTICE, "Creating $file for $domain");
       
   329     open(OUT, $_ = ">$file") or die "Can't open $_: $!\n";
       
   330 
       
   331 	print OUT <<_EOF_;
       
   332 // Autogenerated by $ME: $date
       
   333 zone "$domain" {
       
   334     type slave;
       
   335     masters { $masters; };  // $date
       
   336     file "/etc/bind/slave/$domain";
       
   337     allow-query { any; };
       
   338     allow-transfer { none; };
       
   339     allow-update { none; };
       
   340 };
       
   341 
       
   342 _EOF_
       
   343     close OUT;
       
   344 
       
   345     return 1;
       
   346 }
       
   347 	
       
   348 
       
   349 # vim:sts=4 sw=4 aw ai sm: