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

use Net::SMTP;
use strict;
use Net::DNS::SEC::Tools::conf;
use Net::DNS::SEC::Tools::QWPrimitives;


my $donuts = "donuts";				# Name for donuts script.

#
# If we're running packed, we'll use our local copy of donuts.
#
if (runpacked()) {
    $donuts = "$ENV{'PAR_TEMP'}/inc/donuts";
}

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

DTGetOptions(\%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'],
                ['O|run-count=i',           'Number of times to run before exiting'],
                '',
                ['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;
}

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

my $loopcount = 0;

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");

    # check for limited looping
    if (exists $opts{O}) {
      if ($loopcount >= $opts{O}) { $opts{o} = 1; }
      else                        { $loopcount++; }
    }
    # 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) = @_;

    if ($contact eq '') {
      verbose("  Warning: invalid mail address: mail can not be sent");
      return 1;
    }

    # set up the SMTP object and required data
    my $message = Net::SMTP->new($opts{s});
    if(! $message) {
	print STDERR "unable to connect to mail host \"$opts{s}\"; not sending donuts results\n";
	return;
    }

    $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 \"$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(@_);
}

=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

Specifies command line arguments to be passed to B<donuts> executions.

=item -z TIME

Sleeps TIME seconds between calls to B<donuts>.  The DNSSEC-Tools
B<timetrans> program can be used to convert from large time units (e.g.,
weeks and days) to seconds.

=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>--features 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 "--features live --level 8" -f root@example.com \
     db.example.com example.com admin@example.com

=head1 COPYRIGHT

Copyright 2005-2013 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)>

B<timetrans(1)>

http://dnssec-tools.sourceforge.net

=cut
