#!/usr/bin/perl
#
# This script is a component of Warewulf,
# http://www.runlevelzero.net/greg/warewulf
#
#########################################################################
#
# Copyright (c) 2003, The Regents of the University of California, through
# Lawrence Berkeley National Laboratory (subject to receipt of any
# required approvals from the U.S. Dept. of Energy).  All rights reserved.
#
# 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 2
# of the License, 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.
#
# The GNU GPL Document can be found at:
# http://www.gnu.org/copyleft/gpl.html
#
#########################################################################
#
# Written and maintained by:
#       Greg Kurtzer, <gmkurtzer@lbl.gov>


use IO::Socket;
use IO::Select;
use IO::Poll;
use threads;
use threads::shared;
#use strict;
use constant MAXLEN => 1024;
use Getopt::Long;

$SIG{PIPE} = 'IGNORE';

my %stats :shared;
my %locks :shared;
my $critical_loop :shared;

$critical_loop = '1';

my $usage = "USAGE: $0 [options]
  About:
    wwproxy can aggregate multiple warewulfd daemons together lightening
    the load of warewulfd (sometimes needed on larger clusters) and also
    has the ability of redistributing the node statistics in ganglia
    XML suitable for being slurped by gmetad.

  Options:
    --debug     Do not fork into the background, and show debugging output
    --wport     Port to dump ganglia output [default:9873] (can be none)
    --gport     Port to dump ganglia output [default:8649] (can be none)
    --hosts     List of hostnames (comma delim) [default: localhost]
    --refresh   Time in seconds to refresh data from warewulfd(s) [default:5]
    --legacy    Enable legacy support for warewulfd versions < 2.5
    --help      Show this banner

  This tool is part of the Warewulf cluster distribution
     http://warewulf-cluster.org/
";

GetOptions(
   'debug'     => \$debug,
   'refresh=s' => \$refresh,
   'wport=s'   => \$wport,
   'gport=s'   => \$gport,
   'hosts=s'   => \$hostnames,
   'legacy'    => \$enable_legacy,
   'help'      => \$help,
);

my $ganglia_xml_header = '<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
<!DOCTYPE GANGLIA_XML [
   <!ELEMENT GANGLIA_XML (GRID)*>
      <!ATTLIST GANGLIA_XML VERSION CDATA #REQUIRED>
      <!ATTLIST GANGLIA_XML SOURCE CDATA #REQUIRED>
   <!ELEMENT GRID (CLUSTER | GRID | HOSTS | METRICS)*>
      <!ATTLIST GRID NAME CDATA #REQUIRED>
      <!ATTLIST GRID AUTHORITY CDATA #REQUIRED>
      <!ATTLIST GRID LOCALTIME CDATA #IMPLIED>
   <!ELEMENT CLUSTER (HOST | HOSTS | METRICS)*>
      <!ATTLIST CLUSTER NAME CDATA #REQUIRED>
      <!ATTLIST CLUSTER OWNER CDATA #IMPLIED>
      <!ATTLIST CLUSTER LATLONG CDATA #IMPLIED>
      <!ATTLIST CLUSTER URL CDATA #IMPLIED>
      <!ATTLIST CLUSTER LOCALTIME CDATA #REQUIRED>
   <!ELEMENT HOST (METRIC)*>
      <!ATTLIST HOST NAME CDATA #REQUIRED>
      <!ATTLIST HOST IP CDATA #REQUIRED>
      <!ATTLIST HOST LOCATION CDATA #IMPLIED>
      <!ATTLIST HOST REPORTED CDATA #REQUIRED>
      <!ATTLIST HOST TN CDATA #IMPLIED>
      <!ATTLIST HOST TMAX CDATA #IMPLIED>
      <!ATTLIST HOST DMAX CDATA #IMPLIED>
      <!ATTLIST HOST GMOND_STARTED CDATA #IMPLIED>
   <!ELEMENT METRIC EMPTY>
      <!ATTLIST METRIC NAME CDATA #REQUIRED>
      <!ATTLIST METRIC VAL CDATA #REQUIRED>
      <!ATTLIST METRIC TYPE (string | int8 | uint8 | int16 | uint16 | int32 | uint32 | float | double | timestamp) #REQUIRED>
      <!ATTLIST METRIC UNITS CDATA #IMPLIED>
      <!ATTLIST METRIC TN CDATA #IMPLIED>
      <!ATTLIST METRIC TMAX CDATA #IMPLIED>
      <!ATTLIST METRIC DMAX CDATA #IMPLIED>
      <!ATTLIST METRIC SLOPE (zero | positive | negative | both | unspecified) #IMPLIED>
      <!ATTLIST METRIC SOURCE (gmond | gmetric) #REQUIRED>
   <!ELEMENT HOSTS EMPTY>
      <!ATTLIST HOSTS UP CDATA #REQUIRED>
      <!ATTLIST HOSTS DOWN CDATA #REQUIRED>
      <!ATTLIST HOSTS SOURCE (gmond | gmetric | gmetad) #REQUIRED>
   <!ELEMENT METRICS EMPTY>
      <!ATTLIST METRICS NAME CDATA #REQUIRED>
      <!ATTLIST METRICS SUM CDATA #REQUIRED>
      <!ATTLIST METRICS NUM CDATA #REQUIRED>
      <!ATTLIST METRICS TYPE (string | int8 | uint8 | int16 | uint16 | int32 | uint32 | float | double | timestamp) #REQUIRED>
      <!ATTLIST METRICS UNITS CDATA #IMPLIED>
      <!ATTLIST METRICS SLOPE (zero | positive | negative | both | unspecified) #IMPLIED>
      <!ATTLIST METRICS SOURCE (gmond | gmetric) #REQUIRED>
]>
<GANGLIA_XML VERSION="2.5.4" SOURCE="gmond">
';

my $ganglia_xml_footer = "</GANGLIA_XML>\n";

my %ww_ganglia_conversion = (
   LOADAVG => { GNAME => 'load_one', UNITS => '', TYPE => 'float', SLOAP => 'both'},
   NETTRANSMIT => { GNAME => 'pkts_out', UNITS => 'packets/sec', TYPE => 'float', SLOAP => 'both'},
   NETRECIEVE => { GNAME => 'pkts_in', UNITS => 'packets/sec', TYPE => 'float', SLOAP => 'both'},
   CPUUTIL => { GNAME => 'cpu_user', UNITS => '%', TYPE => 'float', SLOAP => 'both'},
   PROCS => { GNAME => 'proc_total', UNITS => '', TYPE => 'unit32', SLOAP => 'both'},
   CPUCLOCK => { GNAME => 'cpu_speed', UNITS => 'MHz', TYPE => 'unit32', SLOAP => 'both'},
   ARCH => { GNAME => 'machine_type', UNITS => '', TYPE => 'string', SLOAP => 'zero'},
   RELEASE => { GNAME => 'os_release', UNITS => '', TYPE => 'string', SLOAP => 'zero'},
   CPUCOUNT => { GNAME => 'cpu_num', UNITS => '', TYPE => 'unit16', SLOAP => 'zero'},
   SYSNAME => { GNAME => 'os_name', UNITS => '', TYPE => 'string', SLOAP => 'zero'},
   MEMTOTAL => { GNAME => 'mem_total', UNITS => 'MB', TYPE => 'unit32', SLOAP => 'zero'},
   MEMUSED => { GNAME => 'mem_used', UNITS => 'MB', TYPE => 'unit32', SLOAP => 'zero'},
   SWAPTOTAL => { GNAME => 'swap_total', UNITS => 'MB', TYPE => 'unit32', SLOAP => 'zero'},
   SWAPUSED => { GNAME => 'swap_used', UNITS => 'MB', TYPE => 'unit32', SLOAP => 'zero'},
   UPTIME => { GNAME => 'uptime', UNITS => 'days', TYPE => 'unit32', SLOAP => 'zero'},
   NODESTATUS => { GNAME => 'node_status', UNITS => '', TYPE => 'string', SLOAP => 'zero'},
);

if ( $help ) {
   print $usage;
   exit;
}

unless ( $debug ) {
   if ( -f '/var/run/wwproxy.pid' ) {
      print "Warewulf proxy daemon already running!\n";
      open(PID, "/var/run/wwproxy.pid");
      $pid = <PID>;
      close PID;
      print "PID=$pid (/var/run/wwproxy.pid)\n";
      exit 1;
   }
   open(STDIN, "/dev/null");
   open(STDOUT, ">/dev/null");
   open(STDERR, ">/dev/null");
   fork and exit;
}   

$pid = $$;
open(PID, '> /var/run/wwproxy.pid')
   or warn "Could not create PID file at: '/var/run/wwproxy.pid'\n";
print PID $pid;
close PID;

$SIG{HUP} = sub {
   #&get_hosts;
   return('1');
};
$SIG{TERM} = sub {
   unlink('/var/run/wwproxy.pid');
   exit 1;
};
$SIG{INT} = sub {
   warn "-Cought Signal Interrupt, exiting...\n";
   unlink('/var/run/wwproxy.pid');
   $critical_loop = ();
   foreach $thr ( keys %thread ) {
      $thread{$thr}->join();
   }
   exit 1;
};

if ( $wport ) {
   $warewulf_port = $wport;
} else {
   $warewulf_port = '9873';
}

if ( $gport ) {
   $ganglia_port = $gport;
} else {
   $ganglia_port = '8649';
}

if ( ! $refresh) {
   $refresh = '5';
}

my $data;

if ( $wport ne 'none' ) {
   $debug and warn "Building TCP socket at '$warewulf_port'\n";
   $S = IO::Socket::INET->new(Proto     => 'tcp',
                                 LocalPort => $warewulf_port,
                                 Listen    => 1,
                                 Reuse     => 1,
                                 Timeout  => 1,
                                 ) || die "can't make socket: $!";
}

if ( $gport ne 'none' ) {
   $debug and warn "Building TCP socket at '$ganglia_port'\n";
   $R = IO::Socket::INET->new(Proto     => 'tcp',
                                 LocalPort => $ganglia_port,
                                 Listen    => 1,
                                 Reuse     => 1,
                                 ) || die "can't make socket: $!";
}

$debug and warn "Adding the socket handles to IO::Select\n";
$sel = new IO::Select();
if ( $wport ne 'none' ) {
   $debug and warn "Adding warewulf_port to IO:Select\n";
   $sel->add($S);
}
if ( $gport ne 'none' ) {
   $sel->add($R);
}


sub node_status {
   my ( $host, @NULL ) = @_;
   my ( %nodes, %nodecfg, @s, $entry, $value, $nodename, $sock );

   while (1) {
      if ( $locks{"$host"} ) {
         return;
      }
      $locks{"$host"} = 1;
   
      $debug and warn "getting stats from $host\n";
   
      unless ( $host ) {
         $host = 'localhost';
      }
      unless ( $port ) {
         $port = '9873';
      }
      unless ( $timeout ) {
         $timeout = '5';
      }
   
      my $sock = new IO::Socket::INET ( PeerAddr => $host,
                                        PeerPort => 9873,
                                        Proto    => 'tcp',
                                      );
      unless ( $sock ) {
         warn "Could not connect to $host:$port!\n";
      }
      
      $tmp = ();
      while (<$sock>) {
         $tmp .= $_;
      }
      $stats{"$host"} = $tmp;
      
      $locks{"$host"} = ();
      for ($i=0;$i<$refresh;$i++) {
         if ( ! $critical_loop ) {
            exit;
         }
         sleep 1;
      }
   }
   return;
}

# start some threads for syncing...
@hosts = split(/,/, $hostnames);
foreach $hostname ( @hosts ) {
   $thread{"$hostname"} = threads->create("node_status", "$hostname");
}

sub xml_host_string {
   my ( $gname, $value, $gtype, $gunits, $slope, @NULL) = @_;
   my $out;
   $out .= "<METRIC NAME=\"$gname\" ";
   $out .= "VAL=\"$value\" TYPE=\"$gtype\" ";
   $out .= "UNITS=\"$gunits\" ";
   $out .= "TN=\"1\" TMAX=\"180\" DMAX=\"0\" ";
   $out .= "SLOPE=\"$gslope\" ";
   $out .= "SOURCE=\"warewulfd\"/>\n";
   return ($out);
}

$debug and warn "Starting IO::Select loop\n";
$debug and warn "Listening on port $monitor_port\n";
while (1) {
   #local $SIG{ALRM} = sub { &update_stats() };
   #alarm(2);
   while (@ready = $sel->can_read()) {
      foreach $fh (@ready) {
         if ( $fh == $S ) {
            my $new_sock = $S->accept();
            $addr = $new_sock->peerhost();
            $debug and warn "Sending=>TCP:$warewulf_port -> stats to $addr ($network)\n";
            foreach $cluster ( keys %stats ) {
               if ( $enable_legacy ) {
                  print $new_sock "CLUSTER=$cluster\n";
               }
               print $new_sock "$stats{$cluster}\n";
            }
            close($new_sock);
         } elsif ( $fh == $R ) {
            my $new_sock = $R->accept();
            $addr = $new_sock->peerhost();
            $debug and warn "Sending=>TCP:$ganglia_port -> stats to $addr ($network)\n";
            print $new_sock $ganglia_xml_header;
            foreach $cluster ( keys %stats ) {
               print $new_sock "<CLUSTER NAME=\"$cluster\" LOCALTIME=\"unspecified\" OWNER=\"unspecified\" LATLONG=\"unspecified\" URL=\"unspecified\">\n";
               @lines = split(/\n/, $stats{"$cluster"});
               foreach $line ( @lines ) {
                  ($key, $value) = split(/=/, $line);
                  if ( $key eq 'NODE' or $key eq 'NODENAME' ) {
                     if ( $node and $reported ) {
                        print $new_sock "<HOST NAME=\"$node\" ";
                        print $new_sock "REPORTED=\"$reported\" TN=\"$lastcontact\" TMAX=\"600\" DMAX=\"0\" ";
                        print $new_sock "LOCATION=\"unspecified\" GMOND_STARTED=\"\">\n";
                        print $new_sock $ganglia_out_string;
                        print $new_sock &xml_host_string("proc_run", "0", "unit32", "", "both");
                        print $new_sock &xml_host_string("mem_shared", "0", "unit32", "KB", "both");
                        print $new_sock &xml_host_string("mem_free", "unspecified", "unit32", "KB", "both");
                        print $new_sock &xml_host_string("mem_cached", "unspecified", "unit32", "KB", "both");
                        print $new_sock &xml_host_string("mem_buffers", "unspecified", "unit32", "KB", "both");
                        print $new_sock &xml_host_string("cpu_nice", "0.0", "float", "%", "both");
                        print $new_sock &xml_host_string("cpu_system", "0.0", "float", "%", "both");
                        print $new_sock &xml_host_string("cpu_idle", "unspecified", "float", "%", "both");
                        print $new_sock "</HOST>\n";
                        $node = ();
                        $reported = ();
                        $ganglia_out_string = ();
                     }
                     $node = $value;
                  } elsif ( $key eq 'LASTCONTACT' ) {
                     $lastcontact = $value + 1;
                     $reported = time() - $value;
                  } elsif ( $ww_ganglia_conversion{"$key"} ) {
                     $ganglia_out_string .= &xml_host_string(
                                 $ww_ganglia_conversion{$key}{GNAME},
                                 $value,
                                 $ww_ganglia_conversion{$key}{TYPE},
                                 $ww_ganglia_conversion{$key}{UNITS},
                                 $ww_ganglia_conversion{$key}{SLOPE});
                  } 
               }
               if ( $node and $reported ) {
                  print $new_sock "<HOST NAME=\"$node\" ";
                  print $new_sock "REPORTED=\"$reported\" TN=\"$lastcontact\" TMAX=\"600\" DMAX=\"0\" ";
                  print $new_sock "LOCATION=\"unspecified\" GMOND_STARTED=\"\">\n";
                  print $new_sock $ganglia_out_string;
                  print $new_sock &xml_host_string("proc_run", "0", "unit32", "", "both");
                  print $new_sock &xml_host_string("mem_shared", "0", "unit32", "KB", "both");
                  print $new_sock &xml_host_string("mem_free", "unspecified", "unit32", "KB", "both");
                  print $new_sock &xml_host_string("mem_cached", "unspecified", "unit32", "KB", "both");
                  print $new_sock &xml_host_string("mem_buffers", "unspecified", "unit32", "KB", "both");
                  print $new_sock &xml_host_string("cpu_nice", "0.0", "float", "%", "both");
                  print $new_sock &xml_host_string("cpu_system", "0.0", "float", "%", "both");
                  print $new_sock &xml_host_string("cpu_idle", "unspecified", "float", "%", "both");
                  $node = ();
                  $reported = ();
                  $ganglia_out_string = ();
               }
               print $new_sock "</CLUSTER>\n";
            }
            print $new_sock $ganglia_xml_footer;
            close($new_sock);
         }
      }
   }
}
         
exit;
