summaryrefslogtreecommitdiffabout
authorSergey Poznyakoff <gray@gnu.org.ua>2016-02-02 10:54:52 (GMT)
committer Sergey Poznyakoff <gray@gnu.org.ua>2016-02-02 10:54:52 (GMT)
commitdb032057eb89e07b79eefb8617e326e2cd037300 (patch) (unidiff)
tree6b2c80bb44ef91afda06eca064347a6b42c3813a
parente3ea08d43ccf0a117b10e9d52c91612281a4193f (diff)
downloaddnstools-db032057eb89e07b79eefb8617e326e2cd037300.tar.gz
dnstools-db032057eb89e07b79eefb8617e326e2cd037300.tar.bz2
vhostcname: allow for different per-zone servers/keys/ttls
* vhostcname/vhostcname: Rewrite configuration handling. Store zone settings in individual hash cells. Redefine exit codes. Implement status command. Clean up the semantics of start/forced-restart vs. reload. Accept abbreviated command names.
Diffstat (more/less context) (show whitespace changes)
-rwxr-xr-xvhostcname/vhostcname744
1 files changed, 557 insertions, 187 deletions
diff --git a/vhostcname/vhostcname b/vhostcname/vhostcname
index 359101c..fc6dd2d 100755
--- a/vhostcname/vhostcname
+++ b/vhostcname/vhostcname
@@ -1,5 +1,5 @@
1#!/usr/bin/perl 1#!/usr/bin/perl
2# Copyright (C) 2014 Sergey Poznyakoff <gray@gnu.org> 2# Copyright (C) 2014-2016 Sergey Poznyakoff <gray@gnu.org>
3# 3#
4# This program is free software; you can redistribute it and/or modify 4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by 5# it under the terms of the GNU General Public License as published by
@@ -22,30 +22,35 @@ use Sys::Hostname;
22use Cwd qw(getcwd realpath); 22use Cwd qw(getcwd realpath);
23use Net::DNS; 23use Net::DNS;
24 24
25my $script; # This script name; 25my $progname; # This script name;
26 26my $progdescr = "update DNS from Apache virtual host configuration";
27my $config_file = "/etc/vhostcname.conf"; 27my $config_file = "/etc/vhostcname.conf";
28my %config = (
29 core => {
30 'cache' => "/var/run/vhostcname.cache",
31# Default TTL.
32 'ttl' => 3600,
33# A globbing pattern for Apache configuration files.
34 'apache-config-pattern' => "*",
35 }
36);
37
38use constant EX_OK => 0;
39use constant EX_NOTUPDATED => 1;
40use constant EX_USAGE => 64;
41use constant EX_NOINPUT => 66;
42use constant EX_CANTCREAT => 73;
43use constant EX_CONFIG => 78;
28 44
29my $cnamelist = "/var/run/vhostcname.cache";
30my $host; # This host name. 45my $host; # This host name.
31my @zone; # List of acceptable DNS zones.
32my $nameserver; # Nameserver to use for updates. 46my $nameserver; # Nameserver to use for updates.
33my @tsig_args; # Arguments to sing_tsig (path to the DNSSEC key file, or
34 # the key name and hash.
35my $ttl = 3600; # Default TTL.
36my $confdir; # Apache configuration directory.
37my $confpat = "*"; # A globbing pattern for Apache configuration files.
38my $dry_run; # Dry-run mode. 47my $dry_run; # Dry-run mode.
39my $debug; # Debug level. 48my $debug; # Debug level.
40my $allow_wildcard_domains;
41
42my $help; # Display help summary.
43my $man; # Ditto in manpage format.
44 49
45my $status = 0; # Default exit status. 50my $status = EX_OK;# Default exit status.
46 51
47sub err { 52sub err {
48 print STDERR "$script: "; 53 print STDERR "$progname: ";
49 print STDERR $_ for (@_); 54 print STDERR $_ for (@_);
50 print STDERR "\n"; 55 print STDERR "\n";
51 } 56 }
@@ -56,25 +61,193 @@ sub abend {
56 exit($code); 61 exit($code);
57} 62}
58 63
59sub read_config_file($) { 64sub parse_section {
60 my $file = shift; 65 my ($conf, $input) = @_;
61 unless (-f $file) { 66 my $ref = $conf;
62 print STDERR "$script: configuration file $file does not exist\n" 67 my $quote;
63 if ($debug); 68 my $rootname;
64 return; 69 while ($input ne '') {
70 my $name;
71 if (!defined($quote)) {
72 if ($input =~ /^"(.*)/) {
73 $quote = '';
74 $input = $1;
75 } elsif ($input =~ /^(.+?)(?:\s+|")(.*)/) {
76 $name = $1;
77 $input = $2;
78 } else {
79 $name = $input;
80 $input = '';
81 }
82 } else {
83 if ($input =~ /^([^\\"]*)\\(.)(.*)/) {
84 $quote .= $1 . $2;
85 $input = $3;
86 } elsif ($input =~ /^([^\\"]*)"\s*(.*)/) {
87 $name = $quote . $1;
88 $input = $2;
89 $quote = undef;
90 } else {
91 die "unparsable input $input";
92 }
93 }
94
95 if (defined($name)) {
96 $rootname = $name unless defined $rootname;
97 $ref->{$name} = {} unless ref($ref->{$name}) eq 'HASH';
98 $ref = $ref->{$name};
99 $name = undef;
65 } 100 }
66 print STDERR "$script: reading $file\n" if ($debug); 101 }
67 open(my $fd, "<", $file) or abend(1, "cannot open $file: $!"); 102 return ($ref, $rootname);
103}
104
105sub check_mandatory {
106 my ($section, $kw, $loc, $s) = @_;
107 my $err = 0;
108 while (my ($k, $d) = each %{$kw}) {
109 if (ref($d) eq 'HASH'
110 and $d->{mandatory}
111 and !exists($section->{$k})) {
112 if (exists($d->{section})) {
113 if ($s) {
114 err("$loc: mandatory section [$k] not present");
115 ++$err;
116 }
117 } else {
118 err("$loc: mandatory variable \"$k\" not set");
119 ++$err;
120 }
121 }
122 }
123 return $err;
124}
125
126sub readconfig {
127 my $file = shift;
128 my $conf = shift;
129 my %param = @_;
130
131# debug(2, "reading $file");
132 open(my $fd, "<", $file)
133 or do {
134 err("can't open configuration file $file: $!");
135 return 1 if $param{include};
136 exit(EX_NOINPUT);
137 };
138
139 my $line;
140 my $err;
141 my $section = $conf;
142 my $kw = $param{kw};
143 my $include = 0;
144 my $rootname;
145
68 while (<$fd>) { 146 while (<$fd>) {
147 ++$line;
69 chomp; 148 chomp;
149 if (/\\$/) {
150 chop;
151 $_ .= <$fd>;
152 redo;
153 }
154
70 s/^\s+//; 155 s/^\s+//;
71 s/\s+$//; 156 s/\s+$//;
72 s/\s+=\s+/=/;
73 s/#.*//; 157 s/#.*//;
74 next if ($_ eq ""); 158 next if ($_ eq "");
75 unshift(@ARGV, "--$_"); 159
160 if (/^\[(.+?)\]$/) {
161 $include = 0;
162 my $arg = $1;
163 $arg =~ s/^\s+//;
164 $arg =~ s/\s+$//;
165 if ($arg eq 'include') {
166 $include = 1;
167 } else {
168 ($section, $rootname) = parse_section($conf, $1);
169 if (ref($param{kw}) eq 'HASH') {
170 if (defined($rootname)) {
171 if (ref($param{kw}{$rootname}) eq 'HASH'
172 and exists($param{kw}{$rootname}{section})) {
173 $kw = $param{kw}{$rootname}{section};
174 } else {
175 err("$file:$line: unknown section");
176 $kw = undef;
177 }
178 } else {
179 $kw = $param{kw};
180 }
76 } 181 }
77 close($fd); 182 }
183 } elsif (/([\w_-]+)\s*=\s*(.*)/) {
184 my ($k, $v) = ($1, $2);
185 $k = lc($k) if $param{ci};
186
187 if ($include) {
188 if ($k eq 'path') {
189 $err += readconfig($v, $conf, include => 1, @_);
190 } elsif ($k eq 'pathopt') {
191 $err += readconfig($v, $conf, include => 1, @_)
192 if -f $v;
193 } elsif ($k eq 'glob') {
194 foreach my $file (bsd_glob($v, 0)) {
195 $err += readconfig($file, $conf, include => 1, @_);
196 }
197 } else {
198 err("$file:$line: unknown keyword");
199 ++$err;
200 }
201 next;
202 }
203
204 if (defined($kw)) {
205 my $x = $kw->{$k};
206 if (!defined($x)) {
207 err("$file:$line: unknown keyword $k");
208 ++$err;
209 next;
210 } elsif (ref($x) eq 'HASH') {
211 if (exists($x->{re})) {
212 if ($v !~ /$x->{re}/) {
213 err("$file:$line: invalid value for $k");
214 ++$err;
215 next;
216 }
217 if (exists($x->{check})
218 and !&{$x->{check}}($k, $v, "$file:$line")) {
219 ++$err;
220 next;
221 }
222 } elsif (exists($x->{check})) {
223 if (!&{$x->{check}}($k, $v, "$file:$line")) {
224 ++$err;
225 next;
226 }
227 } elsif (!exists($x->{var}) and
228 !exists($x->{parser}) and
229 !exists($x->{mandatory})) {
230 err("$file:$line: unknown keyword $k");
231 ++$err;
232 next;
233 }
234 if (exists($x->{parser})
235 and !&{$x->{parser}}($k, \$v, "$file:$line")) {
236 ++$err;
237 next;
238 }
239 }
240 }
241
242 $section->{$k} = $v;
243 } else {
244 err("$file:$line: malformed line");
245 ++$err;
246 next;
247 }
248 }
249 close $fd;
250 exit(EX_CONFIG) if $err;
78} 251}
79 252
80# Domain names may be formed from the set of alphanumeric ASCII characters 253# Domain names may be formed from the set of alphanumeric ASCII characters
@@ -83,7 +256,7 @@ sub read_config_file($) {
83# label. 256# label.
84sub valid_domain_name { 257sub valid_domain_name {
85 my $name = shift; 258 my $name = shift;
86 $name =~ s/^\*\.// if ($allow_wildcard_domains); 259 $name =~ s/^\*\.// if ($config{core}{'allow-wildcards'});
87 foreach my $label (split(/\./, $name)) { 260 foreach my $label (split(/\./, $name)) {
88 $label =~ s/-+/-/g; 261 $label =~ s/-+/-/g;
89 $label =~ s/[a-zA-Z0-9]-[a-zA-Z0-9]//g; 262 $label =~ s/[a-zA-Z0-9]-[a-zA-Z0-9]//g;
@@ -97,9 +270,9 @@ sub get_cnames($) {
97 my $dir = shift; 270 my $dir = shift;
98 my %ret; 271 my %ret;
99 272
100 foreach my $file (glob "$dir/$confpat") { 273 foreach my $file (glob "$dir/$config{core}{'apache-config-pattern'}") {
101 next unless (-f $file); 274 next unless (-f $file);
102 print STDERR "$script: reading cnames from $file\n" if ($debug > 2); 275 print STDERR "$progname: reading cnames from $file\n" if ($debug > 2);
103 276
104 open(my $fd, "<", $file) or do { 277 open(my $fd, "<", $file) or do {
105 err("can't open file $file: $!"); 278 err("can't open file $file: $!");
@@ -114,13 +287,13 @@ sub get_cnames($) {
114 if (/^Server(Name|Alias)\s+(.*)/) { 287 if (/^Server(Name|Alias)\s+(.*)/) {
115 foreach my $name (split /\s+/, $2) { 288 foreach my $name (split /\s+/, $2) {
116 unless (valid_domain_name($name)) { 289 unless (valid_domain_name($name)) {
117 print STDERR "$script: $file:$line: $name: invalid domain name\n"; 290 print STDERR "$progname: $file:$line: $name: invalid domain name\n";
118 next; 291 next;
119 } 292 }
120 foreach my $z (@zone) { 293 foreach my $z (keys %{$config{zone}}) {
121 if ($name =~ /.*\.$z$/) { 294 if ($name =~ /.*\.$z$/) {
122 if ($name =~ /^\*\.(.+)/ and $1 eq $z) { 295 if ($name =~ /^\*\.(.+)/ and $1 eq $z) {
123 print STDERR "$script: $file:$line: $name: first-level wildcard\n"; 296 print STDERR "$progname: $file:$line: $name: first-level wildcard\n";
124 next; 297 next;
125 } 298 }
126 $ret{$name} = $z; 299 $ret{$name} = $z;
@@ -141,7 +314,7 @@ sub read_cname_list($) {
141 my %ret; 314 my %ret;
142 315
143 if (-f $file) { 316 if (-f $file) {
144 open(my $fd, "<", $file) or abend(1, "cannot open $file: $!"); 317 open(my $fd, "<", $file) or abend(EX_NOINPUT, "cannot open $file: $!");
145 while (<$fd>) { 318 while (<$fd>) {
146 chomp; 319 chomp;
147 s/^\s+//; 320 s/^\s+//;
@@ -161,7 +334,8 @@ sub write_cname_list {
161 334
162 return if ($dry_run); 335 return if ($dry_run);
163 336
164 open(my $fd, ">", $file) or abend(1, "cannot open $file for writing: $!"); 337 open(my $fd, ">", $file) or
338 abend(EX_CANTCREAT, "cannot open $file for writing: $!");
165 foreach my $h (sort keys %hash) { 339 foreach my $h (sort keys %hash) {
166 print $fd "$h $hash{$h}\n"; 340 print $fd "$h $hash{$h}\n";
167 } 341 }
@@ -169,13 +343,14 @@ sub write_cname_list {
169} 343}
170 344
171sub ns_update { 345sub ns_update {
172 my $resolver = shift;
173 my $name = shift; 346 my $name = shift;
174 my $domain = shift; 347 my $domain = shift;
175 my %hash = @_; 348 my %hash = @_;
176 my %ignorerr; 349 my %ignorerr;
177 350
178 print STDERR "$script: updating $name in $domain: ". 351 my $resolver = get_zone_resolver($domain);
352
353 print STDERR "$progname: updating $name in $domain: ".
179 join(',', map { "$_ => $hash{$_}" } keys %hash) . 354 join(',', map { "$_ => $hash{$_}" } keys %hash) .
180 "\n" if ($debug > 1); 355 "\n" if ($debug > 1);
181 return 1 if ($dry_run); 356 return 1 if ($dry_run);
@@ -189,13 +364,13 @@ sub ns_update {
189 $update->push($k => $v); 364 $update->push($k => $v);
190 } 365 }
191 } 366 }
192 $update->sign_tsig(@tsig_args) if ($#tsig_args >= 0); 367 zone_sign_tsig($update, $domain);
193 my $reply = $resolver->send($update); 368 my $reply = $resolver->send($update);
194 if ($reply) { 369 if ($reply) {
195 if ($reply->header->rcode eq 'NOERROR') { 370 if ($reply->header->rcode eq 'NOERROR') {
196 print STDERR "$script: update successful\n" if ($debug>3); 371 print STDERR "$progname: update successful\n" if ($debug>3);
197 } elsif ($ignorerr{$reply->header->rcode}) { 372 } elsif ($ignorerr{$reply->header->rcode}) {
198 print STDERR "$script: ignoring " . $reply->header->rcode . ': ' . 373 print STDERR "$progname: ignoring " . $reply->header->rcode . ': ' .
199 join(',', map { "$_ => $hash{$_}" } keys %hash) . "\n" 374 join(',', map { "$_ => $hash{$_}" } keys %hash) . "\n"
200 if ($debug>3); 375 if ($debug>3);
201 } else { 376 } else {
@@ -203,7 +378,7 @@ sub ns_update {
203 join(',', map { "$_ => $hash{$_}" } keys %hash), 378 join(',', map { "$_ => $hash{$_}" } keys %hash),
204 ': ', 379 ': ',
205 $reply->header->rcode); 380 $reply->header->rcode);
206 $status = 2; 381 $status = EX_NOTUPDATED;
207 return 0; 382 return 0;
208 } 383 }
209 } else { 384 } else {
@@ -211,51 +386,79 @@ sub ns_update {
211 join(',', map { "$_ => $hash{$_}" } keys %hash), 386 join(',', map { "$_ => $hash{$_}" } keys %hash),
212 ': ', 387 ': ',
213 $resolver->errorstring); 388 $resolver->errorstring);
214 $status = 2; 389 $status = EX_NOTUPDATED;
215 return 0; 390 return 0;
216 } 391 }
217 return 1; 392 return 1;
218} 393}
219 394
395sub get_zone_resolver {
396 my $zone = shift;
397 unless (defined($config{zone}{$zone}{resolver})) {
398 my $resolver = new Net::DNS::Resolver;
399 $resolver->nameservers($config{zone}{$zone}{server})
400 if defined $config{zone}{$zone}{server};
401 $config{zone}{$zone}{resolver} = $resolver;
402 }
403 return $config{zone}{$zone}{resolver};
404}
405
406sub zone_sign_tsig {
407 my ($update, $zone) = @_;
408 my @tsig_args;
409
410 my $zcfg = $config{zone}{$zone};
411 if (exists($zcfg->{'ns-key-file'})) {
412 push @tsig_args, split(/\s+/, $zcfg->{'ns-key-file'});
413 } elsif (exists($zcfg->{'ns-key'})) {
414 push @tsig_args, @{$zcfg->{'ns-key'}};
415 }
416 if ($#tsig_args == -1) {
417 if (exists($config{core}{'ns-key-file'})) {
418 push @tsig_args, split(/\s+/, $config{core}{'ns-key-file'});
419 } elsif (exists($config{core}{'ns-key'})) {
420 push @tsig_args, @{$config{core}{'ns-key'}};
421 }
422 }
423 $update->sign_tsig(@tsig_args) if ($#tsig_args >= 0);
424}
425
220sub update_cnames_from_hash { 426sub update_cnames_from_hash {
221 my %hash = @_; 427 my %hash = @_;
222 428
223 print STDERR "$script: " . keys(%hash) . " names to update\n" 429 print STDERR "$progname: " . keys(%hash) . " names to update\n"
224 if ($debug > 2); 430 if ($debug > 2);
225 my %oldhash = read_cname_list($cnamelist); 431 my %oldhash = read_cname_list($config{core}{cache});
226 my @namelist = sort(keys(%hash)); 432 my @namelist = sort(keys(%hash));
227 if (join(",", @namelist) eq join(".", sort(keys(%oldhash)))) { 433 if (join(",", @namelist) eq join(",", sort(keys(%oldhash)))) {
228 print STDERR "$script: nothing to update\n" if ($debug); 434 print STDERR "$progname: nothing to update\n" if ($debug);
229 return; 435 return;
230 } 436 }
231 437
232 my $resolver = new Net::DNS::Resolver;
233 $resolver->nameservers($nameserver) if defined($nameserver);
234
235 my $name; 438 my $name;
236 foreach $name (@namelist) { 439 foreach $name (@namelist) {
237 if ($oldhash{$name}) { 440 if ($oldhash{$name}) {
238 delete $oldhash{$name}; 441 delete $oldhash{$name};
239 } else { 442 } else {
240 ns_update($resolver, $name, $hash{$name}, 443 ns_update($name, $hash{$name},
241 prereq => yxdomain($name), 444 prereq => yxdomain($name),
242 update => rr_del($name), 445 update => rr_del($name),
243 ignore => 'NXDOMAIN'); 446 ignore => 'NXDOMAIN');
244 print STDERR "$script: $name $ttl CNAME $host\n" if ($debug); 447 print STDERR "$progname: $name $config{core}{ttl} CNAME $config{core}{hostname}\n" if ($debug);
245 delete $hash{$name} 448 delete $hash{$name}
246 unless ns_update($resolver, $name, $hash{$name}, 449 unless ns_update($name, $hash{$name},
247 update => rr_add("$name $ttl CNAME $host")); 450 update => rr_add("$name $config{core}{ttl} CNAME $config{core}{hostname}"));
248 } 451 }
249 } 452 }
250 453
251 foreach $name (keys %oldhash) { 454 foreach $name (keys %oldhash) {
252 ns_update($resolver, $name, $oldhash{$name}, 455 ns_update($name, $oldhash{$name},
253 prereq => yxrrset("$name CNAME"), 456 prereq => yxrrset("$name CNAME"),
254 update => rr_del("$name CNAME"), 457 update => rr_del("$name CNAME"),
255 ignore => 'NXRRSET'); 458 ignore => 'NXRRSET');
256 } 459 }
257 460
258 write_cname_list($cnamelist, %hash); 461 write_cname_list($config{core}{cache}, %hash);
259} 462}
260 463
261sub update_cnames_from_dir($) { 464sub update_cnames_from_dir($) {
@@ -263,94 +466,206 @@ sub update_cnames_from_dir($) {
263} 466}
264 467
265sub nscleanup { 468sub nscleanup {
266 print STDERR "$script: Removing DNS CNAME records\n" if ($debug); 469 print STDERR "$progname: Removing DNS CNAME records\n" if ($debug);
267
268 my $resolver = new Net::DNS::Resolver;
269 $resolver->nameservers($nameserver) if defined($nameserver);
270 470
271 my %hash = read_cname_list($cnamelist); 471 my %hash = read_cname_list($config{core}{cache});
272 foreach my $name (keys %hash) { 472 foreach my $name (keys %hash) {
273 print STDERR "$script: removing $name from $hash{$name}\n" 473 print STDERR "$progname: removing $name from $hash{$name}\n"
274 if ($debug); 474 if ($debug);
275 delete $hash{$name} 475 delete $hash{$name}
276 if ns_update($resolver, $name, $hash{$name}, 476 if ns_update($name, $hash{$name},
277 prereq => yxrrset("$name CNAME"), 477 prereq => yxrrset("$name CNAME"),
278 update => rr_del("$name CNAME"), 478 update => rr_del("$name CNAME"),
279 ignore => 'NXRRSET'); 479 ignore => 'NXRRSET');
280 } 480 }
281 481
282 write_cname_list($cnamelist, %hash); 482 write_cname_list($config{core}{cache}, %hash);
483}
484
485###
486sub com_start {
487 abend(EX_USAGE, "too many arguments") unless $#_ == 0;
488 nscleanup();
489 com_reload(@_);
490}
491
492sub com_reload {
493 abend(EX_USAGE, "too many arguments") unless $#_ == 0;
494 my $confdir = -d "$config{core}{'apache-config-directory'}/sites-enabled"
495 ? "$config{core}{'apache-config-directory'}/sites-enabled"
496 : $config{core}{'apache-config-directory'};
497 my %cnames = get_cnames($confdir);
498 update_cnames_from_hash(%cnames);
499 print STDERR "$progname: no cnames defined\n" unless (keys(%cnames) > 0);
283} 500}
284 501
502sub com_stop {
503 abend(EX_USAGE, "too many arguments") unless $#_ == 0;
504 nscleanup;
505}
506
507sub com_status {
508 err("status command ignored");
509 my %stat;
510
511 my %hash = read_cname_list($config{core}{cache});
512 while (my ($name, $zone) = each %hash) {
513 #$name =~ s/.$zone$//;
514 push @{${stat}{$zone}}, $name;
515 }
516
517 foreach my $zone (sort(keys %stat)) {
518 print "Names in zone $zone:\n";
519 foreach my $name (sort(@{$stat{$zone}})) {
520 print " $name\n";
521 }
522 }
523}
285 524
286### 525###
287($script = $0) =~ s/.*\///; 526($progname = $0) =~ s/.*\///;
527
528my %comtab = (
529 start => \&com_start,
530 restart => \&com_reload,
531 'force-restart' => \&com_start,
532 reload => \&com_reload,
533 stop => \&com_stop,
534 status => \&com_status
535);
536
537sub getcom {
538 my $com = shift;
539
540 while (defined($comtab{$com}) and ref($comtab{$com}) ne 'CODE') {
541 $com = $comtab{$com};
542 }
543 die "internal error: unresolved command alias" unless defined $com;
544 return $comtab{$com} if defined $comtab{$com};
545
546 my @v = map { /^$com/ ? $_ : () } sort keys %comtab;
547 if ($#v == -1) {
548 abend(EX_USAGE, "unrecognized command");
549 } elsif ($#v > 0) {
550 abend(EX_USAGE, "ambiguous command: ".join(', ', @v));
551 }
552 return getcom($v[0]);
553}
554
288 555
289## Read configuration 556## Read configuration
290read_config_file($ENV{'VHOSTCNAME_CONF'} ? 557sub parse_ns_key {
291 $ENV{'VHOSTCNAME_CONF'} : $config_file); 558 my ($var, $ref, $loc) = @_;
559 my @result;
560 if ($$ref =~ /(.+?)=(.+)/) {
561 push @result, $1, $2;
562 $$ref = \@result;
563 } else {
564 err("$loc: $var argument must be must be NAME=KEY");
565 return 0;
566 }
567 return 1;
568}
292 569
293GetOptions("help" => \$man, 570sub parse_boolean {
294 "h" => \$help, 571 my ($var, $ref, $loc) = @_;
295 "debug|d+" => \$debug, 572 my %bool = ( yes => 1,
296 "dry-run|n" => \$dry_run, 573 no => 0,
297 "hostname|H=s" => \$host, 574 true => 1,
298 "apache-config-pattern=s" => \$confpat, 575 false => 0,
299 "apache-config-directory=s" => \$confdir, 576 t => 1,
300 "ns-key-file=s" => sub { 577 nil => 0,
301 abend(3, "NS key already set") if ($#tsig_args >= 0); 578 f => 0,
302 push @tsig_args, $_[1]; 579 on => 1,
303 }, 580 off => 0,
304 "ns-key=s" => sub { 581 1 => 1,
305 abend(3, "NS key already set") if ($#tsig_args >= 0); 582 0 => 0);
306 if ($_[1] =~ /(.+?)=(.+)/) { 583
307 push @tsig_args, $1; 584 my $s = $$ref;
308 push @tsig_args, $2; 585 $s =~ tr/A-Z/a-z/;
586 if (exists($bool{$s})) {
587 $$ref = $bool{$s};
309 } else { 588 } else {
310 abend(3, "argument to --ns-key must be NAME=KEY"); 589 err("$loc: argument must be boolean");
590 return 0;
591 }
592 return 1;
311 } 593 }
594
595my %kw = (
596 core => {
597 section => {
598 'apache-config-directory' => 1,
599 'apache-config-pattern' => 1,
600 'cache' => 1,
601 'server' => 1,
602 'ttl' => 1,
603 'ns-key' => { parser => \&parse_ns_key },
604 'ns-key-file' => 1,
605 'hostname' => 1,
606 'allow-wildcards' => { parser => \&parse_boolean }
607 },
312 }, 608 },
313 "cname-file=s" => \$cnamelist, 609 zone => {
314 "zone|z=s@" => \@zone, 610 section => {
315 "ttl=i" => \$ttl, 611 'server' => 1,
316 "server=s" => \$nameserver, 612 'ttl' => 1,
317 "allow-wildcard-domains" => \$allow_wildcard_domains 613 'ns-key' => { parser => \&parse_ns_key },
318 ) or exit(3); 614 'ns-key-file' => 1
319 615 },
320pod2usage(-message => "$script: update DNS from Apache virtual host configuration", 616 }
321 -exitstatus => 0) if $help; 617);
322pod2usage(-exitstatus => 0, -verbose => 2) if $man; 618
323 619GetOptions("help" => sub {
324unless (defined($confdir)) { 620 pod2usage(-exitstatus => EX_OK, -verbose => 2);
621 },
622 "h" => sub {
623 pod2usage(-message => "$progname: $progdescr",
624 -exitstatus => EX_OK);
625 },
626 "usage" => sub {
627 pod2usage(-exitstatus => EX_OK, -verbose => 0);
628 },
629
630 "debug|d+" => \$debug,
631 "dry-run|n" => \$dry_run,
632 "config|c=s" => \$config_file,
633 ) or exit(EX_USAGE);
634
635readconfig($config_file, \%config, kw => \%kw);
636
637unless (defined($config{core}{'apache-config-directory'})) {
325 foreach my $dir ("/etc/apache2", "/etc/httpd") { 638 foreach my $dir ("/etc/apache2", "/etc/httpd") {
326 if (-e "$dir/sites-enabled" and -e "$dir/sites-available") { 639 if (-e "$dir/sites-enabled" and -e "$dir/sites-available") {
327 $confdir = $dir; 640 $config{core}{'apache-config-directory'} = $dir;
328 last; 641 last;
329 } 642 }
330 if (-e "$dir/vhosts.d") { 643 if (-e "$dir/vhosts.d") {
331 $confdir = "$dir/vhosts.d"; 644 $config{core}{'apache-config-directory'} = "$dir/vhosts.d";
332 last; 645 last;
333 } 646 }
334 } 647 }
335 abend(3, 648 abend(EX_CONFIG,
336 "don't know where virtual host configurations are located; use --apache-config-directory option") 649 "don't know where virtual host configurations are located; define apache-config-directory")
337 unless defined($confdir); 650 unless defined($config{core}{'apache-config-directory'});
338} 651}
339 652
340$host = hostname() unless defined($host); 653$config{core}{hostname} = hostname() unless defined($config{core}{hostname});
341push(@zone, $host) if ($#zone == -1); 654$config{zone}{$host} = {} unless exists $config{zone};
655
342$debug++ if ($dry_run); 656$debug++ if ($dry_run);
343 657
344if ($#ARGV == -1) { 658if ($#ARGV == -1) {
345 abend(3, "command not given") unless ($ENV{'DIREVENT_FILE'}); 659 abend(EX_USAGE, "command not given") unless ($ENV{'DIREVENT_FILE'});
346 print STDERR "$script: started as direvent handler for " . 660 print STDERR "$progname: started as direvent handler for " .
347 "$ENV{'DIREVENT_GENEV_NAME'} on $ENV{'DIREVENT_FILE'}\n" 661 "$ENV{'DIREVENT_GENEV_NAME'} on $ENV{'DIREVENT_FILE'}\n"
348 if ($debug); 662 if ($debug);
349 my $cwd = getcwd; 663 my $cwd = getcwd;
350 my $update_dir; 664 my $update_dir;
665 my $confdir = $config{core}{'apache-config-directory'};
351 if (-d "$confdir/sites-available" && -d "$confdir/sites-enabled") { 666 if (-d "$confdir/sites-available" && -d "$confdir/sites-enabled") {
352 if ($cwd eq "$confdir/sites-available") { 667 if ($cwd eq "$confdir/sites-available") {
353 foreach my $file (glob "$confdir/sites-enabled/$confpat") { 668 foreach my $file (glob "$confdir/sites-enabled/$config{core}{'apache-config-pattern'}") {
354 next unless (-l $file); 669 next unless (-l $file);
355 if (realpath(readlink($file)) eq 670 if (realpath(readlink($file)) eq
356 "$confdir/sites-available/$ENV{'DIREVENT_FILE'}") { 671 "$confdir/sites-available/$ENV{'DIREVENT_FILE'}") {
@@ -366,22 +681,11 @@ if ($#ARGV == -1) {
366 } 681 }
367 682
368 update_cnames_from_dir($update_dir) if defined($update_dir); 683 update_cnames_from_dir($update_dir) if defined($update_dir);
369} elsif ($#ARGV != 0) {
370 abend(3, "too many arguments");
371} elsif ($ARGV[0] =~ /^start|restart|force-restart|reload$/) {
372 nscleanup if ($ARGV[0] =~ /start$/);
373 my %cnames = get_cnames(-d "$confdir/sites-enabled" ?
374 "$confdir/sites-enabled" : $confdir);
375 update_cnames_from_hash(%cnames);
376 print STDERR "$script: no cnames defined\n" unless (keys(%cnames) > 0);
377} elsif ($ARGV[0] eq "stop") {
378 nscleanup;
379} elsif ($ARGV[0] eq "status") {
380 err("status command ignored");
381} else {
382 abend(3, "invalid command, try $script --help for more info");
383} 684}
384 685
686my $command = getcom($ARGV[0]);
687&{$command}(@ARGV);
688
385exit($status); 689exit($status);
386 690
387__END__ 691__END__
@@ -431,24 +735,39 @@ corresponding B<direvent.conf>(5) entry:
431 735
432=head1 COMMANDS 736=head1 COMMANDS
433 737
738Unless the program is started as a B<direvent>(8) handler, exactly one
739command must be given in the command line. A command may be supplied
740in full or abbreviated form. Any unambiguous abbreviation is allowed.
741
742Available commands are:
743
434=over 4 744=over 4
435 745
436=item B<start> 746=item B<start>
437 747
438Scan the apache configuration files and register all server names matching 748Scan the apache configuration files and register all server names that
439the supplied zones. 749match the configured zones.
440 750
441=item B<stop> 751=item B<stop>
442 752
443Deregister all hostnames registered previously. 753Deregister all hostnames registered previously.
444 754
445=item B<restart>, B<force-restart>, B<reload> 755=item B<restart>, B<reload>
446 756
447Same as running B<vhostcname stop; vhostcname start>. 757Builds a list of names from the apache configuration (I<apache-list>) and
758compares them with the names registered at the previous run (I<cache>). If
759the two lists differ, the names present in I<apache-list>, but absent in
760I<cache> are registered. The names present in I<cache>, but lacking in
761I<apache-list> are deleted from the DNS.
762
763=item B<force-restart>
764
765Deregister all hostnames registered previously, rescan Apache files, and
766register all names that match the configured zones.
448 767
449=item B<status> 768=item B<status>
450 769
451Ignored 770Displays registered host names.
452 771
453=back 772=back
454 773
@@ -456,13 +775,56 @@ Ignored
456 775
457=over 4 776=over 4
458 777
459=item B<--allow-wildcard-domains> 778=item B<-c>, B<--config=>I<FILE>
779
780Read configuration from I<FILE> instead of the default location
781(F</etc/vhostcname.conf>).
782
783=item B<-d>, B<--debug>
784
785Increases the debug level. Multiple B<-d> options are allowed.
786
787=item B<-n>, B<--dry-run>,
788
789Enables I<dry-run> mode: print what would have been done without actually
790doing it.
791
792=item B<--help>
793
794Displays B<vhostcname> man page.
795
796=item B<-h>
797
798Displays a short help summary and exits.
799
800=item B<--usage>
801
802Displays a short command line syntax reminder.
803
804=back
805
806=head1 CONFIGURATION FILE
807
808Configuration is read from F</etc/vhostcname.conf> or a file specified
809by the B<--config> (B<-c>) command line option. The file consists of
810a number of variable assignments (I<variable> B<=> I<value>), grouped into
811sections. Whitespace is ignored, except that it serves to separate input
812tokens. I<value> is read verbatim, including eventual whitespace characters
813that can appear within it.
814
815A section begins with the line containing its name within square brackets
816(e.g. B<[core]>). The name can be followed by one or more arguments, if
817the section semantics requires so (e.g. B<[zone example.com]>).
818
819The following sections are recognized:
820
821=over 4
822
823=item B<[core]>
460 824
461Allow the use of wildcard (B<*>). When this option is in effect, a wildcard 825=over 8
462will be allowed if it is the very first label in a domain name and it is
463separated from the base zone (see the B<--zone> option) by one or more labels.
464 826
465=item B<--apache-config-directory=>I<DIR> 827=item B<apache-config-directory => I<DIR>
466 828
467Sets the Apache configuration directory. I<DIR> should be either a directory 829Sets the Apache configuration directory. I<DIR> should be either a directory
468where virtual configuration file are located or a directory which hosts the 830where virtual configuration file are located or a directory which hosts the
@@ -474,93 +836,99 @@ If this option is not given, B<vhostcname> will try to deduce where the
474configuration files are located. It will issue a warning message and 836configuration files are located. It will issue a warning message and
475terminate if unable to do that. 837terminate if unable to do that.
476 838
477=item B<--apache-config-pattern=>I<GLOB> 839=item B<apache-config-pattern => I<PATTERN>
478 840
479Shell globbing pattern for virtual host configuration files. By default, 841Shell globbing pattern for virtual host configuration files. By default,
480B<*> is used, meaning that B<vhostcname> will scan all files in the 842B<*> is used, meaning that B<vhostcname> will scan all files in the
481configuration directory. 843configuration directory (note: that includes backup copies too!).
482 844
483=item B<--cname-file=>I<NAME> 845=item B<cache => I<FILE>
484 846
485Name of the file where B<vhostcname> will keep successfully registered 847Name of the cache file where B<vhostcname> keeps successfully registered
486host names. Default is B</var/run/vhostcname.cache>. 848host names. Default is B</var/run/vhostcname.cache>.
487 849
488=item B<-d>, B<--debug> 850=item B<hostname => I<HOSTNAME>
489 851
490Increases the debug level. Multiple B<-d> options are allowed. 852Sets the hostname. Use this if B<vhostcname> is unable to correctly
853determine it.
491 854
492=item B<-n>, B<--dry-run>, 855=item B<allow-wildcards => I<BOOL>
493 856
494Enables I<dry-run> mode: print what would have been done without actually 857Allow the use of wildcard (B<*>) in host names. When this option is in
495doing it. 858effect, a wildcard will be allowed if it is the very first label in a domain
859name and it is separated from the base zone (see the B<zone> section) by one
860more labels.
496 861
497=item B<--help> 862I<BOOL> is one of B<yes>, B<true>, B<t>, B<on>, or B<1> to allow wildcards,
863or one of B<no>, B<false>, B<f>, B<nil>, B<off>, B<0> to disallow them (the
864default).
498 865
499Displays B<vhostcname> man page. 866=back
500 867
501=item B<-h> 868The following variables provide defaults for zones that lack the
869corresponding settings:
502 870
503Displays a short help summary and exits. 871=over 8
504 872
505=item B<-H>, B<--hostname>=I<NAME> 873=item B<server => I<HOST>
506 874
507Sets the hostname. Use this if B<vhostcname> is unable to correctly 875Name of the DNS server to use. Normally B<vhostcname> determines what server
508determine it. 876to use based on the B<SOA> record of the zone to be updated, so this option
877is rarely needed.
509 878
510=item B<--ns-key=>I<NAME>=I<KEY> 879=item B<ttl => I<SECONDS>
511 880
512Define the TSIG key. 881TTL value for new DNS records. Default is 3600.
513 882
514=item B<--ns-key-file=>I<KEYFILE> 883=item B<ns-key => I<NAME>=I<HASH>
884
885Defines the TSIG key.
886
887=item B<ns-key-file => I<FILE>
515 888
516Name of the key file. The argument should be the name of a file 889Name of the key file. The argument should be the name of a file
517generated by the B<dnssec-keygen> utility. Either B<.key> or B<.private> 890generated by the B<dnssec-keygen> utility. Either B<.key> or B<.private>
518file can be used. 891file can be used.
519 892
520This option cannot be used together with B<--ns-key>. 893If both <ns-key> and B<ns-key-file> are used, the latter is given preference.
521 894
522=item B<--server=>I<NAME> 895=back
523 896
524Name of the DNS server to use. Normally B<vhostcname> determines what server 897=item B<[zone I<NAME>]>
525to use based on the B<SOA> record of the zone to be updated, so this option
526is rarely needed.
527 898
528=item B<--ttl=>I<TIME> 899The B<zone> section informs B<vhostcname> that it should handle names
900in zone I<NAME>. Any number of B<[zone]> sections can be defined. If
901none is defined, B<vhostcname> will take hostname as the name of the zone
902to update.
529 903
530TTL value for new DNS records. Default is 3600. 904The variables in a B<zone> section define parameters to be used for that
905particular zone. As such, none of them is mandatory. If the zone I<NAME>
906uses default settings (or settings, defined in the B<[core]> section),
907the section can be empty.
531 908
532=item B<--zone=>I<NAME> 909=over 8
533 910
534Name of the zone which B<vhostcname> can update. Multiple B<--zone> options 911=item B<server => I<HOST>
535can be given.
536 912
537If no B<--zone> option is given, B<vhostcname> will take hostname as the 913Name of the DNS server to use when updating this zone.
538name of the zone.
539 914
540=back 915=item B<ttl => I<SECONDS>
541 916
542=head1 CONFIGURATION FILE 917TTL for records in this zone.
543 918
544If the file B<etc/vhostcname.conf> exists, the program will read its 919=item B<ns-key => I<NAME>=I<HASH>
545configuration from it. A familiar UNIX configuration format is used.
546Empty lines and UNIX comments are ignored. Each non-empty line is either an
547option name, or option assignment, i.e. B<opt>=B<val>, with any amount of
548optional whitespace around the equals sign. Valid option names are
549the same as the long command line options, but without the leading B<-->.
550For example:
551 920
552 zone = vhost.example.com 921TSIG key.
553 ns-key-file = /etc/bind/Kvhost+157+43558.key
554 ttl = 3600
555 922
556=head1 ENVIRONMENT 923=item B<ns-key-file => I<FILE>
557 924
558=over 4 925Name of the key file. The argument should be the name of a file
926generated by the B<dnssec-keygen> utility. Either B<.key> or B<.private>
927file can be used.
559 928
560=item B<VHOSTCNAME_CONF> 929If both <ns-key> and B<ns-key-file> are used, the latter is given preference.
561 930
562The name of the configuration file to use instead of the default 931=back
563F</etc/vhostcname.conf>.
564 932
565=back 933=back
566 934
@@ -574,23 +942,25 @@ Success
574 942
575=item 1 943=item 1
576 944
577Operating system error (unable to open file, etc.) 945Some of the host names could not be updated.
578 946
579=item 2 947=item 64
580 948
581Some of the host names could not be registered. 949Command line usage error.
582 950
583=item 3 951=item 66
584 952
585Command line usage error 953Required input file cannot be opened.
586 954
587=back 955=item 73
588 956
589=head1 BUGS 957Required output file cannot be created or written.
590 958
591Only one key file can be given. This means that if you use multiple 959=item 78
592B<--zone> options, all zones must be configured to accept the same 960
593DNSSEC key. Ditto for the B<--server> option. 961Configuration error.
962
963=back
594 964
595=head1 SEE ALSO 965=head1 SEE ALSO
596 966

Return to:

Send suggestions and report system problems to the System administrator.