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