Some changes to Machines and Rexe
authoradefaria <adefaria@adefaria-lt.audience.local>
Sat, 5 Apr 2014 02:08:46 +0000 (19:08 -0700)
committeradefaria <adefaria@adefaria-lt.audience.local>
Sat, 5 Apr 2014 02:08:46 +0000 (19:08 -0700)
etc/machines.conf [new file with mode: 0644]
lib/Machines.pm
lib/machines.sql [new file with mode: 0644]
test/testrexec.pl

diff --git a/etc/machines.conf b/etc/machines.conf
new file mode 100644 (file)
index 0000000..17f2146
--- /dev/null
@@ -0,0 +1,16 @@
+###############################################################################
+#
+# File:         $RCSfile: machines.conf,v $
+# Revision:     $Revision: 1.0 $
+# Description:  Config file for Machines
+# Author:       Andrew@ClearSCM.com
+# Created:      Fri Apr  4 14:29:21 PDT 2014
+# Modified:     $Date:  $
+# Language:     conf
+#
+# (c) Copyright 2014, ClearSCM, Inc., all rights reserved
+#
+###############################################################################
+MACHINES_SERVER:            adefaria-lt
+MACHINES_USERNAME:          machines
+MACHINES_PASSWORD:          machines
index 804b35a..c872eda 100644 (file)
@@ -54,119 +54,275 @@ package Machines;
 use strict;
 use warnings;
 
-use Display;
-use Utils;
+use Carp;
+use DBI;
+use FindBin;
 
-use base 'Exporter';
+use DateUtils;
+use Display;
+use GetConfig;
+
+our %MACHINESOPTS = GetConfig ("$FindBin::Bin/../etc/machines.conf");
+
+my $defaultFilesystemThreshold = 90;
+my $defaultFilesystemHist      = '6 months';
+my $defaultLoadavgHist         = '6 months';
+
+# Internal methods
+sub _dberror ($$) {
+  my ($self, $msg, $statement) = @_;
+
+  my $dberr    = $self->{db}->err;
+  my $dberrmsg = $self->{db}->errstr;
+  
+  $dberr    ||= 0;
+  $dberrmsg ||= 'Success';
+
+  my $message = '';
+  
+  if ($dberr) {
+    my $function = (caller (1)) [3];
+
+    $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
+             . "SQL Statement: $statement";
+  } # if
+
+  return $dberr, $message;  
+} # _dberror
+
+sub _formatValues (@) {
+  my ($self, @values) = @_;
+  
+  my @returnValues;
+  
+  # Quote data values
+  push @returnValues, $_ eq '' ? 'null' : $self->{db}->quote ($_)  
+    foreach (@values);
+  
+  return @returnValues;
+} # _formatValues
+
+sub _formatNameValues (%) {
+  my ($self, %rec) = @_;
+  
+  my @nameValueStrs;
+  
+  push @nameValueStrs, "$_=" . $self->{db}->quote ($rec{$_})
+    foreach (keys %rec);
+    
+  return @nameValueStrs;
+} # _formatNameValues
+
+sub _error () {\r
+  my ($self) = @_;
+  
+  if ($self->{msg}) {
+    if ($self->{errno}) {
+      carp $self->{msg};
+    } else {
+      cluck $self->{msg};
+    } # if
+  } # if\r
+} # _error
+
+sub _addRecord ($%) {
+  my ($self, $table, %rec) = @_;
+  
+  my $statement  = "insert into $table (";
+     $statement .= join ',', keys %rec;
+     $statement .= ') values (';
+     $statement .= join ',', $self->_formatValues (values %rec);
+     $statement .= ')';
+  
+  my ($err, $msg);
+  
+  $self->{db}->do ($statement);
+  
+  return $self->_dberror ("Unable to add record to $table", $statement);
+} # _addRecord
+
+sub _checkRequiredFields ($$) {
+  my ($fields, $rec) = @_;
+  
+  foreach my $fieldname (@$fields) {
+    my $found = 0;
+    
+    foreach (keys %$rec) {
+      if ($fieldname eq $_) {
+         $found = 1;
+         last;
+      } # if
+    } # foreach
+    
+    return "$fieldname is required"
+      unless $found;
+  } # foreach
+  
+  return;
+} # _checkRequiredFields
+
+sub _connect (;$) {
+  my ($self, $dbserver) = @_;
+  
+  $dbserver ||= $MACHINESOPTS{MACHINES_SERVER};
+  
+  my $dbname   = 'machines';
+  my $dbdriver = 'mysql';
+
+  $self->{db} = DBI->connect (
+    "DBI:$dbdriver:$dbname:$dbserver", 
+    $MACHINESOPTS{MACHINES_USERNAME},
+    $MACHINESOPTS{MACHINES_PASSWORD},
+    {PrintError => 0},
+  ) or croak (
+    "Couldn't connect to $dbname database " 
+  . "as $MACHINESOPTS{MACHINESADM_USERNAME}\@$MACHINESOPTS{MACHINESADM_SERVER}"
+  );
+  
+  $self->{dbserver} = $dbserver;
+  
+  return;
+} # _connect
+
+sub _getRecords ($$) {
+  my ($self, $table, $condition) = @_;
+  
+  my ($err, $msg);
+    
+  my $statement = "select * from $table where $condition";
+  
+  my $sth = $self->{db}->prepare ($statement);
+  
+  unless ($sth) {
+    ($err, $msg) = $self->_dberror ('Unable to prepare statement', $statement);
+    
+    croak $msg;
+  } # if
+
+  my $attempts    = 0;
+  my $maxAttempts = 3;
+  my $sleepTime   = 30;
+  my $status;
+  
+  # We've been having the server going away. Supposedly it should reconnect so
+  # here we simply retry up to $maxAttempts times to re-execute the statement. 
+  # (Are there other places where we need to do this?)
+  $err = 2006;
+  
+  while ($err == 2006 and $attempts++ < $maxAttempts) {
+    $status = $sth->execute;
+    
+    if ($status) {
+      $err = 0;
+      last;
+    } else {
+      ($err, $msg) = $self->_dberror ('Unable to execute statement',
+                                      $statement);
+    } # if
+    
+    last if $err == 0;
+    
+    croak $msg unless $err == 2006;
+
+    my $timestamp = YMDHMS;
+      
+    $self->Error ("$timestamp: Unable to talk to DB server.\n\n$msg\n\n"
+                . "Will try again in $sleepTime seconds", -1);
+                
+    # Try to reconnect
+    $self->_connect ($self->{dbserver});
+
+    sleep $sleepTime;
+  } # while
+
+  $self->Error ("After $maxAttempts attempts I could not connect to the database", $err)
+    if ($err == 2006 and $attempts > $maxAttempts);
+  
+  my @records;
+  
+  while (my $row = $sth->fetchrow_hashref) {
+    push @records, $row;
+  } # while
+  
+  return @records;
+} # _getRecord
 
-our @EXPORT = qw (
-  all
-  new
-);
 
 sub new {
   my ($class, %parms) = @_;
 
 =pod
 
-=head2 new (<parms>)
-
-Construct a new Machines object. The following OO style arguments are
-supported:
-
-Parameters:
-
-=for html <blockquote>
-
-=over
-
-=item file:
-
-Name of an alternate file from which to read machine information. This
-is intended as a quick alternative.
-
-=back
-
-=for html </blockquote>
+=head2 new ($server)
 
-Returns:
-
-=for html <blockquote>
-
-=over
-
-=item Machines object
-
-=back
-
-=for html </blockquote>
+Construct a new Machines object.
 
 =cut
 
-  my $file = $parms{file} ? $parms{file} : "$FindBin::Bin/../etc/machines";
-
-  error "Unable to find $file", 1 if ! -f $file;
-
-  my %machines;
-
-  foreach (ReadFile $file) {
-    my @parts = split;
-
-    # Skip commented out or blank lines
-    next if $parts[0] =~ /^#/ or $parts[0] =~ /^$/;
-
-    $machines{$parts[0]} = $parts[1];
-  } # foreach
-
-  bless {
-    file     => $parms {file},
-    machines => \%machines,
-  }, $class; # bless
-
-  return $class;
+  # Merge %parms with %MACHINEOPTS
+  foreach (keys %parms) {
+    $MACHINESOPTS{$_} = $parms{$_};
+  } # foreach;
+  
+  my $self = bless {}, $class;
+  
+  $self->_connect ();
+  
+  return $self;
 } # new
 
-sub all () {
-  my ($self) = @_;
-
-=pod
-
-=head3 all ()
-
-Returns all known machines as an array of hashes
-
-Parameters:
-
-=for html <blockquote>
-
-=over
-
-=item none
-
-=back
-
-=for html </blockquote>
-
-Returns:
-
-=begin html
-
-<blockquote>
-
-=end html
-
-=over
-
-=item Array of machine hash records
-
-=back
-
-=for html </blockquote>
-
-=cut
-
-  return %{$self->{machines}};
-} # display
+sub add (%) {
+  my ($self, %system) = @_;
+  
+  my @requiredFields = qw(
+    name
+    admin
+    type
+  );
+
+  my $result = _checkRequiredFields \@requiredFields, \%system;
+  
+  return -1, "add: $result" if $result;
+  
+  $system{loadavgHist} ||= $defaultLoadavgHist;
+  
+  return $self->_addRecord ('system', %system);
+} # add
+
+sub delete ($) {
+  my ($self, $name) = @_;
+
+  return $self->_deleteRecord ('system', "name='$name'");  
+} # delete
+
+sub update ($%) {
+  my ($self, $name, %update) = @_;
+
+  return $self->_updateRecord ('system', "name='$name'", %update);
+} # update
+
+sub get ($) {
+  my ($self, $system) = @_;
+  
+  return unless $system;
+  
+  my @records = $self->_getRecords (
+    'system', 
+    "name='$system' or alias like '%$system%'"
+  );
+  
+  if ($records[0]) {
+    return %{$records[0]};
+  } else {
+        return;
+  } # if
+} # get
+
+sub find (;$) {
+  my ($self, $condition) = @_;
+
+  return $self->_getRecords ('system', $condition);
+} # find
 
 1;
 
diff --git a/lib/machines.sql b/lib/machines.sql
new file mode 100644 (file)
index 0000000..0d9a2d4
--- /dev/null
@@ -0,0 +1,138 @@
+system-- -----------------------------------------------------------------------------
+--
+-- File:        $RCSfile: machines.sql,v $
+-- Revision:    $Revision: 1.0 $
+-- Description: Create the machines database
+-- Author:      Andrew@DeFaria.com
+-- Created:     Fri Apr  4 10:31:11 PDT 2014
+-- Modified:    $Date: $
+-- Language:    SQL
+--
+-- Copyright (c) 2014, ClearSCM, Inc., all rights reserved
+--
+-- -----------------------------------------------------------------------------
+-- Warning: The following line will delete the old database!
+drop database if exists machines;
+
+-- Create a new database
+create database machines;
+
+-- Now let's focus on this new database
+use machines;
+
+-- system: Define what makes up a system or machine
+create table system (
+  name             varchar (255) not null,
+  alias            varchar (255),
+  active           enum (
+                     'true',
+                     'false'
+                   ) not null default 'true',
+  admin            tinytext,
+  email            tinytext,
+  os               tinytext,
+  type             enum (
+                     'Linux',
+                     'Unix',
+                     'Windows'
+                   ) not null,
+  region           tinytext,
+  lastheardfrom    datetime,
+  description      text,
+  loadavgHist      enum (
+                     '1 month',
+                     '2 months',
+                     '3 months',
+                     '4 months',
+                     '5 months',
+                     '6 months',
+                     '7 months',
+                     '8 months',
+                     '9 months',
+                     '10 months',
+                     '11 months',
+                     '1 year'
+                   ) not null default '6 months',
+  loadavgThreshold float (4,2) default 5.00,
+
+  primary key (name)
+) engine=innodb; -- system
+
+-- package: A package is any software package that we wish to keep track of
+create table package (
+  system      varchar (255) not null,
+  name        varchar (255) not null,
+  version     tinytext not null,
+  vendor      tinytext,
+  description text,
+
+  key packageIndex (name),
+  key systemIndex (system),
+  foreign key systemLink (system) references system (name)
+    on delete cascade
+    on update cascade,
+  primary key (system, name)
+) engine=innodb; -- package
+  
+-- filesystem: A systems file systems that we are monitoring 
+create table filesystem (
+  system         varchar (255) not null,
+  filesystem     varchar (255) not null,
+  fstype         tinytext not null,
+  mount          tinytext,
+  threshold      int default 90,
+  notification   varchar (255),
+  filesystemHist enum (
+                   '1 month',
+                   '2 months',
+                   '3 months',
+                   '4 months',
+                   '5 months',
+                   '6 months',
+                   '7 months',
+                   '8 months',
+                   '9 months',
+                   '10 months',
+                   '11 months',
+                   '1 year'
+                 ) not null default '6 months',
+  
+  key filesystemIndex (filesystem),
+  foreign key systemLink (system) references system (name)
+    on delete cascade
+    on update cascade,
+  primary key (system, filesystem)
+) engine=innodb; -- filesystem
+
+-- fs: Contains a snapshot reading of a filesystem at a given date and time
+create table fs (
+  system         varchar(255) not null,
+  filesystem     varchar(255) not null,
+  mount          varchar(255) not null,
+  timestamp      datetime     not null,
+  size           bigint,
+  used           bigint,
+  free           bigint,
+  reserve        bigint,
+
+  key mountIndex (mount), 
+  primary key   (system, filesystem, timestamp),
+  foreign key   filesystemLink (system, filesystem)
+    references filesystem (system, filesystem)
+      on delete cascade
+      on update cascade
+) engine=innodb; -- fs
+
+-- loadavg: Contains a snapshot reading of a system's load average
+create table loadavg (
+  system        varchar(255)    not null,
+  timestamp     datetime        not null,
+  uptime        tinytext,
+  users         int,
+  loadavg       float (4,2),
+
+  primary key   (system, timestamp),
+  foreign key systemLink (system) references system (name)
+    on delete cascade
+    on update cascade
+) engine=innodb; -- loadavg
\ No newline at end of file
index 5f35bc3..07ab995 100755 (executable)
@@ -14,6 +14,16 @@ my $hostname = $ENV{HOST}     || 'localhost';
 my $username = $ENV{USERNAME};
 my $password = $ENV{PASSWORD};
 
+my $command  = $ENV{COMMAND};
+
+if (@ARGV) {
+  $command = join ' ', @ARGV;
+} else {
+  $command = 'ls /tmp' unless $command;  
+} # if
+
+print "Attempting to connect to $username\@$hostname to execute \"$command\"\n";
+
 my $remote = Rexec->new (
   host     => $hostname,
   username => $username,
@@ -24,19 +34,18 @@ my $remote = Rexec->new (
 if ($remote) {
   print "Connected to $username\@$hostname using "
       . $remote->{protocol} . " protocol\n";
-    
-  $cmd = "/bin/ls /nonexistent";
 
-  @output = $remote->execute ($cmd);
+  print "Executing command \"$command\" on $hostname as $username\n";    
+  @output = $remote->execute ($command);
   $status = $remote->status;
 
-  print "$cmd status: $status\n";
+  print "\"$command\" status: $status\n";
 
-  $remote->print_lines;
-
-  print "$_\n" foreach ($remote->execute ('cat /etc/passwd'));
+  if (@output == 0) {
+    print "No lines of output received!\n";
+  } else {
+    print "$_\n" foreach (@output);
+  } # if
 } else {
   print "Unable to connect to $username@$hostname\n";
-} # if
-
-
+} # if
\ No newline at end of file