Imported Upstream version 3.2.0
[debian/amanda] / server-src / amreport.pl
1 #! @PERL@
2 # Copyright (c) 2010 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 conf [--version] [--help] [-o configoption]
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 = open3( my $fh, ">&2", ">&2", @cmd)
351       or error("cannot start $cmd[0]: $!", 1);
352     return ($pid, $fh);
353 }
354
355 sub open_mail_output
356 {
357     my ($report, $outputspec) = @_;
358     my $mailto = $outputspec->[1];
359
360     if ($mailto =~ /[*<>()\[\];:\\\/"!$|]/) {
361         error("mail addresses have invalid characters", 1);
362     }
363
364     my $datestamp =
365       $report->get_program_info(
366         $report->get_flag("amflush_run") ? "amflush" : "planner", "start" );
367
368     $datestamp /= 1000000 if $datestamp > 99999999;
369     $datestamp = int($datestamp);
370     my $year  = int( $datestamp / 10000 ) - 1900;
371     my $month = int( ( $datestamp / 100 ) % 100 ) - 1;
372     my $day   = int( $datestamp % 100 );
373     my $date  = POSIX::strftime( '%B %e, %Y', 0, 0, 0, $day, $month, $year );
374     $date =~ s/  / /g;
375
376     my $done = "";
377     if (  !$report->get_flag("got_finish")
378         || $report->get_flag("dump_failed") != 0) {
379         $done = " FAIL:";
380     } elsif ($report->get_flag("results_missing") != 0) {
381         $done = " MISSING:";
382     } elsif ($report->get_flag("dump_strange") != 0) {
383         $done = " STRANGE:";
384     }
385
386     my $subj_str =
387         getconf($CNF_ORG) . $done
388       . ( $report->get_flag("amflush_run") ? " AMFLUSH" :
389           $report->get_flag("amvault_run") ? " AMVAULT" : " AMANDA" )
390       . " MAIL REPORT FOR "
391       . $date;
392
393     my $cfg_mailer = getconf($CNF_MAILER);
394
395     my @cmd = ("$cfg_mailer", "-s", $subj_str, split(/ +/, $mailto));
396     debug("invoking mail app: " . join(" ", @cmd));
397
398
399     my ($pid, $fh);
400     eval { $pid = open3($fh, ">&2", ">&2", @cmd); 1; } or do {
401
402         ($pid, $fh) = (0, undef);
403         my $errstr =
404           "error: could not run command: " . join(" ", @cmd) . ": $@";
405
406         if ($mode == MODE_SCRIPT) {
407             debug($errstr);
408         } else {
409             error($errstr, 1);
410         }
411     };
412
413     return ($pid, $fh);
414 }
415
416 sub run_output {
417     my ($output) = @_;
418     my ($reportspec, $outputspec) = @$output;
419
420     # get the output
421     my ($pid, $fh);
422     if ($outputspec->[0] eq 'file') {
423         $fh = open_file_output($report, $outputspec);
424     } elsif ($outputspec->[0] eq 'printer') {
425         ($pid, $fh) = open_printer_output($report, $outputspec);
426     } elsif ($outputspec->[0] eq 'mail') {
427         ($pid, $fh) = open_mail_output($report, $outputspec);
428     }
429
430     # TODO: add some generic error handling here.  must be compatible
431     # with legacy behavior.
432
433     # TODO: modularize these better
434     if ($reportspec->[0] eq 'xml') {
435         print $fh $report->xml_output("" . getconf($CNF_ORG), $config_name);
436     } elsif ($reportspec->[0] eq 'human') {
437         my $hr =
438           Amanda::Report::human->new( $report, $fh, $config_name, $opt_logfname );
439         $hr->print_human_amreport();
440     } elsif ($reportspec->[0] eq 'postscript') {
441         use Amanda::Report::postscript;
442         my $rep =
443           Amanda::Report::postscript->new( $report, $config_name, $opt_logfname );
444         $rep->write_report($fh);
445     }
446
447     close $fh;
448
449     # clean up any subprocess
450     if (defined $pid) {
451         debug("waiting for child process to finish..");
452         waitpid($pid, 0);
453         if ($? != 0) {
454             warning("child exited with status $?");
455         }
456     }
457 }
458
459
460 ## Application initialization
461
462 Amanda::Util::setup_application("amreport", "server", $CONTEXT_CMDLINE);
463
464 my $config_overrides = new_config_overrides( scalar(@ARGV) + 1 );
465
466 Getopt::Long::Configure(qw/bundling/);
467 GetOptions(
468
469     ## old legacy configuration opts
470     "i" => sub { set_mode(MODE_SCRIPT); $opt_nomail = 1; },
471     opt_set_var("M", \$opt_mailto),
472     opt_set_var("f", \$opt_filename),
473     opt_set_var("l", \$opt_logfname),
474     opt_set_var("p", \$opt_psfname),
475
476     "o=s" => sub { add_config_override_opt($config_overrides, $_[1]); },
477
478     ## trigger default amdump behavior
479     "from-amdump" => sub { set_mode(MODE_SCRIPT) },
480
481     ## new configuration opts
482     "log=s" => sub { set_mode(MODE_CMDLINE); $opt_logfname = $_[1]; },
483     "ps:s" => sub { opt_push_queue([ ['postscript'], [ 'file', $_[1] ] ]); },
484     "mail-text:s" => sub { opt_push_queue([ ['human'], [ 'mail', $_[1] ] ]); },
485     "text:s"      => sub { opt_push_queue([ ['human'], [ 'file', $_[1] ] ]); },
486     "xml:s"       => sub { opt_push_queue([ ['xml'],   [ 'file', $_[1] ] ]); },
487     "print:s"     => sub { opt_push_queue([ [ 'postscript' ], [ 'printer', $_[1] ] ]); },
488
489     'version' => \&Amanda::Util::version_opt,
490     'help'    => \&usage,
491 ) or usage();
492
493 # set command line mode if no options were given
494 $mode = MODE_CMDLINE if ($mode == MODE_NONE);
495
496 if ($mode == MODE_CMDLINE) {
497     (scalar @ARGV == 1) or usage();
498 } else {    # MODE_SCRIPT
499     (scalar @ARGV > 0) or usage();
500 }
501
502 $config_name = shift @ARGV;    # only use first argument
503 $config_name ||= '.';          # default config is current dir
504
505 set_config_overrides($config_overrides);
506 config_init( $CONFIG_INIT_EXPLICIT_NAME, $config_name );
507
508 my ( $cfgerr_level, @cfgerr_errors ) = config_errors();
509 if ( $cfgerr_level >= $CFGERR_WARNINGS ) {
510     config_print_errors();
511     if ( $cfgerr_level >= $CFGERR_ERRORS ) {
512         error( "errors processing config file", 1 );
513     }
514 }
515
516 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
517
518 # read the tapelist
519 my $tl_file = config_dir_relative(getconf($CNF_TAPELIST));
520 my $tl = Amanda::Tapelist->new($tl_file);
521
522 # read the disklist
523 my $diskfile = config_dir_relative(getconf($CNF_DISKFILE));
524 $cfgerr_level += Amanda::Disklist::read_disklist('filename' => $diskfile);
525 ($cfgerr_level < $CFGERR_ERRORS) || die "Errors processing disklist";
526
527 # shim for installchecks
528 $Amanda::Constants::LPR = $ENV{'INSTALLCHECK_MOCK_LPR'}
529     if exists $ENV{'INSTALLCHECK_MOCK_LPR'};
530
531 # calculate the logfile to read from
532 $opt_logfname = Amanda::Util::get_original_cwd() . "/" . $opt_logfname
533         if defined $opt_logfname and $opt_logfname !~ /^\//;
534 my $logfile = $opt_logfname || get_default_logfile();
535 my $historical = defined $opt_logfname;
536 debug("using logfile: $logfile" . ($historical? " (historical)" : ""));
537
538 if ($mode == MODE_CMDLINE) {
539     debug("operating in cmdline mode");
540     apply_output_defaults();
541     push @outputs, [ ['human'], [ 'file', '-' ] ] if !@outputs;
542 } else {
543     debug("operating in script mode");
544     calculate_legacy_outputs();
545 }
546
547 ## Parse the report & set output
548
549 $report = Amanda::Report->new($logfile, $historical);
550 my $exit_status = $report->get_flag("exit_status");
551
552 ## filter outputs by errors & stranges
553
554 @outputs = grep { legacy_send_amreport($_) } @outputs;
555
556 for my $output (@outputs) {
557     debug("planned output: " . join(" ", @{ $output->[FORMAT] }, @{ $output->[OUTPUT] }));
558 }
559
560 ## Output
561
562 for my $output (@outputs) {
563     run_output($output);
564 }
565
566 Amanda::Util::finish_application();
567 exit $exit_status;