#!/usr/local/bin/perl -Tw
use vars qw( $VERSION $DBHOST $DBTYPE $DATABASE $DBUSER $DBPASS $DEBUG $DB
	     $CLASS $ROOTCLASS $GNUPLOT $TERMINAL $DAYS );
$VERSION = 1.1;

###############################################################################
### Configuration #############################################################
###############################################################################

## Load shared configurations and/or private data using 'do' commands, as
## seen below.  Note that several 'do's can be run if necessary.  

# do '/FULL/PATH/TO/CODE/TO/RUN';       

## This is the perl class that you will be using in this script.  

$CLASS   = "TCB::SysLoads";                     # Database class
  
## This is the root class of the above class.  Essentially a hack to let
## there be multiple modules using the same database.

$ROOTCLASS = "TCB::System";                     # Class of the database class

## Modify and uncomment this to use user code instead of just system-wide 
## modules.  Note that this path must be set up as a standard Perl tree;
## I'd personally recommend just installing things system-wide unless you're
## a developer.

# use lib '/PATH/TO/USER/CODE';
# use lib '/home/tskirvin/dev/mdtools/tcb-sysloads';

## Database Information
## You may want to set these with a common config file, using 'do FILE'.
## Also, defaults may already be set within the class; only set these if
## you want to override the defaults.

# $DBHOST   = "";               # System that hosts the database
# $DBTYPE   = "";               # The type of database that we're working on
# $DATABASE = "";               # Name of the database we're connecting to
# $DBUSER   = "";               # Username to connect to the database
# $DBPASS   = "";               # Password to connect to the database

do "/home/webserver/dbaccess/user.sysloads";	# Load DB information

## Where is gnuplot installed?  We need this to do the graphing.

$GNUPLOT = "/usr/local/bin/gnuplot";

## What terminal should we use to make the graph?

$TERMINAL = "postscript color";		# Could be 'png' for web 

## How many days should we plot, by default?

$DAYS = 28;				

###############################################################################
### main() ####################################################################
###############################################################################

use strict;
use DBIx::Frame;
use Date::Parse;

# Load the appropriate class module
{ local $@; eval "use $CLASS";  die "$@\n" if $@; }

delete $ENV{'PATH'};		# Fixes pesky taint problems
$0 =~ s%.*/%%g;         # Lose the annoying path information

# Get initial information from command-line
my ($start, $end) = @ARGV;
my $oldtime   = time - 86400 * $DAYS;
my $starttime = $start ? str2time($start) || $oldtime : $oldtime;
my $endtime   = $end ? str2time($end) : $starttime + $days * 86400;

my @MONTHS = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);

my $prettystart = sprintf("%02d %3s %04d",
                (localtime($starttime))[3],
                $MONTHS[(localtime($starttime))[4]],
                (localtime($starttime))[5] + 1900);
                
my $prettyend = sprintf("%02d %3s %04d",
                (localtime($endtime))[3],
                $MONTHS[(localtime($endtime))[4]],
                (localtime($endtime))[5] + 1900);
                
my $START = sprintf("%04d%02d%02d",
                (localtime($starttime))[5] + 1900,
                (localtime($starttime))[4] + 1,
                (localtime($starttime))[3]); 
                
my $END   = sprintf("%04d%02d%02d",
                (localtime($endtime))[5] + 1900,
                (localtime($endtime))[4] + 1,
                (localtime($endtime))[3]); 

# Connect to the database
$DB = $ROOTCLASS->connect( $DATABASE, $DBUSER, $DBPASS, $DBHOST, $DBTYPE )
        or error("Couldn't connect to $DBHOST: $DBI::errstr");
my $error = $DBI::errstr;       # Avoid a warning, otherwise unnecessary

# Start up GNUPLOT
open(GNUPLOT, "| $GNUPLOT -persist") or die "Couldn't open GNUPLOT";
print GNUPLOT <<INIT;
set title "System Loads from $prettystart to $prettyend"
set timefmt "%d-%b-%Y"
set terminal $TERMINAL
set xdata time
set boxwidth 86400
set yrange [0:100]
set format x "%d-%b-%y"
set xlabel "Date"
set ylabel "Percentage of Use"
set key top left
INIT

# Parse the input

my $machines = {};  my $groups   = {};
my (@graph, %dates);
my $line = 0;  my $count = 2;
while (<STDIN>) {
  $line++;
  chomp;  s%\#.*$%%;    # Trim newlines and anything past a comment character
  next unless $_;       # Skip blank lines
  my ($command, @args) = split(/\s+/, $_);
  if (lc $command eq 'group') {
    my ($group, $machine, $mult, $maxload) = @args;
    next unless ($group && $machine);  
    $mult ||= 1;  $maxload ||= 1;
 
    $$machines{$machine} ||= "$mult,$maxload";

    if (defined($$groups{$group})) {
      my %hash;         $hash{$machine}++;
      foreach (split(',', $$groups{$group})) { $hash{$_}++ }
      $$groups{$group} = join(',', keys %hash);
    } else {
      $$groups{$group} = $machine;
    }

  } elsif (lc $command eq 'machine') {
    my ($machine, $mult, $maxload) = @args;
    next unless $machine;  $mult ||= 1;   $maxload ||= 1;
    $$machines{$machine} ||= "$mult,$maxload";

  } elsif (lc $command eq 'show') {
    my ($machine, @desc) = @args;
    next unless $machine;

    my @machines = machines($machine, $groups);

    my (%loads, %max, %sum);
    my $totalmax = 0;
    foreach my $machine (@machines) {
      my ($mult, $maxload) = split(',', $$machines{$machine});
          $mult ||= 1;  $maxload ||= 1;
      $totalmax += $maxload * $mult;

      # my $mult = $$machines{$_} || 1;
      my @items = $DB->select('SysLoad', { 'Machine' => $machine, 
			'TimeStamp' => [ ">= $START", "<= $END" ] }); 
      foreach my $item (reverse @items) {
      # foreach my $item (@items) {
        my $time = $$item{TimeStamp} || next;  
        my $load = $$item{SysLoad} || 0;
        $loads{$time} += $load;  
        $max{$time} += $maxload || 1;
        $sum{$time} += $load * $mult;
      }
    }
    foreach my $time (keys %loads) {
      $dates{$time} ||= [];
      my $load = sprintf("%2.2f", $loads{$time} * 100 / $max{$time});
      $dates{$time}[$count-2] = $load;
    }
    my $line = "1:$count title \"@desc (SpecFP $totalmax)\" with lines";
    push @graph, $line;
    $count++;
  } else {
    warn "Bad command at line $line: '$command'\n";
  }
}

# Plot the information
my $file = "/tmp/plot.loads.$$-" . time;
open (FILE, ">$file") or die "Couldn't write to $file!";
foreach my $date (sort keys %dates) {
  my $nicedate = $date;
  $nicedate =~ s%(\d\d\d\d)-(\d\d)-(\d\d)%
      join('-', $3, _month($2), $1)%egx;

  my @data = map { "?" unless $_ } @{$dates{$date}}; 
  print FILE "$nicedate @data\n";
}

=cut
my $file = "/tmp/plot.loads.$$-" . time;	# Choose a filename in tmp
open (FILE, ">$file") or die "Couldn't write to $file!";
foreach my $date (sort keys %dates) {
  my $nicedate = $date;
  $nicedate =~ s%(\d\d\d\d)-(\d\d)-(\d\d)%
      join('-', $3, _month($2), $1)%egx;

  my @data = map { "?" unless $_ } @{$dates{$date}}; 
  print FILE "$nicedate @data\n";
  print "$nicedate @data\n" if $DEBUG;
}
=cut

close(FILE);

print "plot \"$file\" using ", join(", '' using ", @graph), "\n" if $DEBUG;
print GNUPLOT "plot \"$file\" using ", join(", '' using ", @graph), "\n" ;

close GNUPLOT;
unlink $file;

# Disconnect and exit

$DB->disconnect;
exit(0);

###############################################################################
### Subroutines ###############################################################
###############################################################################

### _month
# Returns the month, based on the number given.
sub _month { (qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec))[(shift)-1]; }

### machines( machines, groups ) 
# Returns a list of machines from a list which contains machines and
# groups.  'machines' is a single string, comma-delimited, that contains 
# everything we're parsing.  'groups' is a hash reference that contains
# all of the groups of machines to this point.  
sub machines {
  my ($machines, $groups) = @_;  
  return undef unless ($machines && $groups && ref $groups);
  
  my %machines;
  foreach (split(',', $machines)) {
    if (defined $$groups{$_}) { 
      foreach my $mac (machines($$groups{$_}, $groups)) { 
        $machines{$mac}++ if $mac ;
      }
    } else { $machines{$_}++ if $_; }
  }
  
  return sort keys %machines;
}

### error ( MESSAGE )
# Gives a warning of MESSAGE and exits.
sub error { foreach (@_) { warn "$_\n"; } exit(1); }

###############################################################################
### Documentation #############################################################
###############################################################################

=head1 NAME

sysload_graph - create a graph of the system loads of TCB systems

=head1 SYNOPSIS

  sysload_graph < INPUT | ghostview -landscape -

See the description field for more information.

=head1 DESCRIPTION

sysload creates a postscript version of the group's system loads from
input given on STDIN.  The loads are taken from a database; the input
defines which loads to extract and display.

=head1 USAGE

sysload_graph [start] [stop] 

=over 4

=item start

=item stop

Date to start or stop displaying information from.  Must be given in the 
form of YYYY-MM-DD.  Both fields are optional; by default, the start date is
the running date and the end date is 28 days before.

=back

=head1 INPUT 

sysload takes input on STDIN to decide which database entries to view.  
This will normally take the form of a file, containing lines that are
formatted like one of the following:

=over 4

=item machine MACHINE [multiplier] [maxload]

Initializes the data for machine MACHINE from the database.  C<multiplier>
is an optional field giving the SpecFP given from a load average of 1.
C<maxload> is the maximum load the machine should have; this varies by
system.  

=item group GROUP MACHINE [multiplier] [maxload]

Adds MACHINE into the group GROUP.  Also initializes the MACHINE, as above.

=item show MACHINELIST [DESCRIPTION]

Finds the data on the machines in MACHINELIST and displays it in the graph.
MACHINELIST is a comma-delimited, no-spaces list of MACHINE or GROUP entries
from above.  DESCRIPTION is a text-description of this display.  Each 'show' 
will offer an additional line on the graph.

=head2 SAMPLE INPUT

  machine titan 	19	8
  machine rhea  	19	4
  group alpha galatea	58 	2
  group alpha despina	58 	2
  group alpha naiad	58 	2
  group alpha thalassa	58 	2
  show alpha		  Alphas
  show rhea		  Rhea
  show titan		  Titan
  show alpha,titan,rhea	  All

=back

Comments can be added with a '#' character.  Blank lines are ignored.

=head1 OUTPUT

Outputs a color postscript file, which can be viewed by piping it
through 'ghostview -landscape -', saved by outputting to a file, etc.
The graph is date vs system load percentage, with the maximum numbers 
listed with the descriptions of what is being graphed in the upper right
corner (it's probably best to just look at it).

=head1 REQUIREMENTS

GNUPlot, Perl 5.6.0 or better, MySQL, C<DBIx::Frame>, B<TCB::System>,
B<TCB::SysLoads>, the sysload package

=head1 SEE ALSO

L<add-sysload>, L<sysload.cgi>, L<TCB::SysLoads>

=head1 TODO

A different input setup would be nice, but I'm not sure if it's worth 
the trouble.

=head1 AUTHOR

Written by Tim Skirvin <tskirvin@ks.uiuc.edu>.

=head1 HOMEPAGE

B<http://www.ks.uiuc.edu/Development/MDTools/tcb-sysloads>

=head1 LICENSE

This code is distributed under the University of Illinois Open Source
License.  See
C<http://www.ks.uiuc.edu/Development/MDTools/tcb-sysloads/license.html>
for
details.

=head1 COPYRIGHT

Copyright 2001-2004 by the University of Illinois Board of Trustees and
Tim Skirvin <tskirvin@ks.uiuc.edu>.

=cut


###############################################################################
### Version History ###########################################################
###############################################################################
# v1.0		Fri Jan 19 15:05:50 CST 2001
### Initial Version.  No version history was being stored at this point.
# v1.1		Fri May 14 09:27:43 CDT 2004 
### Updated for TCB::SysLoads.  Starting using Date::Parse to get date
### information.
