aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2016-02-02 12:54:52 +0200
committerSergey Poznyakoff <gray@gnu.org.ua>2016-02-02 12:54:52 +0200
commitdb032057eb89e07b79eefb8617e326e2cd037300 (patch)
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.
-rwxr-xr-xvhostcname/vhostcname774
1 files changed, 572 insertions, 202 deletions
diff --git a/vhostcname/vhostcname b/vhostcname/vhostcname
index 359101c..fc6dd2d 100755
--- a/vhostcname/vhostcname
+++ b/vhostcname/vhostcname
@@ -1,408 +1,712 @@
#!/usr/bin/perl
-# Copyright (C) 2014 Sergey Poznyakoff <gray@gnu.org>
+# Copyright (C) 2014-2016 Sergey Poznyakoff <gray@gnu.org>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
use strict;
use Getopt::Long qw(:config gnu_getopt no_ignore_case);
use Pod::Usage;
use Pod::Man;
use Sys::Hostname;
use Cwd qw(getcwd realpath);
use Net::DNS;
-my $script; # This script name;
-
+my $progname; # This script name;
+my $progdescr = "update DNS from Apache virtual host configuration";
my $config_file = "/etc/vhostcname.conf";
+my %config = (
+ core => {
+ 'cache' => "/var/run/vhostcname.cache",
+# Default TTL.
+ 'ttl' => 3600,
+# A globbing pattern for Apache configuration files.
+ 'apache-config-pattern' => "*",
+ }
+);
+
+use constant EX_OK => 0;
+use constant EX_NOTUPDATED => 1;
+use constant EX_USAGE => 64;
+use constant EX_NOINPUT => 66;
+use constant EX_CANTCREAT => 73;
+use constant EX_CONFIG => 78;
-my $cnamelist = "/var/run/vhostcname.cache";
my $host; # This host name.
-my @zone; # List of acceptable DNS zones.
my $nameserver; # Nameserver to use for updates.
-my @tsig_args; # Arguments to sing_tsig (path to the DNSSEC key file, or
- # the key name and hash.
-my $ttl = 3600; # Default TTL.
-my $confdir; # Apache configuration directory.
-my $confpat = "*"; # A globbing pattern for Apache configuration files.
my $dry_run; # Dry-run mode.
my $debug; # Debug level.
-my $allow_wildcard_domains;
-my $help; # Display help summary.
-my $man; # Ditto in manpage format.
-
-my $status = 0; # Default exit status.
+my $status = EX_OK;# Default exit status.
sub err {
- print STDERR "$script: ";
+ print STDERR "$progname: ";
print STDERR $_ for (@_);
print STDERR "\n";
}
sub abend {
my $code = shift;
&err;
exit($code);
}
-sub read_config_file($) {
- my $file = shift;
- unless (-f $file) {
- print STDERR "$script: configuration file $file does not exist\n"
- if ($debug);
- return;
+sub parse_section {
+ my ($conf, $input) = @_;
+ my $ref = $conf;
+ my $quote;
+ my $rootname;
+ while ($input ne '') {
+ my $name;
+ if (!defined($quote)) {
+ if ($input =~ /^"(.*)/) {
+ $quote = '';
+ $input = $1;
+ } elsif ($input =~ /^(.+?)(?:\s+|")(.*)/) {
+ $name = $1;
+ $input = $2;
+ } else {
+ $name = $input;
+ $input = '';
+ }
+ } else {
+ if ($input =~ /^([^\\"]*)\\(.)(.*)/) {
+ $quote .= $1 . $2;
+ $input = $3;
+ } elsif ($input =~ /^([^\\"]*)"\s*(.*)/) {
+ $name = $quote . $1;
+ $input = $2;
+ $quote = undef;
+ } else {
+ die "unparsable input $input";
+ }
+ }
+
+ if (defined($name)) {
+ $rootname = $name unless defined $rootname;
+ $ref->{$name} = {} unless ref($ref->{$name}) eq 'HASH';
+ $ref = $ref->{$name};
+ $name = undef;
+ }
}
- print STDERR "$script: reading $file\n" if ($debug);
- open(my $fd, "<", $file) or abend(1, "cannot open $file: $!");
+ return ($ref, $rootname);
+}
+
+sub check_mandatory {
+ my ($section, $kw, $loc, $s) = @_;
+ my $err = 0;
+ while (my ($k, $d) = each %{$kw}) {
+ if (ref($d) eq 'HASH'
+ and $d->{mandatory}
+ and !exists($section->{$k})) {
+ if (exists($d->{section})) {
+ if ($s) {
+ err("$loc: mandatory section [$k] not present");
+ ++$err;
+ }
+ } else {
+ err("$loc: mandatory variable \"$k\" not set");
+ ++$err;
+ }
+ }
+ }
+ return $err;
+}
+
+sub readconfig {
+ my $file = shift;
+ my $conf = shift;
+ my %param = @_;
+
+# debug(2, "reading $file");
+ open(my $fd, "<", $file)
+ or do {
+ err("can't open configuration file $file: $!");
+ return 1 if $param{include};
+ exit(EX_NOINPUT);
+ };
+
+ my $line;
+ my $err;
+ my $section = $conf;
+ my $kw = $param{kw};
+ my $include = 0;
+ my $rootname;
+
while (<$fd>) {
- chomp;
- s/^\s+//;
- s/\s+$//;
- s/\s+=\s+/=/;
- s/#.*//;
- next if ($_ eq "");
- unshift(@ARGV, "--$_");
+ ++$line;
+ chomp;
+ if (/\\$/) {
+ chop;
+ $_ .= <$fd>;
+ redo;
+ }
+
+ s/^\s+//;
+ s/\s+$//;
+ s/#.*//;
+ next if ($_ eq "");
+
+ if (/^\[(.+?)\]$/) {
+ $include = 0;
+ my $arg = $1;
+ $arg =~ s/^\s+//;
+ $arg =~ s/\s+$//;
+ if ($arg eq 'include') {
+ $include = 1;
+ } else {
+ ($section, $rootname) = parse_section($conf, $1);
+ if (ref($param{kw}) eq 'HASH') {
+ if (defined($rootname)) {
+ if (ref($param{kw}{$rootname}) eq 'HASH'
+ and exists($param{kw}{$rootname}{section})) {
+ $kw = $param{kw}{$rootname}{section};
+ } else {
+ err("$file:$line: unknown section");
+ $kw = undef;
+ }
+ } else {
+ $kw = $param{kw};
+ }
+ }
+ }
+ } elsif (/([\w_-]+)\s*=\s*(.*)/) {
+ my ($k, $v) = ($1, $2);
+ $k = lc($k) if $param{ci};
+
+ if ($include) {
+ if ($k eq 'path') {
+ $err += readconfig($v, $conf, include => 1, @_);
+ } elsif ($k eq 'pathopt') {
+ $err += readconfig($v, $conf, include => 1, @_)
+ if -f $v;
+ } elsif ($k eq 'glob') {
+ foreach my $file (bsd_glob($v, 0)) {
+ $err += readconfig($file, $conf, include => 1, @_);
+ }
+ } else {
+ err("$file:$line: unknown keyword");
+ ++$err;
+ }
+ next;
+ }
+
+ if (defined($kw)) {
+ my $x = $kw->{$k};
+ if (!defined($x)) {
+ err("$file:$line: unknown keyword $k");
+ ++$err;
+ next;
+ } elsif (ref($x) eq 'HASH') {
+ if (exists($x->{re})) {
+ if ($v !~ /$x->{re}/) {
+ err("$file:$line: invalid value for $k");
+ ++$err;
+ next;
+ }
+ if (exists($x->{check})
+ and !&{$x->{check}}($k, $v, "$file:$line")) {
+ ++$err;
+ next;
+ }
+ } elsif (exists($x->{check})) {
+ if (!&{$x->{check}}($k, $v, "$file:$line")) {
+ ++$err;
+ next;
+ }
+ } elsif (!exists($x->{var}) and
+ !exists($x->{parser}) and
+ !exists($x->{mandatory})) {
+ err("$file:$line: unknown keyword $k");
+ ++$err;
+ next;
+ }
+ if (exists($x->{parser})
+ and !&{$x->{parser}}($k, \$v, "$file:$line")) {
+ ++$err;
+ next;
+ }
+ }
+ }
+
+ $section->{$k} = $v;
+ } else {
+ err("$file:$line: malformed line");
+ ++$err;
+ next;
+ }
}
- close($fd);
+ close $fd;
+ exit(EX_CONFIG) if $err;
}
# Domain names may be formed from the set of alphanumeric ASCII characters
# (a-z, A-Z, 0-9). In addition the hyphen is permitted if it is surrounded
# by characters, digits or hyphens, although it is not to start or end a
# label.
sub valid_domain_name {
my $name = shift;
- $name =~ s/^\*\.// if ($allow_wildcard_domains);
+ $name =~ s/^\*\.// if ($config{core}{'allow-wildcards'});
foreach my $label (split(/\./, $name)) {
$label =~ s/-+/-/g;
$label =~ s/[a-zA-Z0-9]-[a-zA-Z0-9]//g;
return 0 if $label =~ /^-/ or $label =~ /-$/;
return 0 if $label =~ /[^a-zA-Z0-9]/;
}
return 1;
}
sub get_cnames($) {
my $dir = shift;
my %ret;
- foreach my $file (glob "$dir/$confpat") {
+ foreach my $file (glob "$dir/$config{core}{'apache-config-pattern'}") {
next unless (-f $file);
- print STDERR "$script: reading cnames from $file\n" if ($debug > 2);
+ print STDERR "$progname: reading cnames from $file\n" if ($debug > 2);
open(my $fd, "<", $file) or do {
err("can't open file $file: $!");
next;
};
my $line = 0;
while (<$fd>) {
s/#.*//;
s/^\s+//;
s/\s+$//;
next if (/^$/);
if (/^Server(Name|Alias)\s+(.*)/) {
foreach my $name (split /\s+/, $2) {
unless (valid_domain_name($name)) {
- print STDERR "$script: $file:$line: $name: invalid domain name\n";
+ print STDERR "$progname: $file:$line: $name: invalid domain name\n";
next;
}
- foreach my $z (@zone) {
+ foreach my $z (keys %{$config{zone}}) {
if ($name =~ /.*\.$z$/) {
if ($name =~ /^\*\.(.+)/ and $1 eq $z) {
- print STDERR "$script: $file:$line: $name: first-level wildcard\n";
+ print STDERR "$progname: $file:$line: $name: first-level wildcard\n";
next;
}
$ret{$name} = $z;
last;
}
}
}
}
}
close($fd)
}
return %ret;
}
sub read_cname_list($) {
my $file = shift;
my %ret;
if (-f $file) {
- open(my $fd, "<", $file) or abend(1, "cannot open $file: $!");
+ open(my $fd, "<", $file) or abend(EX_NOINPUT, "cannot open $file: $!");
while (<$fd>) {
chomp;
s/^\s+//;
s/\s+$//;
s/#.*//;
next if ($_ eq "");
my @a = split / /;
$ret{$a[0]} = $a[1];
}
close($fd);
}
return %ret;
}
sub write_cname_list {
my ($file, %hash) = @_;
return if ($dry_run);
- open(my $fd, ">", $file) or abend(1, "cannot open $file for writing: $!");
+ open(my $fd, ">", $file) or
+ abend(EX_CANTCREAT, "cannot open $file for writing: $!");
foreach my $h (sort keys %hash) {
print $fd "$h $hash{$h}\n";
}
close($fd);
}
sub ns_update {
- my $resolver = shift;
my $name = shift;
my $domain = shift;
my %hash = @_;
my %ignorerr;
- print STDERR "$script: updating $name in $domain: ".
+ my $resolver = get_zone_resolver($domain);
+
+ print STDERR "$progname: updating $name in $domain: ".
join(',', map { "$_ => $hash{$_}" } keys %hash) .
"\n" if ($debug > 1);
return 1 if ($dry_run);
my $update = new Net::DNS::Update($domain);
while (my ($k, $v) = each %hash) {
if ($k eq 'ignore') {
$ignorerr{$v} = 1;
} else {
$update->push($k => $v);
}
}
- $update->sign_tsig(@tsig_args) if ($#tsig_args >= 0);
+ zone_sign_tsig($update, $domain);
my $reply = $resolver->send($update);
if ($reply) {
if ($reply->header->rcode eq 'NOERROR') {
- print STDERR "$script: update successful\n" if ($debug>3);
+ print STDERR "$progname: update successful\n" if ($debug>3);
} elsif ($ignorerr{$reply->header->rcode}) {
- print STDERR "$script: ignoring " . $reply->header->rcode . ': ' .
+ print STDERR "$progname: ignoring " . $reply->header->rcode . ': ' .
join(',', map { "$_ => $hash{$_}" } keys %hash) . "\n"
if ($debug>3);
} else {
err("updating $name failed: ",
join(',', map { "$_ => $hash{$_}" } keys %hash),
': ',
$reply->header->rcode);
- $status = 2;
+ $status = EX_NOTUPDATED;
return 0;
}
} else {
err("updating $name failed: ",
join(',', map { "$_ => $hash{$_}" } keys %hash),
': ',
$resolver->errorstring);
- $status = 2;
+ $status = EX_NOTUPDATED;
return 0;
}
return 1;
}
+sub get_zone_resolver {
+ my $zone = shift;
+ unless (defined($config{zone}{$zone}{resolver})) {
+ my $resolver = new Net::DNS::Resolver;
+ $resolver->nameservers($config{zone}{$zone}{server})
+ if defined $config{zone}{$zone}{server};
+ $config{zone}{$zone}{resolver} = $resolver;
+ }
+ return $config{zone}{$zone}{resolver};
+}
+
+sub zone_sign_tsig {
+ my ($update, $zone) = @_;
+ my @tsig_args;
+
+ my $zcfg = $config{zone}{$zone};
+ if (exists($zcfg->{'ns-key-file'})) {
+ push @tsig_args, split(/\s+/, $zcfg->{'ns-key-file'});
+ } elsif (exists($zcfg->{'ns-key'})) {
+ push @tsig_args, @{$zcfg->{'ns-key'}};
+ }
+ if ($#tsig_args == -1) {
+ if (exists($config{core}{'ns-key-file'})) {
+ push @tsig_args, split(/\s+/, $config{core}{'ns-key-file'});
+ } elsif (exists($config{core}{'ns-key'})) {
+ push @tsig_args, @{$config{core}{'ns-key'}};
+ }
+ }
+ $update->sign_tsig(@tsig_args) if ($#tsig_args >= 0);
+}
+
sub update_cnames_from_hash {
my %hash = @_;
- print STDERR "$script: " . keys(%hash) . " names to update\n"
+ print STDERR "$progname: " . keys(%hash) . " names to update\n"
if ($debug > 2);
- my %oldhash = read_cname_list($cnamelist);
+ my %oldhash = read_cname_list($config{core}{cache});
my @namelist = sort(keys(%hash));
- if (join(",", @namelist) eq join(".", sort(keys(%oldhash)))) {
- print STDERR "$script: nothing to update\n" if ($debug);
+ if (join(",", @namelist) eq join(",", sort(keys(%oldhash)))) {
+ print STDERR "$progname: nothing to update\n" if ($debug);
return;
}
-
- my $resolver = new Net::DNS::Resolver;
- $resolver->nameservers($nameserver) if defined($nameserver);
my $name;
foreach $name (@namelist) {
if ($oldhash{$name}) {
delete $oldhash{$name};
} else {
- ns_update($resolver, $name, $hash{$name},
+ ns_update($name, $hash{$name},
prereq => yxdomain($name),
update => rr_del($name),
ignore => 'NXDOMAIN');
- print STDERR "$script: $name $ttl CNAME $host\n" if ($debug);
+ print STDERR "$progname: $name $config{core}{ttl} CNAME $config{core}{hostname}\n" if ($debug);
delete $hash{$name}
- unless ns_update($resolver, $name, $hash{$name},
- update => rr_add("$name $ttl CNAME $host"));
+ unless ns_update($name, $hash{$name},
+ update => rr_add("$name $config{core}{ttl} CNAME $config{core}{hostname}"));
}
}
foreach $name (keys %oldhash) {
- ns_update($resolver, $name, $oldhash{$name},
+ ns_update($name, $oldhash{$name},
prereq => yxrrset("$name CNAME"),
update => rr_del("$name CNAME"),
ignore => 'NXRRSET');
}
- write_cname_list($cnamelist, %hash);
+ write_cname_list($config{core}{cache}, %hash);
}
sub update_cnames_from_dir($) {
update_cnames_from_hash(get_cnames(shift));
}
sub nscleanup {
- print STDERR "$script: Removing DNS CNAME records\n" if ($debug);
+ print STDERR "$progname: Removing DNS CNAME records\n" if ($debug);
- my $resolver = new Net::DNS::Resolver;
- $resolver->nameservers($nameserver) if defined($nameserver);
-
- my %hash = read_cname_list($cnamelist);
+ my %hash = read_cname_list($config{core}{cache});
foreach my $name (keys %hash) {
- print STDERR "$script: removing $name from $hash{$name}\n"
+ print STDERR "$progname: removing $name from $hash{$name}\n"
if ($debug);
delete $hash{$name}
- if ns_update($resolver, $name, $hash{$name},
+ if ns_update($name, $hash{$name},
prereq => yxrrset("$name CNAME"),
update => rr_del("$name CNAME"),
ignore => 'NXRRSET');
}
- write_cname_list($cnamelist, %hash);
+ write_cname_list($config{core}{cache}, %hash);
+}
+
+###
+sub com_start {
+ abend(EX_USAGE, "too many arguments") unless $#_ == 0;
+ nscleanup();
+ com_reload(@_);
+}
+
+sub com_reload {
+ abend(EX_USAGE, "too many arguments") unless $#_ == 0;
+ my $confdir = -d "$config{core}{'apache-config-directory'}/sites-enabled"
+ ? "$config{core}{'apache-config-directory'}/sites-enabled"
+ : $config{core}{'apache-config-directory'};
+ my %cnames = get_cnames($confdir);
+ update_cnames_from_hash(%cnames);
+ print STDERR "$progname: no cnames defined\n" unless (keys(%cnames) > 0);
}
+sub com_stop {
+ abend(EX_USAGE, "too many arguments") unless $#_ == 0;
+ nscleanup;
+}
+
+sub com_status {
+ err("status command ignored");
+ my %stat;
+
+ my %hash = read_cname_list($config{core}{cache});
+ while (my ($name, $zone) = each %hash) {
+# $name =~ s/.$zone$//;
+ push @{${stat}{$zone}}, $name;
+ }
+
+ foreach my $zone (sort(keys %stat)) {
+ print "Names in zone $zone:\n";
+ foreach my $name (sort(@{$stat{$zone}})) {
+ print " $name\n";
+ }
+ }
+}
###
-($script = $0) =~ s/.*\///;
+($progname = $0) =~ s/.*\///;
+
+my %comtab = (
+ start => \&com_start,
+ restart => \&com_reload,
+ 'force-restart' => \&com_start,
+ reload => \&com_reload,
+ stop => \&com_stop,
+ status => \&com_status
+);
+
+sub getcom {
+ my $com = shift;
+
+ while (defined($comtab{$com}) and ref($comtab{$com}) ne 'CODE') {
+ $com = $comtab{$com};
+ }
+ die "internal error: unresolved command alias" unless defined $com;
+ return $comtab{$com} if defined $comtab{$com};
+
+ my @v = map { /^$com/ ? $_ : () } sort keys %comtab;
+ if ($#v == -1) {
+ abend(EX_USAGE, "unrecognized command");
+ } elsif ($#v > 0) {
+ abend(EX_USAGE, "ambiguous command: ".join(', ', @v));
+ }
+ return getcom($v[0]);
+}
+
## Read configuration
-read_config_file($ENV{'VHOSTCNAME_CONF'} ?
- $ENV{'VHOSTCNAME_CONF'} : $config_file);
+sub parse_ns_key {
+ my ($var, $ref, $loc) = @_;
+ my @result;
+ if ($$ref =~ /(.+?)=(.+)/) {
+ push @result, $1, $2;
+ $$ref = \@result;
+ } else {
+ err("$loc: $var argument must be must be NAME=KEY");
+ return 0;
+ }
+ return 1;
+}
-GetOptions("help" => \$man,
- "h" => \$help,
+sub parse_boolean {
+ my ($var, $ref, $loc) = @_;
+ my %bool = ( yes => 1,
+ no => 0,
+ true => 1,
+ false => 0,
+ t => 1,
+ nil => 0,
+ f => 0,
+ on => 1,
+ off => 0,
+ 1 => 1,
+ 0 => 0);
+
+ my $s = $$ref;
+ $s =~ tr/A-Z/a-z/;
+ if (exists($bool{$s})) {
+ $$ref = $bool{$s};
+ } else {
+ err("$loc: argument must be boolean");
+ return 0;
+ }
+ return 1;
+}
+
+my %kw = (
+ core => {
+ section => {
+ 'apache-config-directory' => 1,
+ 'apache-config-pattern' => 1,
+ 'cache' => 1,
+ 'server' => 1,
+ 'ttl' => 1,
+ 'ns-key' => { parser => \&parse_ns_key },
+ 'ns-key-file' => 1,
+ 'hostname' => 1,
+ 'allow-wildcards' => { parser => \&parse_boolean }
+ },
+ },
+ zone => {
+ section => {
+ 'server' => 1,
+ 'ttl' => 1,
+ 'ns-key' => { parser => \&parse_ns_key },
+ 'ns-key-file' => 1
+ },
+ }
+);
+
+GetOptions("help" => sub {
+ pod2usage(-exitstatus => EX_OK, -verbose => 2);
+ },
+ "h" => sub {
+ pod2usage(-message => "$progname: $progdescr",
+ -exitstatus => EX_OK);
+ },
+ "usage" => sub {
+ pod2usage(-exitstatus => EX_OK, -verbose => 0);
+ },
+
"debug|d+" => \$debug,
"dry-run|n" => \$dry_run,
- "hostname|H=s" => \$host,
- "apache-config-pattern=s" => \$confpat,
- "apache-config-directory=s" => \$confdir,
- "ns-key-file=s" => sub {
- abend(3, "NS key already set") if ($#tsig_args >= 0);
- push @tsig_args, $_[1];
- },
- "ns-key=s" => sub {
- abend(3, "NS key already set") if ($#tsig_args >= 0);
- if ($_[1] =~ /(.+?)=(.+)/) {
- push @tsig_args, $1;
- push @tsig_args, $2;
- } else {
- abend(3, "argument to --ns-key must be NAME=KEY");
- }
- },
- "cname-file=s" => \$cnamelist,
- "zone|z=s@" => \@zone,
- "ttl=i" => \$ttl,
- "server=s" => \$nameserver,
- "allow-wildcard-domains" => \$allow_wildcard_domains
- ) or exit(3);
-
-pod2usage(-message => "$script: update DNS from Apache virtual host configuration",
- -exitstatus => 0) if $help;
-pod2usage(-exitstatus => 0, -verbose => 2) if $man;
-
-unless (defined($confdir)) {
+ "config|c=s" => \$config_file,
+ ) or exit(EX_USAGE);
+
+readconfig($config_file, \%config, kw => \%kw);
+
+unless (defined($config{core}{'apache-config-directory'})) {
foreach my $dir ("/etc/apache2", "/etc/httpd") {
if (-e "$dir/sites-enabled" and -e "$dir/sites-available") {
- $confdir = $dir;
+ $config{core}{'apache-config-directory'} = $dir;
last;
}
if (-e "$dir/vhosts.d") {
- $confdir = "$dir/vhosts.d";
+ $config{core}{'apache-config-directory'} = "$dir/vhosts.d";
last;
}
}
- abend(3,
- "don't know where virtual host configurations are located; use --apache-config-directory option")
- unless defined($confdir);
+ abend(EX_CONFIG,
+ "don't know where virtual host configurations are located; define apache-config-directory")
+ unless defined($config{core}{'apache-config-directory'});
}
-$host = hostname() unless defined($host);
-push(@zone, $host) if ($#zone == -1);
+$config{core}{hostname} = hostname() unless defined($config{core}{hostname});
+$config{zone}{$host} = {} unless exists $config{zone};
+
$debug++ if ($dry_run);
if ($#ARGV == -1) {
- abend(3, "command not given") unless ($ENV{'DIREVENT_FILE'});
- print STDERR "$script: started as direvent handler for " .
+ abend(EX_USAGE, "command not given") unless ($ENV{'DIREVENT_FILE'});
+ print STDERR "$progname: started as direvent handler for " .
"$ENV{'DIREVENT_GENEV_NAME'} on $ENV{'DIREVENT_FILE'}\n"
if ($debug);
my $cwd = getcwd;
my $update_dir;
+ my $confdir = $config{core}{'apache-config-directory'};
if (-d "$confdir/sites-available" && -d "$confdir/sites-enabled") {
if ($cwd eq "$confdir/sites-available") {
- foreach my $file (glob "$confdir/sites-enabled/$confpat") {
+ foreach my $file (glob "$confdir/sites-enabled/$config{core}{'apache-config-pattern'}") {
next unless (-l $file);
if (realpath(readlink($file)) eq
"$confdir/sites-available/$ENV{'DIREVENT_FILE'}") {
$update_dir = "$confdir/sites-enabled";
last;
}
}
} elsif ($cwd eq "$confdir/sites-enabled") {
$update_dir = $cwd;
}
} else {
$update_dir = $cwd;
}
update_cnames_from_dir($update_dir) if defined($update_dir);
-} elsif ($#ARGV != 0) {
- abend(3, "too many arguments");
-} elsif ($ARGV[0] =~ /^start|restart|force-restart|reload$/) {
- nscleanup if ($ARGV[0] =~ /start$/);
- my %cnames = get_cnames(-d "$confdir/sites-enabled" ?
- "$confdir/sites-enabled" : $confdir);
- update_cnames_from_hash(%cnames);
- print STDERR "$script: no cnames defined\n" unless (keys(%cnames) > 0);
-} elsif ($ARGV[0] eq "stop") {
- nscleanup;
-} elsif ($ARGV[0] eq "status") {
- err("status command ignored");
-} else {
- abend(3, "invalid command, try $script --help for more info");
}
+my $command = getcom($ARGV[0]);
+&{$command}(@ARGV);
+
exit($status);
__END__
=head1 NAME
vhostcname - synchronize DNS with Apache virtual host configuration
=head1 SYNOPSIS
B<vhostcname> [OPTIONS] B<COMMAND>
=head1 DESCRIPTION
The program takes a list of DNS zones and scans Apache virtual host
configuration files. For each hostname found in B<ServerName> and
B<ServerAlias> statements, it checks whether this name ends in a
zone from the list, and if so, attempts to register this hostname
using the DNS dynamic updates mechanism (B<RFC 2136>).
A reverse operation is also supported: deregister all host name
registered on the previous run.
The mode of operation is requested by the B<COMMAND> argument.
The available B<COMMAND>s have been chosen so as to allow
@@ -410,196 +714,262 @@ B<vhostcname> to be run as one of the machine's startup
scripts. The exact ways to register it to be run on server startup
and shutdown depend on the operating system and distribution in use.
For example, on Debian-based GNU/Linux:
cd /etc/init.d
ln -sf /usr/bin/vhostcname /etc/init.d
update-rc.d vhostcname defaults
The program can also be ised as a B<direvent>(8) handler. This use
allows for immediate updates of the DNS records upon any modifications
to the Apache configuration files. The following example shows the
corresponding B<direvent.conf>(5) entry:
watcher {
path /etc/apache2/sites-available;
path /etc/apache2/sites-enabled;
event (create,delete,write);
timeout 10;
option (stderr,stdout);
command /usr/bin/vhostcname;
}
=head1 COMMANDS
+Unless the program is started as a B<direvent>(8) handler, exactly one
+command must be given in the command line. A command may be supplied
+in full or abbreviated form. Any unambiguous abbreviation is allowed.
+
+Available commands are:
+
=over 4
=item B<start>
-Scan the apache configuration files and register all server names matching
-the supplied zones.
+Scan the apache configuration files and register all server names that
+match the configured zones.
=item B<stop>
Deregister all hostnames registered previously.
-=item B<restart>, B<force-restart>, B<reload>
+=item B<restart>, B<reload>
-Same as running B<vhostcname stop; vhostcname start>.
+Builds a list of names from the apache configuration (I<apache-list>) and
+compares them with the names registered at the previous run (I<cache>). If
+the two lists differ, the names present in I<apache-list>, but absent in
+I<cache> are registered. The names present in I<cache>, but lacking in
+I<apache-list> are deleted from the DNS.
+
+=item B<force-restart>
+
+Deregister all hostnames registered previously, rescan Apache files, and
+register all names that match the configured zones.
=item B<status>
-Ignored
+Displays registered host names.
=back
=head1 OPTIONS
=over 4
-=item B<--allow-wildcard-domains>
+=item B<-c>, B<--config=>I<FILE>
+
+Read configuration from I<FILE> instead of the default location
+(F</etc/vhostcname.conf>).
+
+=item B<-d>, B<--debug>
-Allow the use of wildcard (B<*>). When this option is in effect, a wildcard
-will be allowed if it is the very first label in a domain name and it is
-separated from the base zone (see the B<--zone> option) by one or more labels.
+Increases the debug level. Multiple B<-d> options are allowed.
-=item B<--apache-config-directory=>I<DIR>
+=item B<-n>, B<--dry-run>,
+
+Enables I<dry-run> mode: print what would have been done without actually
+doing it.
+
+=item B<--help>
+
+Displays B<vhostcname> man page.
+
+=item B<-h>
+
+Displays a short help summary and exits.
+
+=item B<--usage>
+
+Displays a short command line syntax reminder.
+
+=back
+
+=head1 CONFIGURATION FILE
+
+Configuration is read from F</etc/vhostcname.conf> or a file specified
+by the B<--config> (B<-c>) command line option. The file consists of
+a number of variable assignments (I<variable> B<=> I<value>), grouped into
+sections. Whitespace is ignored, except that it serves to separate input
+tokens. I<value> is read verbatim, including eventual whitespace characters
+that can appear within it.
+
+A section begins with the line containing its name within square brackets
+(e.g. B<[core]>). The name can be followed by one or more arguments, if
+the section semantics requires so (e.g. B<[zone example.com]>).
+
+The following sections are recognized:
+
+=over 4
+
+=item B<[core]>
+
+=over 8
+
+=item B<apache-config-directory => I<DIR>
Sets the Apache configuration directory. I<DIR> should be either a directory
where virtual configuration file are located or a directory which hosts the
B<sites-available> and B<sites-enabled> directories. In the latter case,
B<vhostcname> will look for files matching B<apache-config-pattern> in
I<DIR>B</sites-enabled>.
If this option is not given, B<vhostcname> will try to deduce where the
configuration files are located. It will issue a warning message and
terminate if unable to do that.
-=item B<--apache-config-pattern=>I<GLOB>
+=item B<apache-config-pattern => I<PATTERN>
Shell globbing pattern for virtual host configuration files. By default,
B<*> is used, meaning that B<vhostcname> will scan all files in the
-configuration directory.
+configuration directory (note: that includes backup copies too!).
-=item B<--cname-file=>I<NAME>
+=item B<cache => I<FILE>
-Name of the file where B<vhostcname> will keep successfully registered
+Name of the cache file where B<vhostcname> keeps successfully registered
host names. Default is B</var/run/vhostcname.cache>.
-
-=item B<-d>, B<--debug>
-
-Increases the debug level. Multiple B<-d> options are allowed.
-
-=item B<-n>, B<--dry-run>,
-
-Enables I<dry-run> mode: print what would have been done without actually
-doing it.
-
-=item B<--help>
-
-Displays B<vhostcname> man page.
-
-=item B<-h>
-Displays a short help summary and exits.
-
-=item B<-H>, B<--hostname>=I<NAME>
+=item B<hostname => I<HOSTNAME>
Sets the hostname. Use this if B<vhostcname> is unable to correctly
determine it.
-=item B<--ns-key=>I<NAME>=I<KEY>
+=item B<allow-wildcards => I<BOOL>
-Define the TSIG key.
-
-=item B<--ns-key-file=>I<KEYFILE>
+Allow the use of wildcard (B<*>) in host names. When this option is in
+effect, a wildcard will be allowed if it is the very first label in a domain
+name and it is separated from the base zone (see the B<zone> section) by one
+more labels.
-Name of the key file. The argument should be the name of a file
-generated by the B<dnssec-keygen> utility. Either B<.key> or B<.private>
-file can be used.
+I<BOOL> is one of B<yes>, B<true>, B<t>, B<on>, or B<1> to allow wildcards,
+or one of B<no>, B<false>, B<f>, B<nil>, B<off>, B<0> to disallow them (the
+default).
-This option cannot be used together with B<--ns-key>.
+=back
-=item B<--server=>I<NAME>
+The following variables provide defaults for zones that lack the
+corresponding settings:
+
+=over 8
+
+=item B<server => I<HOST>
Name of the DNS server to use. Normally B<vhostcname> determines what server
to use based on the B<SOA> record of the zone to be updated, so this option
-is rarely needed.
+is rarely needed.
-=item B<--ttl=>I<TIME>
+=item B<ttl => I<SECONDS>
-TTL value for new DNS records. Default is 3600.
+TTL value for new DNS records. Default is 3600.
-=item B<--zone=>I<NAME>
+=item B<ns-key => I<NAME>=I<HASH>
-Name of the zone which B<vhostcname> can update. Multiple B<--zone> options
-can be given.
+Defines the TSIG key.
-If no B<--zone> option is given, B<vhostcname> will take hostname as the
-name of the zone.
+=item B<ns-key-file => I<FILE>
+Name of the key file. The argument should be the name of a file
+generated by the B<dnssec-keygen> utility. Either B<.key> or B<.private>
+file can be used.
+
+If both <ns-key> and B<ns-key-file> are used, the latter is given preference.
+
=back
-=head1 CONFIGURATION FILE
+=item B<[zone I<NAME>]>
-If the file B<etc/vhostcname.conf> exists, the program will read its
-configuration from it. A familiar UNIX configuration format is used.
-Empty lines and UNIX comments are ignored. Each non-empty line is either an
-option name, or option assignment, i.e. B<opt>=B<val>, with any amount of
-optional whitespace around the equals sign. Valid option names are
-the same as the long command line options, but without the leading B<-->.
-For example:
+The B<zone> section informs B<vhostcname> that it should handle names
+in zone I<NAME>. Any number of B<[zone]> sections can be defined. If
+none is defined, B<vhostcname> will take hostname as the name of the zone
+to update.
- zone = vhost.example.com
- ns-key-file = /etc/bind/Kvhost+157+43558.key
- ttl = 3600
+The variables in a B<zone> section define parameters to be used for that
+particular zone. As such, none of them is mandatory. If the zone I<NAME>
+uses default settings (or settings, defined in the B<[core]> section),
+the section can be empty.
+
+=over 8
+
+=item B<server => I<HOST>
-=head1 ENVIRONMENT
+Name of the DNS server to use when updating this zone.
+
+=item B<ttl => I<SECONDS>
-=over 4
+TTL for records in this zone.
+
+=item B<ns-key => I<NAME>=I<HASH>
-=item B<VHOSTCNAME_CONF>
+TSIG key.
+
+=item B<ns-key-file => I<FILE>
-The name of the configuration file to use instead of the default
-F</etc/vhostcname.conf>.
+Name of the key file. The argument should be the name of a file
+generated by the B<dnssec-keygen> utility. Either B<.key> o