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