Some cosmetic edits
[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     for (@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     for (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   for my $fieldname (@$fields) {
248     my $found = 0;
249
250     for (keys %$rec) {
251       if ($fieldname eq $_) {
252          $found = 1;
253          last;
254       } # if
255     } # for
256
257     return "$fieldname is required"
258       unless $found;
259   } # for
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   } # for
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     for ($self->FindFilesystem ($system)) {
1838       $filesystemsAlerted++
1839         if $$_{notification};
1840     } # for
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