Imported Upstream version 3.3.3
[debian/amanda] / server-src / amreport.pl
1 #! @PERL@
2 # Copyright (c) 2010-2012 Zmanda, Inc.  All Rights Reserved.
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 # or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 # for more details.
13 #
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
17 #
18 # Contact information: Zmanda Inc., 465 S Mathlida Ave, Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20
21 use lib '@amperldir@';
22 use strict;
23 use warnings;
24
25 use Getopt::Long;
26 use IPC::Open3;
27 use Cwd qw( abs_path );
28 use FileHandle;
29 use POSIX;
30
31 use Amanda::Config qw( :init :getconf config_dir_relative );
32 use Amanda::Util qw( :constants );
33 use Amanda::Tapelist;
34 use Amanda::Disklist;
35 use Amanda::Constants;
36 use Amanda::Debug qw( debug warning );
37 use Amanda::Report;
38 use Amanda::Report::human;
39 use Amanda::Logfile qw( find_latest_log);
40
41 # constants for dealing with outputs
42 use constant FORMAT  => 0;
43 use constant FMT_TYP => 0;
44 use constant FMT_TEMPLATE => 1;
45
46 use constant OUTPUT  => 1;
47 use constant OUT_TYP => 0;
48 use constant OUT_DST => 1;
49
50 # what mode is this running in? MODE_SCRIPT is when run from scripts like
51 # amdump, while MODE_CMDLINE is when run from the command line
52 use constant MODE_NONE    => 0;
53 use constant MODE_SCRIPT  => 1;
54 use constant MODE_CMDLINE => 2;
55
56 ## Global Variables
57
58 my $opt_nomail = 0;
59 my ($opt_mailto, $opt_filename, $opt_logfname, $opt_psfname, $opt_xml);
60 my ($config_name, $report, $outfh);
61 my $mode = MODE_NONE;
62
63 # list of [ report-spec, output-spec ]
64 my (@outputs, @output_queue);
65
66 ## Program subroutines
67
68 sub usage
69 {
70     print <<EOF;
71 Usage: amreport [--version] [--help] [-o configoption] <conf>
72   command-line mode options:
73     [--log=logfile] [--ps=filename] [--text=filename] [--xml=filename]
74     [--print=printer] [--mail-text=recipient]
75   script-mode options:
76     [-i] [-M address] [-f output-file] [-l logfile] [-p postscript-file]
77     [--from-amdump]
78
79 Amreport uses short options for use from shell scripts (e.g., amreport), or
80 long options for use on the command line.
81
82 If the printer is omitted, the printer from the configuration is used.  If the
83 filename is omitted or is "-", output is to stdout.  If the recipient is
84 omitted, then the default mailto from the configuration is used.
85
86 If no options are given, a text report is printed to stdout.  The --from-amdump
87 option triggers script mode, and is used by amdump.
88 EOF
89     exit 1;
90 }
91
92 sub error
93 {
94     my ( $error_msg, $exit_code ) = @_;
95     warning("error: $error_msg");
96     print STDERR "$error_msg\n";
97     exit $exit_code;
98 }
99
100 sub set_mode
101 {
102     my ($new_mode) = @_;
103
104     if ($mode != MODE_NONE && $mode != $new_mode) {
105         error("cannot mix long options (command-line mode), and "
106             . "short options (script mode) with each other", 1);
107     }
108
109     $mode = $new_mode;
110 }
111
112 # Takes a string specifying an option name (e.g. "M") and a reference to a
113 # scalar variable. It's return values are suitable for use in the middle of
114 # option specification, e.g. GetOptions("foo" => \$foo, opt_set_var("bar", \$bar)
115 # It will only let the option be specified (at most) once, though, and will
116 # print an error message and exit otherwise.
117 sub opt_set_var
118 {
119     my ($opt, $ref) = @_;
120     error("must pass scalar ref to opt_set_var", 1)
121       unless (ref($ref) eq "SCALAR");
122
123     return (
124         "$opt=s",
125         sub {
126             my ($op, $val) = @_;
127
128             # all short options are legacy options
129             set_mode(MODE_SCRIPT);
130
131             if (defined($$ref)) {
132                 error("you may specify at most one -$op\n", 1);
133             } else {
134                 $$ref = $val;
135             }
136         }
137     );
138 }
139
140
141 sub opt_push_queue
142 {
143     my ($output) = @_;
144
145     unless ((ref $output eq "ARRAY")
146         && (ref $output->[0] eq "ARRAY")
147         && (ref $output->[1] eq "ARRAY")) {
148         die "error: bad argument to opt_push_queue()";
149     }
150
151     # all queue-pushing options are command-line options
152     set_mode(MODE_CMDLINE);
153
154     push @output_queue, $output;
155 }
156
157 sub get_default_logfile
158 {
159     my $logdir  = config_dir_relative(getconf($CNF_LOGDIR));
160     my $logfile = "$logdir/log";
161
162     if (-f $logfile) {
163         return $logfile;
164
165     } elsif ($mode == MODE_CMDLINE) {
166
167         $logfile = "$logdir/" . find_latest_log($logdir);
168         return $logfile if -f $logfile;
169     }
170
171     # otherwise, bail out
172     error("nothing to report on!", 1);
173 }
174
175 sub apply_output_defaults
176 {
177     my $ttyp         = getconf($CNF_TAPETYPE);
178     my $tt           = lookup_tapetype($ttyp) if $ttyp;
179     my $cfg_template = "" . tapetype_getconf($tt, $TAPETYPE_LBL_TEMPL) if $tt;
180
181     my $cfg_printer = getconf($CNF_PRINTER);
182     my $cfg_mailto = getconf_seen($CNF_MAILTO) ? getconf($CNF_MAILTO) : undef;
183
184     foreach my $job (@output_queue) {
185
186         # supply the configured template if none was given.
187         if (   $job->[FORMAT]->[FMT_TYP] eq 'postscript'
188             && !$job->[FORMAT]->[FMT_TEMPLATE]) {
189             $job->[FORMAT]->[FMT_TEMPLATE] = $cfg_template;
190         }
191
192         # apply default destinations for each destination type
193         if (!$job->[OUTPUT][OUT_DST]) {
194             $job->[OUTPUT][OUT_DST] =
195                 ($job->[OUTPUT]->[OUT_TYP] eq 'printer') ? $cfg_printer
196               : ($job->[OUTPUT]->[OUT_TYP] eq 'mail')    ? $cfg_mailto
197               : ($job->[OUTPUT]->[OUT_TYP] eq 'file')    ? '-'
198               :   undef;    # will result in error
199         }
200
201         push @outputs, $job;
202     }
203 }
204
205
206 sub calculate_legacy_outputs {
207     # Part of the "options" is the configuration.  Do we have a template?  And a
208     # mailto? And mailer?
209
210     my $ttyp = getconf($CNF_TAPETYPE);
211     my $tt = lookup_tapetype($ttyp) if $ttyp;
212     my $cfg_template = "" . tapetype_getconf($tt, $TAPETYPE_LBL_TEMPL) if $tt;
213
214     my $cfg_mailer  = getconf($CNF_MAILER);
215     my $cfg_printer = getconf($CNF_PRINTER);
216     my $cfg_mailto  = getconf_seen($CNF_MAILTO) ? getconf($CNF_MAILTO) : undef;
217
218     if (!defined $opt_mailto) {
219         # ignore the default value for mailto
220         $opt_mailto = getconf_seen($CNF_MAILTO)? getconf($CNF_MAILTO) : undef;
221         # (note that we still may not send mail if CNF_MAILER is not set)
222     } else {
223         # check that mailer is defined if we got an explicit -M, but go on
224         # processing (we will probably do nothing..)
225         if (!$cfg_mailer) {
226             warning("a mailer is not defined; will not send mail");
227             print "Warning: a mailer is not defined";
228         }
229     }
230
231     # should we send a mail?
232     if ($cfg_mailer and $opt_mailto) {
233         # -i and -f override this
234         if (!$opt_nomail and !$opt_filename) {
235             push @outputs, [ [ 'human' ], [ 'mail', $opt_mailto ] ];
236         }
237     }
238
239     # human/xml output to a file?
240     if ($opt_filename) {
241         if ($opt_xml) {
242             push @outputs, [ [ 'xml' ], [ 'file', $opt_filename ] ];
243         } else {
244             push @outputs, [ [ 'human' ], [ 'file', $opt_filename ] ];
245         }
246     }
247
248     # postscript output to a printer?
249     # (this is just silly)
250     if ($Amanda::Constants::LPR and $cfg_template) {
251         # oddly, -i ($opt_nomail) will disable printing, but -i -f prints.
252         if ((!$opt_nomail and !$opt_psfname) or ($opt_nomail and $opt_filename)) {
253             # but we don't print if the text report isn't going anywhere
254             unless ((!$cfg_mailer or !$opt_mailto) and !($opt_filename and !$opt_xml)) {
255                 push @outputs, [ [ 'postscript', $cfg_template ], [ 'printer', $cfg_printer ] ]
256             }
257         }
258     }
259
260     # postscript output to a file?
261     if ($opt_psfname and $cfg_template) {
262         push @outputs, [ [ 'postscript', $cfg_template ], [ 'file', $opt_psfname ] ];
263     }
264 }
265
266 sub legacy_send_amreport
267 {
268     my ($output) = @_;
269     my $cfg_send = getconf($CNF_SEND_AMREPORT_ON);
270
271     ## only check $cfg_send if we are in script mode and sending mail
272     return 1 if ($mode != MODE_SCRIPT);
273     return 1 if !($output->[OUTPUT]->[OUT_TYP] eq "mail");
274
275     ## do not bother checking for errors or stranges if set to 'all' or 'never'
276     return 1 if ($cfg_send == $SEND_AMREPORT_ALL);
277     return 0 if ($cfg_send == $SEND_AMREPORT_NEVER);
278
279     my $output_name = join(" ", @{ $output->[FORMAT] }, @{ $output->[OUTPUT] });
280     my $send_amreport = 0;
281
282     debug("testingamreport_send_on=$cfg_send, output:$output_name");
283
284     if ($cfg_send == $SEND_AMREPORT_STRANGE) {
285
286         if (   !$report->get_flag("got_finish")
287             || ($report->get_flag("dump_failed") != 0)
288             || ($report->get_flag("results_missing") != 0)
289             || ($report->get_flag("dump_strange") != 0)) {
290
291             debug("send-amreport-on=$cfg_send, condition filled for $output_name");
292             $send_amreport = 1;
293
294         } else {
295
296             debug("send-amreport-on=$cfg_send, condition not filled for $output_name");
297             $send_amreport = 0;
298         }
299
300     } elsif ($cfg_send = $SEND_AMREPORT_ERROR) {
301
302         if (   !$report->get_flag("got_finish")
303             || ($report->get_flag("exit_status") != 0)
304             || ($report->get_flag("dump_failed") != 0)
305             || ($report->get_flag("results_missing") != 0)
306             || ($report->get_flag("dump_strange") != 0)) {
307
308             debug("send-amreport-on=$cfg_send, condition filled for $output_name");
309             $send_amreport = 1;
310
311         } else {
312
313             debug("send-amreport-on=$cfg_send, condition not filled for $output_name");
314             $send_amreport = 0;
315         }
316     }
317
318     return $send_amreport;
319 }
320
321 sub open_file_output {
322     my ($report, $outputspec) = @_;
323
324     my $filename = $outputspec->[1];
325     $filename = Amanda::Util::get_original_cwd() . "/$filename"
326       unless ($filename eq "-" || $filename =~ m{^/});
327
328     if ($filename eq "-") {
329         return \*STDOUT;
330     } else {
331         open my $fh, ">", $filename or die "Cannot open '$filename': $!";
332         return $fh;
333     }
334 }
335
336 sub open_printer_output
337 {
338     my ($report, $outputspec) = @_;
339     my $printer = $outputspec->[1];
340
341     my @cmd;
342     if ($printer and $Amanda::Constants::LPRFLAG) {
343         @cmd = ( $Amanda::Constants::LPR, $Amanda::Constants::LPRFLAG, $printer );
344     } else {
345         @cmd = ( $Amanda::Constants::LPR );
346     }
347
348     debug("invoking printer: " . join(" ", @cmd));
349
350     # redirect stdout/stderr to stderr, which is usually the amdump log
351     my ($pid, $fh);
352     if (!-f $Amanda::Constants::LPR || !-x $Amanda::Constants::LPR) {
353         my $errstr = "error: the mailer '$Amanda::Constants::LPR' is not an executable program.";
354         print STDERR "$errstr\n";
355         if ($mode == MODE_SCRIPT) {
356             debug($errstr);
357         } else {
358             error($errstr, 1);
359         }
360     } else {
361         eval { $pid = open3($fh, ">&2", ">&2", @cmd); } or do {
362             ($pid, $fh) = (0, undef);
363             chomp $@;
364             my $errstr = "error: $@: $!";
365
366             print STDERR "$errstr\n";
367             if ($mode == MODE_SCRIPT) {
368                 debug($errstr);
369             } else {
370                 error($errstr, 1);
371             }
372         };
373     }
374     return ($pid, $fh);
375 }
376
377 sub open_mail_output
378 {
379     my ($report, $outputspec) = @_;
380     my $mailto = $outputspec->[1];
381
382     if ($mailto =~ /[*<>()\[\];:\\\/"!$|]/) {
383         error("mail addresses have invalid characters", 1);
384     }
385
386     my $datestamp =
387       $report->get_program_info(
388         $report->get_flag("amflush_run") ? "amflush" : 
389         $report->get_flag("amvault_run") ? "amvault" : "planner", "start" );
390
391     $datestamp /= 1000000 if $datestamp > 99999999;
392     $datestamp = int($datestamp);
393     my $year  = int( $datestamp / 10000 ) - 1900;
394     my $month = int( ( $datestamp / 100 ) % 100 ) - 1;
395     my $day   = int( $datestamp % 100 );
396     my $date  = POSIX::strftime( '%B %e, %Y', 0, 0, 0, $day, $month, $year );
397     $date =~ s/  / /g;
398
399     my $done = "";
400     if (  !$report->get_flag("got_finish")
401         || $report->get_flag("dump_failed") != 0) {
402         $done = " FAIL:";
403     } elsif ($report->get_flag("results_missing") != 0) {
404         $done = " MISSING:";
405     } elsif ($report->get_flag("dump_strange") != 0) {
406         $done = " STRANGE:";
407     }
408
409     my $subj_str =
410         getconf($CNF_ORG) . $done
411       . ( $report->get_flag("amflush_run") ? " AMFLUSH" :
412           $report->get_flag("amvault_run") ? " AMVAULT" : " AMANDA" )
413       . " MAIL REPORT FOR "
414       . $date;
415
416     my $cfg_mailer = getconf($CNF_MAILER);
417
418     my @cmd = ("$cfg_mailer", "-s", $subj_str, split(/ +/, $mailto));
419     debug("invoking mail app: " . join(" ", @cmd));
420
421
422     my ($pid, $fh);
423     if (!-f $cfg_mailer || !-x $cfg_mailer) {
424         my $errstr = "error: the mailer '$cfg_mailer' is not an executable program.";
425         print STDERR "$errstr\n";
426         if ($mode == MODE_SCRIPT) {
427             debug($errstr);
428         } else {
429             error($errstr, 1);
430         }
431         
432     } else {
433         eval { $pid = open3($fh, ">&2", ">&2", @cmd) } or do {
434             ($pid, $fh) = (0, undef);
435             chomp $@;
436             my $errstr = "error: $@: $!";
437
438             print STDERR "$errstr\n";
439             if ($mode == MODE_SCRIPT) {
440                 debug($errstr);
441             } else {
442                 error($errstr, 1);
443             }
444         };
445     }
446
447     return ($pid, $fh);
448 }
449
450 sub run_output {
451     my ($output) = @_;
452     my ($reportspec, $outputspec) = @$output;
453
454     # get the output
455     my ($pid, $fh);
456     if ($outputspec->[0] eq 'file') {
457         $fh = open_file_output($report, $outputspec);
458     } elsif ($outputspec->[0] eq 'printer') {
459         ($pid, $fh) = open_printer_output($report, $outputspec);
460     } elsif ($outputspec->[0] eq 'mail') {
461         ($pid, $fh) = open_mail_output($report, $outputspec);
462     }
463
464     # TODO: add some generic error handling here.  must be compatible
465     # with legacy behavior.
466
467     if (defined $fh) {
468         # TODO: modularize these better
469         if ($reportspec->[0] eq 'xml') {
470             print $fh $report->xml_output("" . getconf($CNF_ORG), $config_name);
471         } elsif ($reportspec->[0] eq 'human') {
472             my $hr = Amanda::Report::human->new($report, $fh, $config_name,
473                                                 $opt_logfname );
474             $hr->print_human_amreport();
475         } elsif ($reportspec->[0] eq 'postscript') {
476             use Amanda::Report::postscript;
477             my $rep = Amanda::Report::postscript->new($report, $config_name,
478                                                       $opt_logfname );
479             $rep->write_report($fh);
480         }
481
482         close $fh;
483     }
484
485     # clean up any subprocess
486     if (defined $pid) {
487         debug("waiting for child process to finish..");
488         waitpid($pid, 0);
489         if ($? != 0) {
490             warning("child exited with status $?");
491         }
492     }
493 }
494
495
496 ## Application initialization
497
498 Amanda::Util::setup_application("amreport", "server", $CONTEXT_CMDLINE);
499
500 my $config_overrides = new_config_overrides( scalar(@ARGV) + 1 );
501
502 debug("Arguments: " . join(' ', @ARGV));
503 Getopt::Long::Configure(qw/bundling/);
504 GetOptions(
505
506     ## old legacy configuration opts
507     "i" => sub { set_mode(MODE_SCRIPT); $opt_nomail = 1; },
508     opt_set_var("M", \$opt_mailto),
509     opt_set_var("f", \$opt_filename),
510     opt_set_var("l", \$opt_logfname),
511     opt_set_var("p", \$opt_psfname),
512
513     "o=s" => sub { add_config_override_opt($config_overrides, $_[1]); },
514
515     ## trigger default amdump behavior
516     "from-amdump" => sub { set_mode(MODE_SCRIPT) },
517
518     ## new configuration opts
519     "log=s" => sub { set_mode(MODE_CMDLINE); $opt_logfname = $_[1]; },
520     "ps:s" => sub { opt_push_queue([ ['postscript'], [ 'file', $_[1] ] ]); },
521     "mail-text:s" => sub { opt_push_queue([ ['human'], [ 'mail', $_[1] ] ]); },
522     "text:s"      => sub { opt_push_queue([ ['human'], [ 'file', $_[1] ] ]); },
523     "xml:s"       => sub { opt_push_queue([ ['xml'],   [ 'file', $_[1] ] ]); },
524     "print:s"     => sub { opt_push_queue([ [ 'postscript' ], [ 'printer', $_[1] ] ]); },
525
526     'version' => \&Amanda::Util::version_opt,
527     'help'    => \&usage,
528 ) or usage();
529
530 # set command line mode if no options were given
531 $mode = MODE_CMDLINE if ($mode == MODE_NONE);
532
533 if ($mode == MODE_CMDLINE) {
534     (scalar @ARGV == 1) or usage();
535 } else {    # MODE_SCRIPT
536     (scalar @ARGV > 0) or usage();
537 }
538
539 $config_name = shift @ARGV;    # only use first argument
540 $config_name ||= '.';          # default config is current dir
541
542 set_config_overrides($config_overrides);
543 config_init( $CONFIG_INIT_EXPLICIT_NAME, $config_name );
544
545 my ( $cfgerr_level, @cfgerr_errors ) = config_errors();
546 if ( $cfgerr_level >= $CFGERR_WARNINGS ) {
547     config_print_errors();
548     if ( $cfgerr_level >= $CFGERR_ERRORS ) {
549         error( "errors processing config file", 1 );
550     }
551 }
552
553 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
554
555 # read the tapelist
556 my $tl_file = config_dir_relative(getconf($CNF_TAPELIST));
557 my $tl = Amanda::Tapelist->new($tl_file);
558
559 # read the disklist
560 my $diskfile = config_dir_relative(getconf($CNF_DISKFILE));
561 $cfgerr_level += Amanda::Disklist::read_disklist('filename' => $diskfile);
562 ($cfgerr_level < $CFGERR_ERRORS) || die "Errors processing disklist";
563
564 # shim for installchecks
565 $Amanda::Constants::LPR = $ENV{'INSTALLCHECK_MOCK_LPR'}
566     if exists $ENV{'INSTALLCHECK_MOCK_LPR'};
567
568 # calculate the logfile to read from
569 $opt_logfname = Amanda::Util::get_original_cwd() . "/" . $opt_logfname
570         if defined $opt_logfname and $opt_logfname !~ /^\//;
571 my $logfile = $opt_logfname || get_default_logfile();
572 my $historical = defined $opt_logfname;
573 debug("using logfile: $logfile" . ($historical? " (historical)" : ""));
574
575 if ($mode == MODE_CMDLINE) {
576     debug("operating in cmdline mode");
577     apply_output_defaults();
578     push @outputs, [ ['human'], [ 'file', '-' ] ] if !@outputs;
579 } else {
580     debug("operating in script mode");
581     calculate_legacy_outputs();
582 }
583
584 ## Parse the report & set output
585
586 $report = Amanda::Report->new($logfile, $historical);
587 my $exit_status = $report->get_flag("exit_status");
588
589 ## filter outputs by errors & stranges
590
591 @outputs = grep { legacy_send_amreport($_) } @outputs;
592
593 for my $output (@outputs) {
594     debug("planned output: " . join(" ", @{ $output->[FORMAT] }, @{ $output->[OUTPUT] }));
595 }
596
597 ## Output
598
599 for my $output (@outputs) {
600     run_output($output);
601 }
602
603 Amanda::Util::finish_application();
604 exit $exit_status;