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