fb63079c385888de4b13382a320543ee1c448e02
[clearscm.git] / clearadm / lib / Clearadm.pm
1 =pod
2
3 =head1 NAME $RCSfile: Clearadm.pm,v $
4
5 Object oriented interface to Clearadm.
6
7 =head1 VERSION
8
9 =over
10
11 =item Author
12
13 Andrew DeFaria <Andrew@ClearSCM.com>
14
15 =item Revision
16
17 $Revision: 1.54 $
18
19 =item Created
20
21 Tue Dec 07 09:13:27 EST 2010
22
23 =item Modified
24
25 $Date: 2012/11/09 06:43:26 $
26
27 =back
28
29 =head1 SYNOPSIS
30
31 Provides the Clearadm object which handles all interaction with the Clearadm
32 database. Similar add/change/delete/update methods for other record types. In
33 general you must orient your record hashs to have the appropriately named
34 keys that correspond to the database. Also see mothod documentation for
35 specifics about the method you are envoking.
36
37  # Create new Clearadm object
38  my $clearadm = new Clearadm;
39  
40  # Add a new system
41  my %system = (
42   name          => 'jupiter',
43   alias         => 'defaria.com',
44   admin         => 'Andrew DeFaria',
45   os            => 'Linux defaria.com 2.6.32-25-generic-pae #45-Ubuntu SMP Sat Oct 16 21:01:33 UTC 2010 i686 GNU/Linux',
46   type          => 'Linux',
47   description   => 'Home server',
48  );
49  
50  my ($err, $msg) = $clearadm->AddSystem (%system);
51  
52  # Find systems matching 'jup'
53  my @systems = $clearadm->FindSystem ('jup');
54  
55  # Get a system by name
56  my %system = $clearadm->GetSystem ('jupiter');
57  
58  # Update system
59  my %update = (
60   'region' => 'East Coast',
61  );
62
63  my ($err, $msg) = $clearadm->UpdateSystem ('jupiter', %update);
64  
65  # Delete system (Warning: will delete all related records regarding this
66  # system).
67  my ($err, $msg) = $clearadm->DeleteSystem ('jupiter');
68
69 =head1 DESCRIPTION
70
71 This package provides and object oriented interface to the Clearadm database.
72 Methods are provided to manipulate records by adding, updating and deleting 
73 them. In general you need to specify a hash which contains keys and values 
74 corresponding to the database field names and values.
75
76 =head1 ROUTINES
77
78 The following methods are available:
79
80 =cut
81
82 package Clearadm;
83
84 use strict;
85 use warnings;
86
87 use Carp;
88 use DBI;
89 use File::Basename;
90 use Net::Domain qw(hostdomain);
91
92 use FindBin;
93
94 use lib "$FindBin::Bin", "$FindBin::Bin/../../lib";
95
96 use DateUtils;
97 use Display;
98 use GetConfig;
99 use Mail;
100
101 my $conf = dirname (__FILE__) . '/../etc/clearadm.conf';
102
103 our %CLEAROPTS = GetConfig ($conf);
104
105 # Globals
106 our $VERSION  = '$Revision: 1.54 $';
107    ($VERSION) = ($VERSION =~ /\$Revision: (.*) /);
108   
109 $CLEAROPTS{CLEARADM_USERNAME} = $ENV{CLEARADM_USERNAME} 
110                               ? $ENV{CLEARADM_USERNAME}
111                               : $CLEAROPTS{CLEARADM_USERNAME}
112                               ? $CLEAROPTS{CLEARADM_USERNAME}
113                               : 'clearwriter';
114 $CLEAROPTS{CLEARADM_PASSWORD} = $ENV{CLEARADM_PASSWORD} 
115                               ? $ENV{CLEARADM_PASSWORD}
116                               : $CLEAROPTS{CLEARADM_PASSWORD}
117                               ? $CLEAROPTS{CLEARADM_PASSWORD}
118                               : 'clearwriter';
119 $CLEAROPTS{CLEARADM_SERVER}   = $ENV{CLEARADM_SERVER} 
120                               ? $ENV{CLEARADM_SERVER} 
121                               : $CLEAROPTS{CLEARADM_SERVER}
122                               ? $CLEAROPTS{CLEARADM_SERVER}
123                               : 'localhost';
124
125 my $defaultFilesystemThreshold = 90;
126 my $defaultFilesystemHist      = '6 months';
127 my $defaultLoadavgHist         = '6 months';
128
129 # Internal methods
130 sub _dberror ($$) {
131   my ($self, $msg, $statement) = @_;
132
133   my $dberr    = $self->{db}->err;
134   my $dberrmsg = $self->{db}->errstr;
135   
136   $dberr    ||= 0;
137   $dberrmsg ||= 'Success';
138
139   my $message = '';
140   
141   if ($dberr) {
142     my $function = (caller (1)) [3];
143
144     $message = "$function: $msg\nError #$dberr: $dberrmsg\n"
145              . "SQL Statement: $statement";
146   } # if
147
148   return $dberr, $message;  
149 } # _dberror
150
151 sub _formatValues (@) {
152   my ($self, @values) = @_;
153   
154   my @returnValues;
155   
156   # Quote data values
157   push @returnValues, $_ eq '' ? 'null' : $self->{db}->quote ($_)  
158     foreach (@values);
159   
160   return @returnValues;
161 } # _formatValues
162
163 sub _formatNameValues (%) {
164   my ($self, %rec) = @_;
165   
166   my @nameValueStrs;
167   
168   push @nameValueStrs, "$_=" . $self->{db}->quote ($rec{$_})
169     foreach (keys %rec);
170     
171   return @nameValueStrs;
172 } # _formatNameValues
173
174 sub _addRecord ($%) {
175   my ($self, $table, %rec) = @_;
176   
177   my $statement  = "insert into $table (";
178      $statement .= join ',', keys %rec;
179      $statement .= ') values (';
180      $statement .= join ',', $self->_formatValues (values %rec);
181      $statement .= ')';
182   
183   my ($err, $msg);
184   
185   $self->{db}->do ($statement);
186   
187   return $self->_dberror ("Unable to add record to $table", $statement);
188 } # _addRecord
189
190 sub _deleteRecord ($;$) {
191   my ($self, $table, $condition) = @_;
192   
193   my $count;
194   
195   my $statement  = "select count(*) from $table ";
196      $statement .= "where $condition"
197       if $condition;
198   
199   my $sth = $self->{db}->prepare ($statement)
200     or return $self->_dberror ('Unable to prepare statement', $statement);
201     
202   $sth->execute
203     or return $self->_dberror ('Unable to execute statement', $statement);
204     
205   my @row = $sth->fetchrow_array;
206   
207   $sth->finish;
208   
209   if ($row[0]) {
210     $count = $row[0];
211   } else {
212     $count = 0;
213   } # if
214   
215   return ($count, 'Records deleted')
216     if $count == 0;
217     
218   $statement  = "delete from $table ";
219   $statement .= "where $condition"
220     if $condition;
221   
222   $self->{db}->do ($statement);
223   
224   if ($self->{db}->err) {
225     return $self->_dberror ("Unable to delete record from $table", $statement);
226   } else {
227     return $count, 'Records deleted';
228   } # if
229 } # _deleteRecord
230
231 sub _updateRecord ($$%) {
232   my ($self, $table, $condition, %rec) = @_;
233
234   my $statement  = "update $table set ";
235      $statement .= join ',', $self->_formatNameValues (%rec);
236      $statement .= " where $condition"
237        if $condition;
238   
239   $self->{db}->do ($statement);
240   
241   return $self->_dberror ("Unable to update record in $table", $statement);
242 } # _updateRecord
243
244 sub _checkRequiredFields ($$) {
245   my ($fields, $rec) = @_;
246   
247   foreach my $fieldname (@$fields) {
248     my $found = 0;
249     
250     foreach (keys %$rec) {
251       if ($fieldname eq $_) {
252          $found = 1;
253          last;
254       } # if
255     } # foreach
256     
257     return "$fieldname is required"
258       unless $found;
259   } # foreach
260   
261   return;
262 } # _checkRequiredFields
263
264 sub _getRecords ($$) {
265   my ($self, $table, $condition) = @_;
266   
267   my ($err, $msg);
268     
269   my $statement = "select * from $table where $condition";
270   
271   my $sth = $self->{db}->prepare ($statement);
272   
273   unless ($sth) {
274     ($err, $msg) = $self->_dberror ('Unable to prepare statement', $statement);
275     
276     croak $msg;
277   } # if
278
279   my $attempts    = 0;
280   my $maxAttempts = 3;
281   my $sleepTime   = 30;
282   my $status;
283   
284   # We've been having the server going away. Supposedly it should reconnect so
285   # here we simply retry up to $maxAttempts times to re-execute the statement. 
286   # (Are there other places where we need to do this?)
287   $err = 2006;
288   
289   while ($err == 2006 and $attempts++ < $maxAttempts) {
290     $status = $sth->execute;
291     
292     if ($status) {
293       $err = 0;
294       last;
295     } else {
296       ($err, $msg) = $self->_dberror ('Unable to execute statement',
297                                       $statement);
298     } # if
299     
300     last if $err == 0;
301     
302     croak $msg unless $err == 2006;
303
304     my $timestamp = YMDHMS;
305       
306     $self->Error ("$timestamp: Unable to talk to DB server.\n\n$msg\n\n"
307                 . "Will try again in $sleepTime seconds", -1);
308                 
309     # Try to reconnect
310     $self->_connect ($self->{dbserver});
311
312     sleep $sleepTime;
313   } # while
314
315   $self->Error ("After $maxAttempts attempts I could not connect to the database", $err)
316     if ($err == 2006 and $attempts > $maxAttempts);
317   
318   my @records;
319   
320   while (my $row = $sth->fetchrow_hashref) {
321     push @records, $row;
322   } # while
323   
324   return @records;
325 } # _getRecord
326
327 sub _aliasSystem ($) {
328   my ($self, $system) = @_;
329   
330   my %system = $self->GetSystem ($system);
331   
332   if ($system{name}) {
333     return $system{name};
334   } else {
335         return;
336   } # if
337 } # _aliasSystem
338
339 sub _getLastID () {
340   my ($self) = @_;
341   
342   my $statement = 'select last_insert_id()';
343   
344   my $sth = $self->{db}->prepare ($statement);
345   
346   my ($err, $msg);
347   
348   unless ($sth) {
349     ($err, $msg) = $self->_dberror ('Unable to prepare statement', $statement);
350     
351     croak $msg;
352   } # if
353     
354   my $status = $sth->execute;
355   
356   unless ($status) {
357     ($err, $msg) = $self->_dberror ('Unable to execute statement', $statement);
358     
359     croak $msg;
360   } # if
361     
362   my @records;
363
364   my @row = $sth->fetchrow_array;
365   
366   return $row[0];
367 } # _getLastID
368
369 sub _connect (;$) {
370   my ($self, $dbserver) = @_;
371   
372   $dbserver ||= $CLEAROPTS{CLEARADM_SERVER};
373   
374   my $dbname   = 'clearadm';
375   my $dbdriver = 'mysql';
376
377   $self->{db} = DBI->connect (
378     "DBI:$dbdriver:$dbname:$dbserver", 
379     $CLEAROPTS{CLEARADM_USERNAME},
380     $CLEAROPTS{CLEARADM_PASSWORD},
381     {PrintError => 0},
382   ) or croak (
383     "Couldn't connect to $dbname database " 
384   . "as $CLEAROPTS{CLEARADM_USERNAME}\@$CLEAROPTS{CLEARADM_SERVER}"
385   );
386   
387   $self->{dbserver} = $dbserver;
388   
389   return;
390 } # _connect
391
392 sub new (;$) {
393   my ($class, $dbserver) = @_;
394
395   my $self = bless {}, $class;
396
397   $self->_connect ($dbserver);
398
399   return $self;
400 } # new
401
402 sub SetNotify () {
403   my ($self) = @_;
404   
405   $self->{NOTIFY} = $CLEAROPTS{CLEARADM_NOTIFY};
406   
407   return;
408 } # SetNotify
409
410 sub Error ($;$) {
411   my ($self, $msg, $errno) = @_;
412
413   # If $errno is specified we need to stop. However we need to notify somebody
414   # that cleartasks is no longer running.  
415   error $msg;
416   
417   if ($errno) {
418     if ($self->{NOTIFY}) {
419       mail (
420         to      => $self->{NOTIFY},
421         subject => 'Internal error occurred in Clearadm',
422         data    => "<p>An unexpected, internal error occurred in Clearadm:</p><p>$msg</p>",
423         mode    => 'html',
424       );
425     
426       exit $errno  if $errno > 0;
427     } # if
428   } # if
429   
430   return;
431 } # Error
432
433 sub AddSystem (%) {
434   my ($self, %system) = @_;
435   
436   my @requiredFields = (
437     'name',
438   );
439
440   my $result = _checkRequiredFields \@requiredFields, \%system;
441   
442   return -1, "AddSystem: $result"
443     if $result;
444   
445   $system{loadavgHist} ||= $defaultLoadavgHist;
446   
447   return $self->_addRecord ('system', %system);
448 } # AddSystem
449
450 sub DeleteSystem ($) {
451   my ($self, $name) = @_;
452
453   return $self->_deleteRecord ('system', "name='$name'");  
454 } # DeleteSystem
455
456 sub UpdateSystem ($%) {
457   my ($self, $name, %update) = @_;
458
459   return $self->_updateRecord ('system', "name='$name'", %update);
460 } # UpdateSystem
461
462 sub GetSystem ($) {
463   my ($self, $system) = @_;
464   
465   return
466     unless $system;
467   
468   my @records = $self->_getRecords (
469     'system', 
470     "name='$system' or alias like '%$system%'"
471   );
472   
473   if ($records[0]) {
474     return %{$records[0]};
475   } else {
476         return;
477   } # if
478 } # GetSystem
479
480 sub FindSystem (;$) {
481   my ($self, $system) = @_;
482
483   $system ||= '';
484   
485   my $condition = "name like '%$system%' or alias like '%$system%'";
486                          
487   return $self->_getRecords ('system', $condition);
488 } # FindSystem
489
490 sub SearchSystem (;$) {\r
491   my ($self, $condition) = @_;
492   
493   $condition = "name like '%'" unless $condition;
494   
495   return $self->_getRecords ('system', $condition); \r
496 } # SearchSystem
497
498 sub AddPackage (%) {
499   my ($self, %package) = @_;
500   
501   my @requiredFields = (
502     'system',
503     'name',
504     'version'
505   );
506
507   my $result = _checkRequiredFields \@requiredFields, \%package;
508   
509   return -1, "AddPackage: $result"
510     if $result;
511   
512   return $self->_addRecord ('package', %package);
513 } # AddPackage
514
515 sub DeletePackage ($$) {
516   my ($self, $system, $name) = @_;
517   
518   return $self->_deleteRecord (
519     'package', 
520     "(system='$system' or alias='$system') and name='$name'");
521 } # DeletePackage
522
523 sub UpdatePackage ($$%) {
524   my ($self, $system, $name, %update) = @_;
525   
526   $system = $self->_aliasSystem ($system);
527   
528   return
529     unless $system;
530     
531   return $self->_updateRecord ('package', "system='$system'", %update);
532 } # UpdatePackage
533
534 sub GetPackage($$) {
535   my ($self, $system, $name) = @_;
536   
537   $system = $self->_aliasSystem ($system);
538   
539   return
540     unless $system;
541     
542   return
543     unless $name;
544     
545   my @records = $self->_getRecords (
546     'package', 
547     "system='$system' and name='$name'"
548   );
549   
550   if ($records[0]) {
551     return %{$records[0]};
552   } else {
553         return;
554   } # if
555 } # GetPackage
556
557 sub FindPackage ($;$) {
558   my ($self, $system, $name) = @_;
559
560   $name ||= '';
561
562   $system = $self->_aliasSystem ($system);
563   
564   return
565     unless $system;
566     
567   my $condition = "system='$system' and name like '%$name%'";
568   
569   return $self->_getRecords ('package', $condition);
570 } # FindPackage
571
572 sub AddFilesystem (%) {
573   my ($self, %filesystem) = @_;
574   
575   my @requiredFields = (
576     'system',
577     'filesystem',
578     'fstype'
579   );
580
581   my $result = _checkRequiredFields \@requiredFields, \%filesystem;
582   
583   return -1, "AddFilesystem: $result"
584     if $result;
585     
586   # Default filesystem threshold
587   $filesystem{threshold} ||= $defaultFilesystemThreshold;
588   
589   return $self->_addRecord ('filesystem', %filesystem);
590 } # AddFilesystem
591
592 sub DeleteFilesystem ($$) {
593   my ($self, $system, $filesystem) = @_;
594   
595   $system = $self->_aliasSystem ($system);
596   
597   return
598     unless $system;
599     
600   return $self->_deleteRecord (
601     'filesystem', 
602     "system='$system' and filesystem='$filesystem'"
603   );
604 } # DeleteFilesystem
605
606 sub UpdateFilesystem ($$%) {
607   my ($self, $system, $filesystem, %update) = @_;
608   
609   $system = $self->_aliasSystem ($system);
610   
611   return
612     unless $system;
613     
614   return $self->_updateRecord (
615     'filesystem',
616     "system='$system' and filesystem='$filesystem'",
617     %update
618   );
619 } # UpdateFilesystem
620
621 sub GetFilesystem ($$) {
622   my ($self, $system, $filesystem) = @_;
623   
624   $system = $self->_aliasSystem ($system);
625   
626   return
627     unless $system;
628     
629   return
630     unless $filesystem;
631     
632   my @records = $self->_getRecords (
633     'filesystem', 
634     "system='$system' and filesystem='$filesystem'"
635   );
636   
637   if ($records[0]) {
638     return %{$records[0]};
639   } else {
640     return;
641   } # if
642 } # GetFilesystem
643
644 sub FindFilesystem ($;$) {
645   my ($self, $system, $filesystem) = @_;
646   
647   $filesystem ||= '';
648
649   $system = $self->_aliasSystem ($system);
650   
651   return
652     unless $system;
653
654   my $condition = "system='$system' and filesystem like '%$filesystem%'";     
655       
656   return $self->_getRecords ('filesystem', $condition);
657 } # FindFilesystem
658
659 sub AddVob (%) {
660   my ($self, %vob) = @_;
661   
662   my @requiredFields = (
663     'system',
664     'tag',
665   );
666
667   my $result = _checkRequiredFields \@requiredFields, \%vob;
668   
669   return -1, "AddVob: $result"
670     if $result;
671   
672   return $self->_addRecord ('vob', %vob);
673 } # AddVob
674
675 sub DeleteVob ($) {
676   my ($self, $tag) = @_;
677   
678   return $self->_deleteRecord ('vob', "tag='$tag'");
679 } # DeleteVob
680
681 sub GetVob ($) {
682   my ($self, $tag) = @_;
683   
684   return 
685     unless $tag;
686     
687   my @records = $self->_getRecords ('vob', "tag='$tag'");
688   
689   if ($records[0]) {
690     return %{$records[0]};
691   } else {
692         return;
693   } # if
694 } # GetVob
695
696 sub FindVob ($) {
697   my ($self, $tag) = @_;
698   
699   return $self->_getRecords ('vob', "tag like '%$tag%'");
700 } # FindVob
701
702 sub AddView (%) {
703   my ($self, %view) = @_;
704   
705   my @requiredFields = (
706     'system',
707     'tag',
708   );
709
710   my $result = _checkRequiredFields \@requiredFields, \%view;
711   
712   return -1, "AddView: $result"
713     if $result;
714   
715   return $self->_addRecord ('view', %view);
716 } # AddView
717
718 sub DeleteView ($) {
719   my ($self, $tag) = @_;
720   
721   return $self->_deleteRecord ('vob', "tag='$tag'");
722 } # DeleteView
723
724 sub GetView ($) {
725   my ($self, $tag) = @_;
726   
727   return
728     unless $tag;
729   
730   my @records = $self->_getRecords ('view', "tag='$tag'");
731   
732   if ($records[0]) {
733     return %{$records[0]};
734   } else {
735         return;
736   } # if
737 } # GetView
738
739 sub FindView (;$$$$) {
740   my ($self, $system, $region, $tag, $ownerName) = @_;
741
742   $system    ||= '';
743   $region    ||= '';
744   $tag       ||= '';
745   $ownerName ||= '';
746   
747   my $condition;
748   
749   $condition  = "system like '%$system%'";
750   $condition .= ' and ';
751   $condition  = "region like '%$region%'";
752   $condition .= ' and ';
753   $condition .= "tag like '%$tag'";
754   $condition .= ' and ';
755   $condition .= "ownerName like '%$ownerName'";
756                          
757   return $self->_getRecords ('view', $condition);
758 } # FindView
759
760 sub AddFS (%) {
761   my ($self, %fs) = @_;
762   
763   my @requiredFields = (
764     'system',
765     'filesystem',
766   );
767
768   my $result = _checkRequiredFields \@requiredFields, \%fs;
769   
770   return -1, "AddFS: $result"
771     if $result;
772   
773   # Timestamp record
774   $fs{timestamp} = Today2SQLDatetime;
775   
776   return $self->_addRecord ('fs', %fs);
777 } # AddFS
778
779 sub TrimFS ($$) {
780   my ($self, $system, $filesystem) = @_;
781   
782   my %filesystem = $self->GetFilesystem ($system, $filesystem);
783   
784   return
785     unless %filesystem;
786    
787   my %task = $self->GetTask ('scrub');
788   
789   $self->Error ("Unable to find scrub task!", 1) unless %task;
790    
791   my $days;
792   my $today = Today2SQLDatetime;
793   
794   # TODO: SubtractDays uses just an approximation (i.e. subtracting 30 days when
795   # in February is not right.
796   if ($filesystem{filesystemHist} =~ /(\d+) month/i) {
797     $days = $1 * 30;
798   } elsif ($filesystem{filesystemHist} =~ /(\d+) year/i) {
799     $days = $1 * 365;
800   } # if
801
802   my $oldage = SubtractDays $today, $days;
803   
804   my ($dberr, $dbmsg) = $self->_deleteRecord (
805     'fs',
806     "system='$system' and filesystem='$filesystem' and timestamp<='$oldage'"
807   );
808   
809   if ($dbmsg eq 'Records deleted') {
810     return (0, $dbmsg)
811       if $dberr == 0;
812       
813     my %runlog;
814     
815     $runlog{task}    = $task{name};
816     $runlog{started} = $today;
817     $runlog{status}  = 0;
818     $runlog{message} = 
819       "Scrubbed $dberr fs records for filesystem $system:$filesystem";
820     
821     my ($err, $msg) = $self->AddRunlog (%runlog);
822     
823     $self->Error ("Unable to add runlog - (Error: $err)\n$msg") if $err;
824   } # if
825   
826   return ($dberr, $dbmsg);
827 } # TrimFS
828
829 sub TrimLoadavg ($) {
830   my ($self, $system) = @_;
831   
832   my %system = $self->GetSystem ($system);
833   
834   return
835     unless %system;
836     
837   my %task = $self->GetTask ('loadavg');
838   
839   $self->Error ("Unable to find loadavg task!", 1) unless %task;
840    
841   my $days;
842   my $today = Today2SQLDatetime;
843
844   # TODO: SubtractDays uses just an approximation (i.e. subtracting 30 days when
845   # in February is not right.
846   if ($system{loadavgHist} =~ /(\d+) month/i) {
847     $days = $1 * 30;
848   } elsif ($system{loadavgHist} =~ /(\d+) year/i) {
849     $days = $1 * 365;
850   } # if
851
852   my $oldage = SubtractDays $today, $days;
853   
854   my ($dberr, $dbmsg) = $self->_deleteRecord (
855     'loadavg',
856     "system='$system' and timestamp<='$oldage'"
857   );
858   
859   if ($dbmsg eq 'Records deleted') {
860     return (0, $dbmsg)
861       if $dberr == 0;
862       
863     my %runlog;
864     
865     $runlog{task}    = $task{name};
866     $runlog{started} = $today;
867     $runlog{status}  = 0;
868     $runlog{message} = 
869       "Scrubbed $dberr loadavg records for system $system";
870
871     my ($err, $msg) = $self->AddRunlog (%runlog);
872     
873     $self->Error ("Unable to add runload (Error: $err)\n$msg") if $err;
874   } # if
875
876   return ($dberr, $dbmsg);
877 } # TrimLoadavg
878
879 sub GetFS ($$;$$$$) {
880   my ($self, $system, $filesystem, $start, $end, $count, $interval) = @_;
881   
882   $system = $self->_aliasSystem ($system);
883   
884   return
885     unless $system;
886     
887   return
888     unless $filesystem;
889     
890   $interval ||= 'Minute';
891   
892   my $size = $interval =~ /month/i
893            ? 7
894            : $interval =~ /day/i
895            ? 10
896            : $interval =~ /hour/i
897            ? 13
898            : 16;
899     
900   undef $start if $start and $start =~ /earliest/i;
901   undef $end   if $end   and $end   =~ /latest/i;
902   
903   my $condition  = "system='$system' and filesystem='$filesystem'";
904      $condition .= " and timestamp>='$start'" if $start;
905      $condition .= " and timestamp<='$end'"   if $end;
906      
907      $condition .= " group by left(timestamp,$size)";
908   
909   if ($count) {
910     # We can't simply do a "limit 0, $count" as that just gets the front end of
911     # the records return (i.e. if $count = say 10 and the timestamp range
912     # returns 40 rows we'll see only rows 1-10, not rows 31-40). We need limit
913     # $offset, $count where $offset = the number of qualifying records minus
914     # $count
915     my $nbrRecs = $self->Count ('fs', $condition);
916     my $offset  = $nbrRecs - $count;
917   
918     # Offsets of < 0 are not allowed.
919     $offset = 0
920       if $offset < 0;
921
922     $condition .= " limit $offset, $count";
923   } # if
924        
925   my $statement = <<"END";
926 select
927   system,
928   filesystem,
929   mount,
930   left(timestamp,$size) as timestamp,
931   avg(size) as size,
932   avg(used) as used,
933   avg(free) as free,
934   reserve
935 from
936   fs
937   where $condition
938 END
939
940   my ($err, $msg);
941   
942   my $sth = $self->{db}->prepare ($statement);
943   
944   unless ($sth) {
945     ($err, $msg) = $self->_dberror ('Unable to prepare statement', $statement);
946     
947     croak $msg;
948   } # if
949     
950   my $status = $sth->execute;
951   
952   unless ($status) {
953     ($err, $msg) = $self->_dberror ('Unable to execute statement', $statement);
954     
955     croak $msg;
956   } # if
957     
958   my @records;
959   
960   while (my $row = $sth->fetchrow_hashref) {
961     push @records, $row;
962   } # while
963   
964   return @records;
965 } # GetFS
966
967 sub GetLatestFS ($$) {
968   my ($self, $system, $filesystem) = @_;
969   
970   $system = $self->_aliasSystem ($system);
971   
972   return
973     unless $system;
974     
975   return
976     unless $filesystem;
977     
978   my @records = $self->_getRecords (
979     'fs',
980     "system='$system' and filesystem='$filesystem'"
981   . " order by timestamp desc limit 0, 1",
982   );
983   
984   if ($records[0]) {
985         return %{$records[0]};
986   } else {
987         return;
988   } # if
989 } # GetLatestFS
990
991 sub AddLoadavg () {
992   my ($self, %loadavg) = @_;
993   
994   my @requiredFields = (
995     'system',
996   );
997
998   my $result = _checkRequiredFields \@requiredFields, \%loadavg;
999   
1000   return -1, "AddLoadavg: $result"
1001     if $result;
1002   
1003   # Timestamp record
1004   $loadavg{timestamp} = Today2SQLDatetime;
1005   
1006   return $self->_addRecord ('loadavg', %loadavg);
1007 } # AddLoadavg
1008
1009 sub GetLoadavg ($;$$$$) {
1010   my ($self, $system, $start, $end, $count, $interval) = @_;
1011            
1012   $system = $self->_aliasSystem ($system);
1013   
1014   return
1015     unless $system;
1016     
1017   $interval ||= 'Minute';
1018   
1019   my $size = $interval =~ /month/i
1020            ? 7
1021            : $interval =~ /day/i
1022            ? 10
1023            : $interval =~ /hour/i
1024            ? 13
1025            : 16;
1026     
1027   my $condition;
1028   
1029   undef $start if $start and $start =~ /earliest/i;
1030   undef $end   if $end   and $end   =~ /latest/i;
1031   
1032   $condition .= " system='$system'"        if $system;
1033   $condition .= " and timestamp>='$start'" if $start;
1034   $condition .= " and timestamp<='$end'"   if $end;
1035   
1036   $condition .= " group by left(timestamp,$size)";
1037
1038   if ($count) {
1039     # We can't simply do a "limit 0, $count" as that just gets the front end of
1040     # the records return (i.e. if $count = say 10 and the timestamp range
1041     # returns 40 rows we'll see only rows 1-10, not rows 31-40). We need limit
1042     # $offset, $count where $offset = the number of qualifying records minus
1043     # $count
1044     my $nbrRecs = $self->Count ('loadavg', $condition);
1045     my $offset  = $nbrRecs - $count;
1046   
1047     # Offsets of < 0 are not allowed.
1048     $offset = 0
1049       if $offset < 0;
1050
1051     $condition .= " limit $offset, $count";
1052   } # if
1053         
1054   my $statement = <<"END";
1055 select
1056   system,
1057   left(timestamp,$size) as timestamp,
1058   uptime,
1059   users,
1060   avg(loadavg) as loadavg
1061 from
1062   loadavg
1063   where $condition
1064 END
1065
1066   my ($err, $msg);
1067   
1068   my $sth = $self->{db}->prepare ($statement);
1069   
1070   unless ($sth) {
1071     ($err, $msg) = $self->_dberror ('Unable to prepare statement', $statement);
1072     
1073     croak $msg;
1074   } # if
1075     
1076   my $status = $sth->execute;
1077   
1078   unless ($status) {
1079     ($err, $msg) = $self->_dberror ('Unable to execute statement', $statement);
1080     
1081     croak $msg;
1082   } # if
1083     
1084   my @records;
1085   
1086   while (my $row = $sth->fetchrow_hashref) {
1087     push @records, $row;
1088   } # while
1089   
1090   return @records;
1091 } # GetLoadvg
1092
1093 sub GetLatestLoadavg ($) {
1094   my ($self, $system) = @_;
1095   
1096   $system = $self->_aliasSystem ($system);
1097   
1098   return
1099     unless $system;
1100     
1101   my @records = $self->_getRecords (
1102     'loadavg',
1103     "system='$system'"
1104   . " order by timestamp desc limit 0, 1",
1105   );
1106   
1107   if ($records[0]) {
1108     return %{$records[0]};
1109   } else {
1110     return;
1111   } # if
1112 } # GetLatestLoadavg
1113
1114 sub AddTask (%) {
1115   my ($self, %task) = @_;
1116   
1117   my @requiredFields = (
1118     'name',
1119     'command'
1120   );
1121   
1122   my $result = _checkRequiredFields \@requiredFields, \%task;
1123   
1124   return -1, "AddTask: $result"
1125     if $result;
1126   
1127   return $self->_addRecord ('task', %task);    
1128 } # AddTask
1129
1130 sub DeleteTask ($) {
1131   my ($self, $name) = @_;
1132   
1133   return $self->_deleteRecord ('task', "name='$name'");
1134 } # DeleteTask
1135
1136 sub FindTask ($) {
1137   my ($self, $name) = @_;
1138   
1139   $name ||= '';
1140   
1141   my $condition = "name like '%$name%'";
1142                 
1143   return $self->_getRecords ('task', $condition);
1144 } # FindTask
1145
1146 sub GetTask ($) {
1147   my ($self, $name) = @_;
1148   
1149   return
1150     unless $name;
1151   
1152   my @records = $self->_getRecords ('task', "name='$name'");
1153
1154   if ($records[0]) {
1155     return %{$records[0]};
1156   } else {
1157     return;
1158   } # if  
1159 } # GetTask
1160
1161 sub UpdateTask ($%) {
1162   my ($self, $name, %update) = @_;
1163   
1164   return $self->_updateRecord ('task', "name='$name'", %update);
1165 } # Update
1166
1167 sub AddSchedule (%) {
1168   my ($self, %schedule) = @_;
1169   
1170   my @requiredFields = (
1171     'task',
1172   );
1173   
1174   my $result = _checkRequiredFields \@requiredFields, \%schedule;
1175   
1176   return -1, "AddSchedule: $result"
1177     if $result;
1178   
1179   return $self->_addRecord ('schedule', %schedule);    
1180 } # AddSchedule
1181
1182 sub DeleteSchedule ($) {
1183   my ($self, $name) = @_;
1184   
1185   return $self->_deleteRecord ('schedule', "name='$name'");
1186 } # DeleteSchedule
1187
1188 sub FindSchedule (;$$) {
1189   my ($self, $name, $task) = @_;
1190   
1191   $name ||= '';
1192   $task||= '';
1193   
1194   my $condition  = "name like '%$name%'";
1195      $condition .= ' and ';
1196      $condition .= "task like '%$task%'";
1197
1198   return $self->_getRecords ('schedule', $condition); 
1199 } # FindSchedule
1200
1201 sub GetSchedule ($) {
1202   my ($self, $name) = @_;
1203   
1204   my @records = $self->_getRecords ('schedule', "name='$name'");
1205   
1206   if ($records[0]) {
1207     return %{$records[0]};
1208   } else {
1209     return;
1210   } # if  
1211 } # GetSchedule
1212
1213 sub UpdateSchedule ($%) {
1214   my ($self, $name, %update) = @_;
1215   
1216   return $self->_updateRecord ('schedule', "name='$name'", %update);
1217 } # UpdateSchedule
1218
1219 sub AddRunlog (%) {
1220   my ($self, %runlog) = @_;
1221   
1222   my @requiredFields = (
1223     'task',
1224   );
1225   
1226   my $result = _checkRequiredFields \@requiredFields, \%runlog;
1227   
1228   return -1, "AddRunlog: $result"
1229     if $result;
1230   
1231   $runlog{ended} = Today2SQLDatetime;
1232   
1233   my ($err, $msg) = $self->_addRecord ('runlog', %runlog);
1234
1235   return ($err, $msg, $self->_getLastID);
1236 } # AddRunlog
1237
1238 sub DeleteRunlog ($) {
1239   my ($self, $condition) = @_;
1240   
1241   return $self->_deleteRecord ('runlog', $condition);
1242 } # DeleteRunlog
1243
1244 sub FindRunlog (;$$$$$$) {
1245   my ($self, $task, $system, $status, $id, $start, $page) = @_;
1246   
1247   $task ||= '';
1248   
1249   # If ID is specified then that's all that really matters as it uniquely
1250   # identifies a runlog entry;
1251   my $condition;
1252   
1253   unless ($id) {
1254     $condition  = "task like '%$task%'";
1255     
1256     if ($system) {
1257       $condition .= " and system like '%$system%'"
1258         unless $system eq 'All';
1259     } else {
1260       $condition .= ' and system is null';
1261     } # unless
1262         
1263     if (defined $status) {
1264       if ($status =~ /!(-*\d+)/) {
1265         $condition .= " and status<>$1";
1266       } else {
1267         $condition .= " and status=$status"
1268       } # if
1269     } # if
1270     
1271     $condition .= " order by started desc"; 
1272     
1273     if (defined $start) {
1274       $page ||= 10;
1275       $condition .= " limit $start, $page";
1276     } # unless
1277   } else {
1278     $condition = "id=$id";
1279   } # unless
1280   
1281   return $self->_getRecords ('runlog', $condition);
1282 } # FindRunlog
1283
1284 sub GetRunlog ($) {
1285   my ($self, $id) = @_;
1286   
1287   return
1288     unless $id;
1289   
1290   my @records = $self->_getRecords ('runlog', "id=$id");
1291   
1292   if ($records[0]) {
1293     return %{$records[0]};
1294   } else {
1295     return;
1296   } # if  
1297 } # GetRunlog
1298
1299 sub UpdateRunlog ($%) {
1300   my ($self, $id, %update) = @_;
1301   
1302   return $self->_updateRecord ('runlog', "id=$id", %update);
1303 } # UpdateRunlog
1304
1305 sub Count ($;$) {
1306   my ($self, $table, $condition) = @_;
1307   
1308   $condition = $condition ? 'where ' . $condition : '';
1309     
1310   my ($err, $msg);
1311   
1312   my $statement = "select count(*) from $table $condition";
1313   
1314   my $sth = $self->{db}->prepare ($statement);
1315   
1316   unless ($sth) {
1317     ($err, $msg) = $self->_dberror ('Unable to prepare statement', $statement);
1318     
1319     croak $msg;
1320   } # if
1321     
1322   my $status = $sth->execute;
1323   
1324   unless ($status) {
1325     ($err, $msg) = $self->_dberror ('Unable to execute statement', $statement);
1326     
1327     croak $msg;
1328   } # if
1329     
1330   # Hack! Statements such as the following:
1331   #
1332   # select count(*) from fs where system='jupiter' and filesystem='/dev/sdb5'
1333   # > group by left(timestamp,10);                    
1334   # +----------+
1335   # | count(*) |
1336   # +----------+
1337   # |       49 |
1338   # |       98 |
1339   # |      140 |
1340   # |        7 |
1341   # |       74 |
1342   # |      124 |
1343   # |      190 |
1344   # +----------+
1345   # 7 rows in set (0.00 sec)
1346   # 
1347   # Here we want 7 but what we see in $records[0] is 49. So the hack is that if
1348   # statement contains "group by" then we assume we have the above and return
1349   # scalar @records, otherwise we return $records[0];
1350   if ($statement =~ /group by/i) {
1351     my $allrows = $sth->fetchall_arrayref;
1352
1353     return scalar @{$allrows};
1354   } else {
1355     my @records = $sth->fetchrow_array;
1356
1357     return $records[0];
1358   } # if
1359 } # Count
1360
1361 # GetWork returns two items, the number of seconds to wait before the next task
1362 # and array of hash records of work to be done immediately. The caller should
1363 # execute the work to be done, timing it, and subtracting it from the $sleep
1364 # time returned. If the caller exhausts the $sleep time then they should call
1365 # us again.
1366 sub GetWork () {
1367   my ($self) = @_;
1368   
1369   my ($err, $msg);
1370   
1371   my $statement = <<"END";
1372 select
1373   schedule.name as schedulename,
1374   task.name,
1375   task.system as system,
1376   task.command,
1377   schedule.notification,
1378   frequency,
1379   runlog.started as lastrun
1380 from
1381   task,
1382   schedule left join runlog on schedule.lastrunid=runlog.id
1383 where
1384       schedule.task=task.name
1385   and schedule.active='true'
1386 order by lastrun
1387 END
1388      
1389   my $sth = $self->{db}->prepare ($statement);
1390   
1391   unless ($sth) {
1392     ($err, $msg) = $self->_dberror ('Unable to prepare statement', $statement);
1393     
1394     croak $msg;
1395   } # if
1396     
1397   my $status = $sth->execute;
1398   
1399   unless ($status) {
1400     ($err, $msg) = $self->_dberror ('Unable to execute statement', $statement);
1401     
1402     croak $msg;
1403   } # if
1404   
1405   my $sleep;  
1406   my @records;
1407   
1408   while (my $row = $sth->fetchrow_hashref) {
1409    if ($$row{system} !~ /localhost/i) {
1410      my %system = $self->GetSystem ($$row{system});
1411     
1412      # Skip inactive systems
1413      next if $system{active} eq 'false';
1414    } # if
1415     
1416     # If started is not defined then this task was never run so run it now.
1417     unless ($$row{lastrun}) {
1418       push @records, $row;
1419       next;
1420     } # unless
1421     
1422     # TODO: Handle frequencies better.
1423     my $seconds;
1424     
1425     if ($$row{frequency} =~ /(\d+) seconds/i) {
1426       $seconds = $1;
1427     } elsif ($$row{frequency} =~ /(\d+) minute/i) {
1428       $seconds = $1 * 60;
1429     } elsif ($$row{frequency} =~ /(\d+) hour/i) {
1430       $seconds = $1 * 60 * 60;
1431     } elsif ($$row{frequency} =~ /(\d+) day/i) {
1432       $seconds= $1 * 60 * 60 * 24;
1433     } else {
1434       warning "Don't know how to handle frequencies like $$row{frequency}";
1435       next;
1436     } # if
1437     
1438     my $today    = Today2SQLDatetime;
1439     my $lastrun  = Add ($$row{lastrun}, (seconds => $seconds));
1440     my $waitTime = DateToEpoch ($lastrun) - DateToEpoch ($today);
1441       
1442     if ($waitTime < 0) {
1443       # We're late - push this onto records and move on
1444       push @records, $row;
1445     } # if
1446     
1447     $sleep ||= $waitTime;
1448     
1449     if ($sleep > $waitTime) {
1450       $sleep = $waitTime;
1451     } # if
1452   } # while
1453   
1454   # Even if there is nothing to do the caller should sleep a bit and come back
1455   # to us. So if it ends up there's nothing past due, and nothing upcoming, then
1456   # sleep for a minute and return here. Somebody may have added a new task next
1457   # time we're called.
1458   if (@records == 0 and not $sleep) {
1459     $sleep = 60;
1460   } # if
1461   
1462   return ($sleep, @records);  
1463 } # GetWork
1464
1465 sub GetUniqueList ($$) {
1466   my ($self, $table, $field) = @_;
1467   
1468   my ($err, $msg);
1469   
1470   my $statement = "select $field from $table group by $field";
1471   
1472   my $sth = $self->{db}->prepare ($statement);
1473   
1474   unless ($sth) {
1475     ($err, $msg) = $self->_dberror ('Unable to prepare statement', $statement);
1476     
1477     croak $msg;
1478   } # if
1479     
1480   my $status = $sth->execute;
1481   
1482   unless ($status) {
1483     ($err, $msg) = $self->_dberror ('Unable to execute statement', $statement);
1484     
1485     croak $msg;
1486   } # if
1487
1488   my @values;
1489   
1490   while (my @row = $sth->fetchrow_array) {
1491     if ($row[0]) {
1492       push @values, $row[0];
1493     } else {
1494       push @values, '<NULL>';
1495     } # if
1496   } # foreach
1497
1498   return @values;
1499 } # GetUniqueList
1500
1501 sub AddAlert(%) {
1502   my ($self, %alert) = @_;
1503   
1504   my @requiredFields = (
1505     'name',
1506     'type',
1507   );
1508   
1509   my $result = _checkRequiredFields \@requiredFields, \%alert;
1510   
1511   return -1, "AddAlert: $result"
1512     if $result;
1513   
1514   return $self->_addRecord ('alert', %alert);  
1515 } # AddAlert
1516
1517 sub DeleteAlert ($) {
1518   my ($self, $name) = @_;
1519   
1520   return $self->_deleteRecord ('alert', "name='$name'");
1521 } # DeleteAlert
1522
1523 sub FindAlert (;$) {
1524   my ($self, $alert) = @_;
1525   
1526   $alert ||= '';
1527   
1528   my $condition = "name like '%$alert%'";
1529     
1530   return $self->_getRecords ('alert', $condition);                
1531 } # FindAlert
1532
1533 sub GetAlert ($) {
1534   my ($self, $name) = @_;
1535   
1536   return
1537     unless $name;
1538   
1539   my @records = $self->_getRecords ('alert', "name='$name'");
1540   
1541   if ($records[0]) {
1542     return %{$records[0]};
1543   } else {
1544     return;
1545   } # if  
1546 } # GetAlert
1547
1548 sub SendAlert ($$$$$$$) {
1549   my (
1550     $self,
1551     $alert,
1552     $system,
1553     $notification,
1554     $subject,
1555     $message,
1556     $to,
1557     $runlogID,
1558   ) = @_;
1559   
1560   my $footing  = '<hr><p style="text-align: center;">';
1561      $footing .= '<font color="#bbbbbb">';
1562   my $year     = (localtime)[5] + 1900;
1563      $footing .= "<a href='$CLEAROPTS{CLEARADM_WEBBASE}'>Clearadm</a><br>"; 
1564      $footing .= "Copyright &copy; $year, ClearSCM, Inc. - All rights reserved";
1565  
1566   my %alert = $self->GetAlert ($alert);
1567   
1568   if ($alert{type} eq 'email') {
1569     my $from = 'Clearadm@' . hostdomain;
1570     
1571     mail (
1572       from    => $from,
1573       to      => $to,
1574       subject => "Clearadm Alert: $system: $subject",
1575       mode    => 'html',
1576       data    => $message, 
1577       footing => $footing,     
1578     );
1579   } else {
1580     $self->Error ("Don't know how to send $alert{type} alerts\n"
1581                 . "Subject: $subject\n"
1582                 . "Message: $message", 1);
1583   } # if
1584
1585   # Log alert
1586   my %alertlog = (
1587     alert        => $alert,
1588     system       => $system,
1589     notification => $notification,
1590     runlog       => $runlogID,
1591     timestamp    => Today2SQLDatetime,
1592     message      => $subject,  
1593   );  
1594   
1595   return $self->AddAlertlog (%alertlog);
1596 } # SendAlert
1597
1598 sub GetLastAlert ($$) {
1599   my ($self, $notification, $system) = @_;
1600   
1601   my $statement = <<"END";
1602 select
1603   runlog,
1604   timestamp
1605 from 
1606   alertlog
1607 where
1608       notification='$notification'
1609   and system='$system'
1610 order by
1611   timestamp desc
1612 limit 
1613   0, 1
1614 END
1615                 
1616   my $sth = $self->{db}->prepare ($statement)
1617     or return $self->_dberror ('Unable to prepare statement', $statement);
1618     
1619   $sth->execute
1620     or return $self->_dberror ('Unable to execute statement', $statement);
1621     
1622   my $alertlog= $sth->fetchrow_hashref;
1623   
1624   $sth->finish;
1625   
1626   if ($alertlog) {
1627     return %$alertlog;
1628   } else {
1629     return;
1630   } # if
1631 } # GetLastAlert
1632
1633 sub GetLastTaskFailure ($$) {
1634   my ($self, $task, $system) = @_;
1635   
1636   my $statement = <<"END";
1637 select
1638   id,
1639   ended
1640 from 
1641   runlog
1642 where
1643       status <> 0 
1644   and task='$task'
1645   and system='$system'
1646   and alerted='true'
1647 order by
1648   ended desc
1649 limit 
1650   0, 1
1651 END
1652                 
1653   my $sth = $self->{db}->prepare ($statement)
1654     or return $self->_dberror ('Unable to prepare statement', $statement);
1655     
1656   $sth->execute
1657     or return $self->_dberror ('Unable to execute statement', $statement);
1658     
1659   my $runlog= $sth->fetchrow_hashref;
1660   
1661   $sth->finish;
1662   
1663   if ($$runlog{ended}) {
1664     return %$runlog;
1665   } # if
1666   
1667   # If we didn't get any ended in the last call then there's nothing that
1668   # qualified. Still let's return a record (%runlog) that has a valid id so
1669   # that the caller can update that runlog with alerted = 'true'.
1670   $statement = <<"END";
1671 select
1672   id
1673 from
1674   runlog
1675 where
1676       status <> 0
1677   and task='$task'
1678   and system='$system'
1679 order by 
1680   ended desc
1681 limit
1682   0, 1
1683 END
1684
1685   $sth = $self->{db}->prepare ($statement)
1686     or return $self->_dberror ('Unable to prepare statement', $statement);
1687       
1688   $sth->execute
1689     or return $self->_dberror ('Unable to execute statement', $statement);
1690       
1691   $runlog = $sth->fetchrow_hashref;
1692     
1693   $sth->finish;
1694     
1695   if ($runlog) {
1696     return %$runlog;
1697   } else {
1698     return
1699   } # if
1700 } # GetLastTaskFailure 
1701
1702 sub Notify ($$$$$$) {
1703   my (
1704     $self,
1705     $notification,
1706     $subject,
1707     $message,
1708     $task,
1709     $system,
1710     $filesystem,
1711     $runlogID,
1712   ) = @_;
1713
1714   $runlogID = $self->_getLastID
1715     unless $runlogID;
1716     
1717   my ($err, $msg);
1718   
1719   # Update filesystem, if $filesystem was specified
1720   if ($filesystem) {
1721     ($err, $msg) = $self->UpdateFilesystem (
1722       $system,
1723       $filesystem, (
1724         notification => $notification,
1725       ),
1726     );
1727     
1728     $self->Error ("Unable to set notification for filesystem $system:$filesystem "
1729                . "(Status: $err)\n$msg", $err) if $err;
1730   } # if
1731   
1732   # Update system
1733   ($err, $msg) = $self->UpdateSystem (
1734     $system, (
1735       notification => $notification,
1736     ),
1737   );
1738   
1739   my %notification = $self->GetNotification ($notification);
1740   
1741   my %lastnotified = $self->GetLastAlert ($notification, $system);
1742   
1743   if (%lastnotified and $lastnotified{timestamp}) {
1744     my $today        = Today2SQLDatetime;
1745     my $lastnotified = $lastnotified{timestamp};
1746       
1747     if ($notification{nomorethan} =~ /hour/i) {
1748       $lastnotified = Add ($lastnotified, (hours => 1));
1749     } elsif ($notification{nomorethan} =~ /day/i) {
1750       $lastnotified = Add ($lastnotified, (days => 1));
1751     } elsif ($notification{nomorethan} =~ /week/i) {
1752       $lastnotified = Add ($lastnotified, (days => 7));
1753     } elsif ($notification{nomorethan} =~ /month/i) {
1754       $lastnotified = Add ($lastnotified, (month => 1));
1755     } # if
1756       
1757     # If you want to fake an alert in the debugger just change $diff accordingly
1758     my $diff = Compare ($today, $lastnotified);
1759     
1760     return
1761       if $diff <= 0;
1762   } # if  
1763
1764   my $when       = Today2SQLDatetime;
1765   my $nomorethan = lc $notification{nomorethan};
1766   my %alert      = $self->GetAlert ($notification{alert});
1767   my $to         = $alert{who};
1768
1769   # If $to is null then this means to send the alert to the admin for the
1770   # machine.
1771   unless ($to) {
1772     if ($system) {
1773       my %system = $self->GetSystem ($system);
1774     
1775       $to = $system{email};
1776     } else {
1777       # If we don't know what system this error occurred on we'll have to notify
1778       # the "super user" defined as $self->{NOTIFY} (The receiver of last
1779       # resort)
1780       $to = $self->{NOTIFY};
1781     } # if
1782   } # unless
1783   
1784   unless ($to) {
1785     Error "To undefined";
1786   } # unless
1787   
1788   $message .= "<p>You will receive this alert no more than $nomorethan.</p>";
1789   
1790   ($err, $msg) = $self->SendAlert (
1791     $notification{alert},
1792     $system,
1793     $notification{name},
1794     $subject,
1795     $message,
1796     $to,
1797     $runlogID,
1798   );
1799  
1800   $self->Error ("Unable to send alert (Status: $err)\n$msg", $err) if $err;
1801
1802   verbose "Sent alert to $to";
1803
1804   # Update runlog to indicate we notified the user for this execution
1805   ($err, $msg) = $self->UpdateRunlog (
1806     $runlogID, (
1807       alerted => 'true',
1808     ),
1809   );
1810   
1811   $self->Error ("Unable to update runlog (Status: $err)\n$msg", $err) if $err;
1812
1813   return;  
1814 } # Notify
1815
1816 sub ClearNotifications ($$;$) {
1817   my ($self, $system, $filesystem) = @_;
1818   
1819   my ($err, $msg);
1820   
1821   if ($filesystem) {
1822     ($err, $msg) = $self->UpdateFilesystem (
1823       $system,
1824       $filesystem, (notification => undef),
1825     );
1826     
1827     error "Unable to clear notification for filesystem $system:$filesystem "
1828         . "(Status: $err)\n$msg", $err
1829       if $err;
1830     
1831     # Check to see any of this system's filesystems have notifications. If none
1832     # then it's save to say we've turned off the last notification for a 
1833     # filesystem involved with this system and if $system{notification} was
1834     # 'Filesystem' then we can toggle off the notification on the system too
1835     my $filesystemsAlerted = 0;
1836     
1837     foreach ($self->FindFilesystem ($system)) {
1838       $filesystemsAlerted++ 
1839         if $$_{notification};
1840     } # foreach
1841     
1842     my %system = $self->GetSystem ($system);
1843     
1844     return
1845       unless $system;
1846       
1847     if ($system{notification}                 and 
1848         $system{notification} eq 'Filesystem' and
1849         $filesystemsAlerted == 0) {
1850       ($err, $msg) = $self->UpdateSystem ($system, (notification => undef));
1851
1852       $self->Error ("Unable to clear notification for system $system "
1853                   . "(Status: $err)\n$msg", $err) if $err;
1854     } # if
1855   } else {
1856     ($err, $msg) = $self->UpdateSystem ($system, (notification => undef));
1857        
1858     $self->Error ("Unable to clear notification for system $system "
1859                 . "(Status: $err)\n$msg", $err) if $err;
1860   } # if
1861   
1862   return;
1863 } # ClearNotifications
1864
1865 sub SystemAlive (%) {
1866   my ($self, %system) = @_;
1867
1868   # If we've never heard from this system then we will assume that the system
1869   # has not been set up to run clearagent and has never checked in. In any event
1870   # we cannot say the system died because we've never known it to be alive!  
1871   return 1
1872     unless $system{lastheardfrom};
1873     
1874   # If a system is not active (may have been temporarily been deactivated) then
1875   # we don't want to turn on the bells and whistles alerting people it's down.
1876   return 1
1877     if $system{active} eq 'false';
1878     
1879   my $today         = Today2SQLDatetime;
1880   my $lastheardfrom = $system{lastheardfrom};
1881       
1882   my $tenMinutes = 10 * 60;
1883   
1884   $lastheardfrom = Add ($lastheardfrom, (seconds => $tenMinutes));
1885
1886   if (DateToEpoch ($lastheardfrom) < DateToEpoch ($today)) {
1887     $self->UpdateSystem (
1888       $system{name}, (
1889         notification => 'Heartbeat'
1890       ),
1891     );
1892    
1893     return;
1894   } else {
1895     if ($system{notification}) {
1896       $self->UpdateSystem (
1897         $system{name}, (
1898           notification => undef
1899         ),
1900       );
1901     }
1902     return 1;
1903   } # if
1904 } # SystemAlive
1905
1906 sub UpdateAlert ($%) {
1907   my ($self, $name, %update) = @_;
1908   
1909   return $self->_updateRecord (
1910     'alert',
1911     "name='$name'",
1912     %update
1913   );
1914 } # UpdateAlert
1915
1916 sub AddAlertlog (%) {
1917   my ($self, %alertlog) = @_;
1918   
1919   my @requiredFields = (
1920     'alert',
1921     'notification',
1922   );
1923   
1924   my $result = _checkRequiredFields \@requiredFields, \%alertlog;
1925   
1926   return -1, "AddAlertlog: $result"
1927     if $result;
1928   
1929   # Timestamp record
1930   $alertlog{timestamp} = Today2SQLDatetime;
1931   
1932   return $self->_addRecord ('alertlog', %alertlog);  
1933 } # AddAlertlog
1934
1935 sub DeleteAlertlog ($) {
1936   my ($self, $condition) = @_;
1937   
1938   return
1939     unless $condition;
1940     
1941   if ($condition =~ /all/i) {
1942     return $self->_deleteRecord ('alertlog');
1943   } else {
1944     return $self->_deleteRecord ('alertlog', $condition);
1945   } # if
1946 } # DeleteAlertlog
1947
1948 sub FindAlertlog (;$$$$$) {
1949   my ($self, $alert, $system, $notification, $start, $page) = @_;
1950   
1951   $alert        ||= '';
1952   $system       ||= '';
1953   $notification ||= '';
1954   
1955   my $condition  = "alert like '%$alert%'";
1956      $condition .= ' and ';
1957      $condition .= "system like '%$system%'";
1958      $condition .= ' and ';
1959      $condition .= "notification like '%$notification%'";
1960      $condition .= " order by timestamp desc";
1961      
1962      if (defined $start) {
1963        $page ||= 10;
1964        $condition .= " limit $start, $page";
1965      } # unless
1966
1967   return $self->_getRecords ('alertlog', $condition);
1968 } # FindAlertLog
1969
1970 sub GetAlertlog ($) {
1971   my ($self, $alert) = @_;
1972   
1973   return
1974     unless $alert;
1975   
1976   my @records = $self->_getRecords ('alertlog', "alert='$alert'");
1977   
1978   if ($records[0]) {
1979     return %{$records[0]};
1980   } else {
1981     return;
1982   } # if  
1983 } # GetAlertlog
1984
1985 sub UpdateAlertlog ($%) {
1986   my ($self, $alert, %update) = @_;
1987   
1988   return $self->_updateRecord (
1989     'alertlog',
1990     "alert='$alert'",
1991     %update
1992   );
1993 } # UpdateAlertlog
1994
1995 sub AddNotification (%) {
1996   my ($self, %notification) = @_;
1997   
1998   my @requiredFields = (
1999     'name',
2000     'alert',
2001     'cond'
2002   );
2003   
2004   my $result = _checkRequiredFields \@requiredFields, \%notification;
2005   
2006   return -1, "AddNotification: $result"
2007     if $result;
2008   
2009   return $self->_addRecord ('notification', %notification);  
2010 } # AddNotification
2011
2012 sub DeleteNotification ($) {
2013   my ($self, $name) = @_;
2014   
2015   return $self->_deleteRecord ('notification', "name='$name'");
2016 } # DeletePackage
2017
2018 sub FindNotification (;$$) {
2019   my ($self, $name, $cond, $ordering) = @_;
2020   
2021   $name ||= '';
2022   
2023   my $condition  = "name like '%$name%'";
2024      $condition .= " and $cond"
2025        if $cond;
2026   
2027   return $self->_getRecords ('notification', $condition);                
2028 } # FindNotification
2029
2030 sub GetNotification ($) {
2031   my ($self, $name) = @_;
2032   
2033   return
2034     unless $name;
2035   
2036   my @records = $self->_getRecords ('notification', "name='$name'");
2037   
2038   if ($records[0]) {
2039     return %{$records[0]};
2040   } else {
2041     return;
2042   } # if  
2043 } # GetNotification
2044
2045 sub UpdateNotification ($%) {
2046   my ($self, $name, %update) = @_;
2047   
2048   return $self->_updateRecord (
2049     'notification',
2050     "name='$name'",
2051     %update
2052   );
2053 } # UpdateNotification
2054
2055 1;
2056
2057 =pod
2058
2059 =head1 CONFIGURATION AND ENVIRONMENT
2060
2061 DEBUG: If set then $debug is set to this level.
2062
2063 VERBOSE: If set then $verbose is set to this level.
2064
2065 TRACE: If set then $trace is set to this level.
2066
2067 =head1 DEPENDENCIES
2068
2069 =head2 Perl Modules
2070
2071 L<Carp>
2072
2073 L<DBI>
2074
2075 L<FindBin>
2076
2077 L<Net::Domain|Net::Domain>
2078
2079 =head2 ClearSCM Perl Modules
2080
2081 =begin man 
2082
2083  DateUtils
2084  Display
2085  GetConfig
2086  Mail
2087
2088 =end man
2089
2090 =begin html
2091
2092 <blockquote>
2093 <a href="http://clearscm.com/php/scm_man.php?file=lib/DateUtils.pm">DateUtils</a><br>
2094 <a href="http://clearscm.com/php/scm_man.php?file=lib/Display.pm">Display</a><br>
2095 <a href="http://clearscm.com/php/scm_man.php?file=lib/GetConfig.pm">GetConfig</a><br>
2096 <a href="http://clearscm.com/php/scm_man.php?file=lib/Mail.pm">Mail</a><br>
2097 </blockquote>
2098
2099 =end html
2100
2101 =head1 BUGS AND LIMITATIONS
2102
2103 There are no known bugs in this module
2104
2105 Please report problems to Andrew DeFaria <Andrew@ClearSCM.com>.
2106
2107 =head1 LICENSE AND COPYRIGHT
2108
2109 Copyright (c) 2010, ClearSCM, Inc. All rights reserved.
2110
2111 =cut