#!/usr/bin/perl
# Copyright 2004-2008 SPARTA, Inc.  All rights reserved.
# See the COPYING file included with the DNSSEC-Tools package for details.

use Net::SMTP;
use strict;

my %opts = (z => 60*60*24,
	    t => '/tmp/donutsd',
	    f => $ENV{'USER'} || $ENV{'LOGNAME'},
	    s => 'localhost');

LocalGetOptions(\%opts,
		['a|donuts-arguments=s', 'Arguments to pass to donuts'],
		['z|time-between-checks=s', 'Time between checks'],
		['t|temporary-directory=s', 'Temporary directory to store results in'],
                ['i|input-list=s',           'File containing file/zonename/zonecontact pairings'],
                ['o|run-once',               'Run once and exit'],
                '',
                ['GUI:separator',   'E-Mail options:'],
		['s|smtp-server=s', 'SMTP server to mail mail through (localhost)'],
                ['f|from-address=s','From address to use when sending mail'],
                ['x|include-diff-output','Include diff output in mail'],
                ['e|email-summary-to=s', 'Email a result summary to ADDRESS'],
                '',
		['v|verbose','Turn on verbose output'],
	       ) || exit();

my %zoneinfo;

if ($opts{'i'}) {
    if (! -f $opts{'i'}) {
	print STDERR "Can't find or read the file: $opts{i}\n";
	exit 1;
    }

    if ($opts{i} =~ /.xml$/i) {
	# xml configuration
	require XML::Smart;
	my $doc = XML::Smart->new($opts{i});
	my $nodes = $doc->{'donutsd'}[0]{'zones'}[0]{'zone'};
	foreach my $zone (@$nodes) {
	    push @ARGV, $zone->{'file'}, $zone->{'name'}, $zone->{'contact'};
	    if ($zone->{'donutsargs'}) {
		$zoneinfo{$zone->{'name'}}{'donutsargs'} =
		  $zone->{'donutsargs'};
	    }
	}

	# parse command line arguments as well:
	my $configs = $doc->{'donutsd'}[0]{'configs'}[0]{'config'};
	foreach my $config (@$configs) {
	    if (!$opts{$config->{'flag'}}) {
		$opts{$config->{'flag'}} = $config->{'value'};
	    }
	}
    } else {
	# simple text file
	open(I, $opts{'i'});
	while (<I>) {
	    next if (/^\s*$/);
	    next if (/^\s*#/);
	    push @ARGV, split();
	}
	close(I);
    }
}

if ($#ARGV == -1) {
    print STDERR
      "You must specify at least one ZONEFILE ZONENAME ZONECONTACT set\n";
    exit 1;
}

if (($#ARGV+1)%3 != 0) {
    print STDERR
      "Arguments must be passed in triples of: ZONEFILE ZONENAME ZONECONTACT\n";
    exit 1;
}

# creat temporary directory
if (! -d $opts{'t'}) {
    mkdir($opts{'t'});
}

while (!$opts{o}) {
    my @args = @ARGV;
    while ($#args > -1) {
	my $file = shift @args;
	my $zonename = shift @args;
	my $contactaddr = shift @args;

	my $mailit;
	
	verbose("running donuts on $file/$zonename");
	System("donuts $opts{a} $zoneinfo{$zonename}{'donutsargs'} $file $zonename > $opts{t}/$zonename.new 2>&1");
	if (-f "$opts{t}/$zonename") {
	    verbose("  comparing results from last run");
	    system("diff -u $opts{t}/$zonename $opts{t}/$zonename.new > $opts{t}/$zonename.diff 2>&1");
	    $mailit = $?;
	} else {
	    verbose("  there was no data from a previous run");
	    $mailit = 1;
	}
	if ($mailit) {
	    verbose("  output changed; mailing $contactaddr about $file");
	    mailcontact($contactaddr, "$opts{t}/$zonename.new", $zonename);
	}
	if (-f "$opts{t}/$zonename") {
	    unlink("$opts{t}/$zonename");
	}

	# extract last error line
	if ($opts{a} =~ /(-v|--verbose)/ || 
	    $zoneinfo{$zonename}{'donutsargs'} =~ /(-v|--verbose)/) {
	    System("tail -6 $opts{t}/$zonename.new >> $opts{t}/donuts.summary.new");
	} else {
	    System("tail -1 $opts{t}/$zonename.new >> $opts{t}/donuts.summary.new");
	}

	verbose("  $opts{t}/$zonename.new => $opts{t}/$zonename");
	rename("$opts{t}/$zonename.new", "$opts{t}/$zonename");
    }

    # mail the global administrator a summary
    if ($opts{'e'}) {
	system("diff -u $opts{t}/donuts.summary $opts{t}/donuts.summary.new > $opts{t}/donuts.summary.diff 2>&1");
	if ($?) {
	    mailcontact($opts{e}, "$opts{t}/donuts.summary.new", "all-zones");
	}
    }
    verbose("  $opts{t}/donuts.summary.new => $opts{t}/donuts.summary");
    rename("$opts{t}/donuts.summary.new", "$opts{t}/donuts.summary");

    # sleep and start again in a while
    verbose("sleeping for $opts{z}\n");
    sleep($opts{z});
}

######################################################################
# mailcontact()
#  - emails a contact address with the donuts error output
sub mailcontact {
    my ($contact, $file, $zonename) = @_;

    # set up the SMTP object and required data
    my $message = Net::SMTP->new($opts{s});
    $message->mail($opts{f});
    $message->to(split(/,\s*/,$contact));
    $message->data();

    # create headers
    $message->datasend("To: " . $contact . "\n");
    $message->datasend("From: " . $opts{'f'} . "\n");
    $message->datasend("Subject: donuts output for zone: $zonename\n\n");

    # create the body of the message: the warning
    $message->datasend("The donuts dns zone-file syntax checker was run on the \"$zonename\"\n");
    $message->datasend("and there were resulting errors or errors that have changed since the last run.\n");
    $message->datasend("The results of this run of donuts can be found below:\n\n");
    $message->datasend("You will not receive another message until the output from donuts has changed.\n\n");

    # create the body of the message: the donuts results
    # Include the file
    $message->datasend(("-" x 70) . "\n\n");
    open(IF,"$file");
    while (<IF>) {
        $message->datasend($_);
    }
    close(IF);

    # create the body of the message: the donuts results
    # Include the file
    if ($opts{'x'}) {
        $message->datasend("\n" . ("-" x 70) . "\n\n");
        $file =~ s/.new$/.diff/;
        open(IF,"$file");
        while (<IF>) {
            $message->datasend($_);
        }
        close(IF);
    }

    # finish and send the message
    $message->dataend();
    $message->quit;
}


######################################################################
# verbose() routine
#  - passes arguments to print iff $opts{v} is defined (ie, -v was specified)
#
sub verbose {
    if ($opts{'v'}) {
	print STDERR @_;
	if ($_[$#_] !~ /\n$/) {
	    print STDERR "\n";
	}
    }
}

######################################################################
# System()
#  - calls system but first calls verbose() on the args
sub System {
    verbose("  running: ", @_);
    system(@_);
}

######################################################################
# argument parsing
#
sub LocalGetOptions {
    if (eval {require Getopt::GUI::Long;}) {
	import Getopt::GUI::Long;
	Getopt::GUI::Long::Configure(qw(display_help no_ignore_case));
	return GetOptions(@_);
    }
    require Getopt::Long;
    import Getopt::Long;
    Getopt::Long::Configure(qw(auto_help no_ignore_case));
    GetOptions(LocalOptionsMap(@_));
}

sub LocalOptionsMap {
    my ($st, $cb, @opts) = ((ref($_[0]) eq 'HASH') 
			    ? (1, 1, $_[0]) : (0, 2));
    for (my $i = $st; $i <= $#_; $i += $cb) {
	if ($_[$i]) {
	    next if (ref($_[$i]) eq 'ARRAY' && $_[$i][0] =~ /^GUI:/);
	    push @opts, ((ref($_[$i]) eq 'ARRAY') ? $_[$i][0] : $_[$i]);
	    push @opts, $_[$i+1] if ($cb == 2);
	}
    }
    return @opts;
}

=pod

=head1 NAME

B<donutsd> - Run the B<donuts> syntax checker periodically and report the
results to an administrator

=head1 SYNOPSIS

  donutsd [-z FREQ] [-t TMPDIR] [-f FROM] [-s SMTPSERVER] [-a DONUTSARGS]
          [-x] [-v] [-i zonelistfile] [ZONEFILE ZONENAME ZONECONTACT]

=head1 DESCRIPTION

B<donutsd> runs B<donuts> on a set of zone files every so often (the
frequency is specified by the I<-z> flag which defaults to 24 hours) and
watches for changes in the results.  These changes may be due to the
time-sensitive nature of DNSSEC-related records (e.g., RRSIG validity
periods) or because parent/child relationships have changed.  If any
changes have occurred in the output since the last run of B<donuts> on a
particular zone file, the results are emailed to the specified zone
administrator's email address.

=head1 OPTIONS

=over

=item -v

Turns on more verbose output.

=item -o

Run once and quit, as opposed to sleeping or re-running forever.

=item -a ARGUMENTS

Passes arguments to command line arguments of B<donuts> runs.

=item -z TIME

Sleeps TIME seconds between calls to B<donuts>.

=item -e ADDRESS

Mail ADDRESS with a summary of the results from all the files.
These are the last few lines of the B<donuts> output for each zone that
details the number of errors found.

=item -s SMTPSERVER

When sending mail, send it to the SMTPSERVER specified.  The default
is I<localhost>.

=item -f FROMADDR

When sending mail, use FROMADDR for the From: address.

=item -x

Send the I<diff> output in the email message as well as the B<donuts> output.

=item -t TMPDIR

Store temporary files in TMPDIR.

=item -i INPUTZONES

See the next section details.

=back

=head1 ZONE ARGUMENTS

The rest of the arguments to B<donutsd> should be triplets of the
following information:

=over

=item ZONEFILE

The zone file to examine.

=item ZONENAME

The zonename that file is supposed to be defining.

=item ZONECONTACT

An email address of the zone administrator (or a comma-separated
list of addresses.)  The results will be sent to this email address.

=back

Additionally, instead of listing all the zones you wish to monitor on
the command line, you can use the I<-i> flag which specifies a
file to be read listing the TRIPLES instead.  Each line in this file
should contain one triple with white-space separating the arguments.

Example:

   db.zonefile1.com   zone1.com   admin@zone1.com
   db.zonefile2.com   zone2.com   admin@zone2.com,admin2@zone2.com

For even more control, you can specify an XML file (whose name must end in
B<.xml>) that describes the same information.  This also allows for per-zone
customization of the B<donuts> arguments.  The B<XML::Smart> Perl module must
be installed in order to use this feature.

 <donutsd>
   <zones>
    <zone>
      <file>db.example.com</file>
      <name>example.com</name>
      <contact>admin@example.com</contact>
      <!-- this is not a signed zone therefore we'll
           add these args so we don't display DNSSEC errors -->
      <donutsargs>-i DNSSEC</donutsargs>
    </zone>
   </zones>
 </donutsd>

The B<donutsd> tree may also contain a I<configs> section where
command-line flags can be specified:

 <donutsd>
  <configs>
   <config><flag>a</flag><value>--live --level 8</value></config>
   <config><flag>e</flag><value>wes@example.com</value></config>
  </configs>
  <zones>
   ...
  </zones>
 </donutsd>

Real command line flags will be used in preference to those specified
in the B<.xml> file, however.

=head1 EXAMPLE

  donutsd -a "--live --level 8" -f root@somewhere.com \
     db.example.com example.com admin@example.com

=head1 COPYRIGHT

Copyright 2005-2008 SPARTA, Inc.  All rights reserved.
See the COPYING file included with the DNSSEC-Tools package for details.

=head1 AUTHOR

Wes Hardaker <hardaker@users.sourceforge.net>

=head1 SEE ALSO

B<donuts(8)>

http://dnssec-tools.sourceforge.net

=cut
