# -*-perl-*-

# $Id: Cricket.plm,v 1.2 2006/10/11 15:55:23 thomas Exp $

# $Log: Cricket.plm,v $
# Revision 1.2  2006/10/11 15:55:23  thomas
# Allow multiple (virtual) hosts to be monitored in one configuration
#
# Revision 1.1  2005/02/08 20:10:14  thomas
# Generate cricket config files
#


=head1 NAME

B<Cricket.pm> - Interface to Cricket configurations

=head1 DESCRIPTION

This module controls the creation of cricket configuration files from
B<what> host configurations.

Cricket B<hosts> files (that contain lists of hosts to be monitored)
are maintained by this module, using aggregated information from across all hosts known to B<What>.

Cricket B<Defaults> files are ignored by this module.  They are
maintained by other means.

=head1 XML FORMAT

The data for the cricket configuration is rooted in the B<what>
configuration tree at B</Data/Monitor/Cricket>.  Attributes of this node
specify the root of the cricket tree and other details.  See the
B<EXAMPLES> section for details.

Nodes below B</Data/Monitor/Cricket> correspond to a path in the file
system into the cricket configuration tree.  Any node with a
B<collect> attribute will cause the hosts file in the corresponding
directory to contain an entry for the specified host.  Other
attributes of the node will also be placed in the cricket hosts file,
in the cricket B<I<name> = I<value>> format.  Special attributes
(interpreted by this module instead of passed on to the cricket
B<hosts> file are:

=over 4

=item collect

This boolean must be present and non-zero ('yes', '1', and 'true' are
all acceptable values) for this module to manage the cricket hosts
file at the specified path.  If this attribute is present and zero
('no', '0', 'false'), the attribute will be included in the hosts file
as B<collect=false>.

=item disable_missing

If this attribute is present in the what configuration, any hosts
found in the current B<hosts> file, but missing from the what
configuration, will be given a B<collect = false> attribute in the
B<hosts> file.

=item cricket_hostsfile

This attribute gives the name of the file instead of B<hosts>.

=item cricket_target_name

This attribute gives the name for the target in the cricket hosts
file.  The default is B<${target}>.

=back

=head1 EXAMPLES

=over 4

=item Global

Somewhere, the default configuration for each host contains
information about the root of the cricket configuration:

  <node path="/Data/Monitor/Cricket"
        config_root="/s/cricket-config/data"
  />

=item Service

The following XML will cause the file
B</s/cricket-config/data/ntp-stats/ntp_servers/hosts> to be controlled
by the cricket data configurator.  Because B<disable_missing> is
specified, any servers found in the hosts file that don't have
configuration for this service will be left in the file, but disabled
via a B<collect=false> attribute.

 <what>

  <include name="/s/cricket/what/cricket-global" />

  Add the ntp specific bits
  <replace path="/Data/Monitor/Cricket/${/Data/Monitor/Cricket/config_root}/ntp-stats/ntp_servers"
          collect="yes"
          disable_missing="yes"
   />

 </what>


=back

=head1 SEE ALSO

B<cricket:1>

B<What::AllHosts::Data.3>

B<What::Data.3>

=cut

use strict;

package What::Cricket;
use base qw(What);

use Text::ParseWords;
use Net::hostent;
use What::Data;

sub generate_cricket {
  my $ref = shift;
  my $what = shift; #not used

  my $self = bless {} => (ref($ref) || $ref);

  $self->{data} = new What::Data;
  if (!defined $self->{data}) {
    $what->err ("Failed to access monitoring data. (this is Bad (tm)).\n");
    return 0;
  }

  # Build the monitoring data structure
  $self->debug (2, "Reading host configurations...\n");
  $self->{files} = {};
  my ($host, $d, $data, $vhost);
  foreach $host (sort @{$self->{data}->find()}) {
    $what->debug (3, 'Reading configuration for %s%s', $host, "\n");
    $d = $self->{data}->find ($host);
    $data = $d->{Data}->{Monitor}->{Cricket};
    next unless defined ($data);

    # Last one wins!
    # foreach ('config_root', 'data_root', 'log_root') {
    #   $self->{meta}->{$_} = $data->{$_} if (defined $data->{$_});
    # }

    foreach $vhost (sort keys %$data) {
      # This is _not_ a host!
      next if ($vhost eq 'config_root');
      $self->walk_cricket ($vhost, $data->{$vhost}, '');
    }
  }

  $self->update_cricket ();

}

###############################################################################
# If this node has a collect attr, process it.
# If this node has kids, recurse it.

sub walk_cricket {
  my $self = shift;
  my $host = shift;
  my $data = shift;
  my $filename = shift;

  # Set a data node if we're supposed to and/or walk the tree
  foreach (keys %$data) {
    if (ref $data->{$_}) {
      $self->walk_cricket ($host, $data->{$_}, "$filename/$_");
    } else {
      if (defined $data->{collect}) {
	# if (defined $data->{cricket_target_name}) {
	#  $self->{files}->{$filename}->{$data->{cricket_target_name}}->{$_} = $data->{$_};
	# } else {
	$self->{files}->{$filename}->{$host}->{$_} = $data->{$_};
	# }
      }
    }
  }
}


###############################################################################
# Here's the payout.  Update a bunch of cricket config files

sub update_cricket {
  my $self = shift;

  # my $root = $cricket->{meta}->{config_root};
  my ($path, $targets, $new);
  foreach $path (keys %{$self->{files}}) {
    if (!-d $path) {
      print STDERR "$path requested but does not exist.\n";
      print STDERR "Please create and try again.\n";
      next;
    }
    $new = $self->{files}->{$path};

    # Read old config
    my $config = $self->read_config ("$path/hosts");

    $self->update_config ($config, $new);

    # Write out new config
    $self->write_config ("$path/hosts", $config);
  }
}


###############################################################################

# Return an hashref of targets in the old config
# Each target is a hashref of attributes.

sub read_config {
  my $self = shift;
  my $fname = shift;

  my $data = {};
  my $fh = new FileHandle $fname, 'r';
  my $h;
  if (!defined $fh) {
    print STDERR "Failed to read existing config file $fname.\n";
    return $data;
  }

  # Read only 'target <foo>' sections with attributes
  my ($tname, $target, @w);
  while (<$fh>) {
    chomp;
    @w = shellwords ($_);
    if (!@w || ($w[0] eq '#')) {
      # Empty line or Comment
      next;
    } elsif ($w[0] =~ /target/i) {
      # New target -- maybe finalize the old target
      $data->{$tname} = $target if (defined $tname);
      ($target, $tname) = ({}, $w[1]);
      if (!defined ($h = gethostbyname($w[1]))) {
	# Can't collect if it doesn't exist
	$target->{collect}='false';
      }
    } elsif ($w[1] eq '=') {
      $target->{$w[0]} = $w[2];
    }
  }
  # Maybe finalize last target
  $data->{$tname} = $target if (defined $tname);
  return $data;
}

###############################################################################
# Update $config based on the information in $new
# both configs are target hashes of attribute hashes

sub update_config {
  my $self = shift;
  my $config = shift;
  my $new = shift;

  my ($targets, $target);
  my $disable_missing = 0;

  # See if any targets have disable_missing defined
  foreach (keys %$new) {
    if (defined $new->{$_}->{disable_missing} &&
	($new->{$_}->{disable_missing} =~ /^\s*(1|true|yes)\s*$/i)) {
      $disable_missing = 1;
    }
  }

  # Get the one true list of all targets
  foreach (keys %$config, keys %$new) { $targets->{$_} = $1; }

  # Update config
  foreach $target (keys %$targets) {
    # Replace old with new
    if (defined $new->{$target}) {
      $config->{$target} = $new->{$target};
    } elsif ($disable_missing) {
      $config->{$target}->{collect} = 'false';
    }
  }
  return 1;
}


###############################################################################

sub write_config {
  my $self = shift;
  my $fname = shift;
  my $config = shift;

  my ($target, $attr);
  my $content = <<__Eof__
# Cricket configuration data updated by What::Cricket
# Hand-made changes will be lost!

__Eof__
  ;

  foreach $target (sort keys %$config) {
    $content .= "target $target\n";
    foreach $attr (sort keys %{$config->{$target}}) {
      next if ($attr =~ /^(disable_missing|cricket_target_name)$/i);
      # Don't include all the 'collect=true' attributes
      next if ($attr =~ /collect/i
	       && $config->{$target}->{$attr} =~ /yes/i);
      $content .= "  $attr = $config->{$target}->{$attr}\n";
    }
  }

  # Update and do the revision control
  my $res = $self->check_and_replace_content_revision_control
    ($fname, $content, 0, 0, undef);
  return $res;
}

###############################################################################

1;
