#!/usr/bin/perl

# FILE: populate-hostgroup.pl
# SYNOPSIS: Create a usable nagios config for a hostgroup, taking input as a list of hosts (file)
#           or a range (sequence).  Uses input files for host/group templates, and writes out a working
#           config with HOSTNAME, ALIAS, ADDRESS, and PARENTS.
# GUILTY_PARTY: Eli Stair
# LICENSE: GPL, Copyright 2006 Eli Stair <eli.stair {at} gmail {dot} com>

# TODO: allow specifying FQDN hosts in input file, using split() to separate host & domain (DONE, verify all conditions)
# TODO: allow specifying either hostname _OR_ IP for hostname input (DONE)
# TODO: allow specifying domain a host belongs to (avoid searching) (DONE)

# TODO: update code to build configs directly from Ganglia XML
# TODO: make template sourcing more robust and intuitive (I forget how it works...)
# TODO: make L3 parent switch detection more robust
# TODO: more features...


use Getopt::Long;
use Pod::Usage;
use Net::DNS;
use Net::SNMP;
use Net::Traceroute;


##################################################
### Define ARGV/runtime options:
##################################################
GetOptions(
  "h|help"		=>  \$help,
  "f|file=s"		=>  \$hostlist_file,
  "n|number=i"		=>  \$hostlist_file_fieldnum,
  "s|startrange=i"	=>  \$start,
  "e|endrange=i"	=>  \$stop,
  "g|hostgroup=s"	=>  \$hostgroup,
  "d|domain=s"		=>  \@domain,
  "t|template=s"	=>  \$template,
  "m|mode=s"		=>  \$mode,
  "c|cstring=s"		=>  \$cstring,
  "debug"		=>  \$debug,
  "inherit"		=>  \$inherit,
);

my $res = Net::DNS::Resolver->new(
  nameservers => \@server,
  recurse     => 0,
  debug       => 0,
);

my $tr = Net::Traceroute->new(
  host		=> undef,
  debug		=> 0,
  queries	=> 2,
  query_timeout	=> 2,
  timeout	=> 5,
  max_ttl	=> 10,
  use_icmp	=> 1
);


##################################################
### Define program flow:

# check cmdline input:
&processargs;


if ( $mode eq file ) {
  #running on file as input list:
  &create_hostlist_file;
  &write_new_config;
  exit 0;
} elsif ( $mode eq "seq" ) {
  #sequence-generated config
  &create_hostlist_seq;
  &write_new_config;
  exit 0;
} else {
  die "Using invalid mode ( -m )! \n";
}

##################################################
### FUNCTIONS:

### FUNC: ProcessArgs
sub processargs {

  if ($debug) {
    print "IN DEBUG! \n";
  }

  if ($man or $help) {
    pod2usage(-verbose => 2,-noperldoc => 1,-exitval => 3);
    pod2usage(1);
    exit 1;
  }
  
  unless (defined ($mode)) {
    pod2usage("$0: Not enough arguments.\n");
  }
  if ( $mode eq "file" ) {
    unless (defined ($hostlist_file)) {
      pod2usage("$0: FILE mode missing input file.\n");
    }
  }
  if ( $mode eq "seq") {
    unless (defined ( $start && $stop && $hostgroup )) {
      pod2usage("$0: SEQ mode needs a START, STOP, and HOSTGROUP parameter.\n");
    }
  }
  unless (defined (@domain)) {
    pod2usage("$0: DOMAIN must be specified.\n");
  }
  unless (defined ($template)) {
    pod2usage("$0: TEMPLATE file must be specified.\n");
  }
  unless (defined ($hostlist_file_fieldnum)) {
    $hostlist_file_fieldnum = 0;
  }

}
### /FUNC: ProcessArgs

### FUNC: write_new_config  
sub write_new_config {
# File operations:

# create backup of config:
if (-e $hostgroup.cfg.new) {
  open (NEWCONFIG, "<", "$hostgroup.cfg.new") || die "Can't open CFGFILE ( \"$hostgroup.cfg.new\" )! $! ";
  open (OLDCONFIG, ">", "$hostgroup.cfg.old") || die "Can't open CFGFILE ( \"$hostgroup.cfg.new\" )! $! ";
  @OLDCONFIG = <NEWCONFIG>;
  print OLDCONFIG (@OLDCONFIG);
  close OLDCONFIG;
  close NEWCONFIG;
  open (NEWCONFIG, ">", "$hostgroup.cfg.new") || die "Can't open CFGFILE ( \"$hostgroup.cfg.new\" )! $! ";
  print NEWCONFIG "";
  close NEWCONFIG;
}

# Open our new config file for appending to:
open (NEWCONFIG, ">>", "$hostgroup.cfg.new") || die "Can't open CFGFILE! $! ";

# Open & read header template:
# // changing from hostgroup-based to template-based file sourcing:
#open (HEADER, "<", "$hostgroup-cfg.header") || die "Can't open HEADER ( \"$hostgroup-cfg.header\" )! $! ";
open (HEADER, "<", "$template-cfg.header") || die "Can't open HEADER ( \"$template-cfg.header\" )! $! ";
@HEADER = <HEADER>;
close HEADER;

# iterate over & write out config (hostgroup) header:
foreach (@HEADER) {
  # set hostgroup as defined at runtime:
  $_ =~ s/HOSTGROUP/$hostgroup/g;
  # set hostlist to calculated list:
#  unless ($_ =~ /MEMBERS/) {
    #print NEWCONFIG $_;
#    print $_;
#  } else {
    $_ =~ s/MEMBERS/$hoststring/g;
    #print NEWCONFIG $_;
    print $_;
#  } #/unless 
} #/foreach @header

# open host-config (body) template
# // changing from hostgroup-based to template-based file sourcing:
#open (TEMPLATE, "<", "$hostgroup-cfg.template") || die "Can't open TEMPLATE ( \"$hostgroup-cfg.template\" )! $! ";
open (TEMPLATE, "<", "$template-cfg.template") || die "Can't open TEMPLATE ( \"$template-cfg.template\" )! $! ";
@TEMPLATE = <TEMPLATE>;
close TEMPLATE;

# iterate over & write out per-host config:
foreach my $current_host (sort(keys %{hostlist})) { # outer loop, iterate over each host
  foreach my $line (@TEMPLATE) { # for each host in hostlist, iterate over template
    $writeline = $line;
    # handle host alias (FQDN with our provided zone):
    if ( $writeline =~ /alias/ ) {
      #$writeline =~ s/HOST/$current_host.$domain/g;
      #$writeline =~ s/HOST/${current_host}$hostlist{$current_host}{domain}/g;
      $writeline =~ s/HOST/$hostlist{$current_host}{fqdn}/g;      # $hostlist{$hostname}{fqdn}
    } #/if for alias
    $writeline =~ s/HOST/$current_host/g;
    $writeline =~ s/IP/$hostlist{$current_host}{ip}/g;
    $writeline =~ s/PARENTS/$hostlist{$current_host}{router}/g;
    print $writeline;
    #print NEWCONFIG $writeline;
  } #/foreach TEMPLATE
} #/foreach @hostlist


# all done (program ends now), close output file:
close NEWCONFIG;
} #/sub write_new_config
### /FUNC: write_new_config  


### FUNC: query_dns
sub query_dns {
  my $fqdn = shift;
  print "## IN query_dns, FQDN=($fqdn) \n" if ($debug);
  $query = $res->send("$fqdn");
  $query_returncode = ($query->{header}->{rcode});
  $query_count = ($query->{header}->{ancount});
    foreach my $rr ($query->answer) {
      next unless $rr->type eq "A";
      my $ip =  $rr->address;
      print "// EXITING query_dns, got IP=($ip) \n" if ($debug);
      return $ip;
    }
} #/sub query_dns
### /FUNC: query_dns


### FUNC: query_router
sub query_router {
  my $ip = shift;
  print "## IN query_router, IP=($ip) \n" if ($debug);
  $query_tr = $tr->clone(host => "$ip");
  if($query_tr->found) {
    # use object ->stat to check result (if success) of the query.
    my $hops = $query_tr->hops;
    if($hops > 1) {
      $router = $query_tr->hop_query_host($query_tr->hops - 1, 0);
    } #/if >1 hop
    # get SNMP sysName from router, to be used as PARENT:
    my $sysName = '.1.3.6.1.2.1.1.5.0';
    ($session, $error) = Net::SNMP->session(
       -hostname  => "$router",
       -community => "$cstring",
       -port      => 161,
       -timeout   => 1
    );
    my $router_sysname = $session->get_request(
      Varbindlist => [$sysName]
    );
    print "// performing SNMP GET of sysName from $router \n" if ($debug);
    my $router = $router_sysname->{$sysName};
    print "// EXITING query_router, got ROUTER=($router) \n" if ($debug);
    return $router;
  } else {
    $router = "UNKNOWN";
    print "// EXITING query_router WITHOUT FINDING ROUTER, set default ROUTER=($router) \n" if ($debug);
    return $router;
  }
} #/sub query_router
### /FUNC: query_router


### DEPRECATED:
sub query_snmp {
  my $ip = shift;
  print "## IN query_snmp, IP=($ip) \n" if ($debug);
  my ($snmp, $snmp_error) = Net::SNMP->session(
    -hostname  => shift || "$ip"
  );
  my $router_sysname = $snmp->get_result(
    -varbindlist => [.1.3.6.1.2.1.1.5.0]
  );
if (!defined($snmp)) {
  printf("ERROR: %s.\n", $snmp_error);
  exit 1;
  }
  return $router_sysname;
}
### /DEPRECATED

### FUNC: resolve_domain
sub resolve_domain {
  my $hostname = shift;
  #foreach (@_) {
    #print "INPUT ARRAY HAS: $_ \n";
  #  push(@domain,$_);
  #}
  print "## IN resolve_domain, HOSTNAME = $hostname, DOMAIN = @domain // \n" if ($debug);
  foreach $domain (@domain) {
    print "// $hostname loop for domain $domain \n" if ($debug);
    $query = $res->send("$hostname.$domain");
    $query_returncode = ($query->{header}->{rcode});
    $query_count = ($query->{header}->{ancount});
      foreach my $rr ($query->answer) {
        next unless $rr->type eq "A";
        my $ip =  $rr->address;
        print "// FOUND IP =  $ip for HOST $hostname.$domain \n" if ($debug);
        return $domain if defined($ip);
        #return $ip;
      }
  }
} #/resolve_domain

### FUNC: create_hostlist_file
sub create_hostlist_file {
  # Creating list from file:
  open (HOSTLIST, "<", "$hostlist_file") || die "Can't open CFGFILE ( \"$hostlist_file\" )! $! ";
  @HOSTLIST = <HOSTLIST>;
  close OLDCONFIG;
  foreach (@HOSTLIST) {
    #print "IN create_hostlist_file (pre-strops), HOST $hostname \n" if ($debug);
    chomp($_);
    $_ =~ s/^#.*//; # Strip comments
    $_ =~ s/^\/.*//; # Strip comments
    $_ =~ s/^\s+//; #  Strip leading whitespace
    $_ =~ s/\s+$//; #  Strip trailing whitespace
    $_ =~ tr/A-Z/a-z/; # transliterate retarded Windows DNS entries to canonical lowercase...
    next if $_ =~ s/^$//; # Skip blank lines
    $_ = (split ' ', $_)[$hostlist_file_fieldnum];    # use correct input field from hostlist file
    if ( defined($inherit) ) { ### if we're specifying FQDN in the input file, use it here:
      my $temp_hostname = (split '\.', $_)[0]; # strip any periods (or decimals) from hostname (fix this eventually to use FQDN's _OR_ IP)
      $fqdn = $_;
      $domainname = $_;
      $domainname =~ s/$temp_hostname\.//;
      print "########### IN DEFINED INHERIT, DOMAINNAME=($domainname), FQDN=($fqdn) \n" if ($debug);
    }
    $_ = (split '\.', $_)[0]; # strip any periods (or decimals) from hostname (fix this eventually to use FQDN's _OR_ IP)
    next if $_ =~ s/^$//; # 
    my $hostname = "$_";
    print "########### IN create_hostlist_file (post-str_ops), HOST $hostname \n" if ($debug);

    # host:domain membership needs to happen BEFORE FQDN is determined,
    # so a loop/functio needs to happen just before $ip is set via query_dns()
    #my $fqdn = "$_.${domain}";
    my $domain = resolve_domain($hostname,@domain);
    if (defined($inherit)) {
      print "// IN DEFINED INHERIT, skipping lookup of FQDN \n" if ($debug);
    } else {
      $fqdn = "$hostname.${domain}";
    }
    my $ip = query_dns($fqdn);
    #print "// COMPLETED FUNC:'query_dns'$hostname \n" if ($debug);
    my $router = query_router($ip);
    #print "// COMPLETED FUNC:'query_router'$hostname \n" if ($debug);
    #hoh for hosts
    $hostlist{$hostname}{hostname} = "$hostname";
    $hostlist{$hostname}{domain} = "$domain";
    $hostlist{$hostname}{fqdn} = "$fqdn";
    $hostlist{$hostname}{ip} = "$ip";
    $hostlist{$hostname}{router} = "$router";
    print "//////////// EXITING create_hostlist_file, HOST $fqdn \n\n " if ($debug);
  } #/foreach

  #  Create a CSV host string:
  my $iter = 0;
  foreach (keys %hostlist) {
    ++$iter;
    if ($iter == 1) {
      $hoststring = "$_";
    } else {
      $hoststring = "$hoststring,$_";
    } #/if
  }  #/foreach
} #/sub create_hostlist_file
### /FUNC: create_hostlist_file


### FUNC: create_hostlist_seq
sub create_hostlist_seq {
  # Creating list from range:
  %hostlist = ();
  # TESTING domain, it's b0rken!
  my $domain = resolve_domain($hostname,@domain);
  foreach ( $start .. $stop ) {
    my $hostname = "${hostgroup}${_}";
    my $fqdn = "${hostgroup}${_}.${domain}";
    #my $fqdn = "${hostgroup}${_}${domain}";
    my $ip = query_dns($fqdn);
    my $router = query_router($ip);
    #hoh for hosts
    $hostlist{$hostname}{hostname} = "$hostname";
    $hostlist{$hostname}{fqdn} = "$fqdn";
    $hostlist{$hostname}{ip} = "$ip";
    $hostlist{$hostname}{router} = "$router";
  } #/foreach

  #  Create a CSV host string:
  my $iter = 0;
  foreach (sort(keys %hostlist)) {
    ++$iter;
    if ($iter == 1) {
      $hoststring = "$_";
    } else {
      $hoststring = "$hoststring,$_";
    } #/if
  }  #/foreach
} #/sub create_hostlist_seq
### FUNC: create_hostlist_seq





########################################
### END FUNCTIONS

### START DOC
########################################


=head1 NAME

populate_hostgroup.pl - Create a host configuration file from a sequence:pattern or input file.

=head1 SYNOPSIS

populate_hostgroup.pl 

=head1 OPTIONS


  "h|help"              =>  \$help,
  "f|file=s"            =>  \$hostlist_file,
  "n|number=i"          =>  \$hostlist_file_fieldnum,
  "s|startrange=i"      =>  \$start,
  "e|endrange=i"        =>  \$stop,
  "g|hostgroup=s"       =>  \$hostgroup,
  "d|domain=s"          =>  \@domain,
  "t|template=s"        =>  \$template,
  "m|mode=s"            =>  \$mode,
  "c|cstring=s"         =>  \$cstring,
  "debug"               =>  \$debug,
  "inherit"             =>  \$inherit,



REQUIRED:

 -m
 -d
 -t
 -c



OPTIONAL:

 --help, --man      This help.
 --debug	    Enables verbose debug output from internal checks/functions.

 --inherit	    

=head1 DESCRIPTION

Create a usable nagios config for a hostgroup, taking input as a list of hosts (file)
or a range (sequence).  Uses input files for host/group templates, and writes out a working
config with HOSTNAME, ALIAS, ADDRESS, and PARENTS.


=head1 CAVEAT EMPTOR

Yep, it's free, you get what you pay for.  That being said, this exists becuase I made it to fill
a void.  It works great for me, and I use it regularly.  As new needs come up or existing features
irk me, new ones will be added or old ones fixed.

=head1 AUTHOR

LICENSE: GPL, Copyright 2006 Eli Stair <eli.stair {at} gmail {dot} com>


