2 # Copyright (c) 2010-2012 Zmanda, Inc. All Rights Reserved.
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.
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
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
18 # Contact information: Zmanda Inc., 465 S Mathlida Ave, Suite 300
19 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
21 use lib '@amperldir@';
27 use Cwd qw( abs_path );
31 use Amanda::Config qw( :init :getconf config_dir_relative );
32 use Amanda::Util qw( :constants );
35 use Amanda::Constants;
36 use Amanda::Debug qw( debug warning );
38 use Amanda::Report::human;
39 use Amanda::Logfile qw( find_latest_log);
41 # constants for dealing with outputs
42 use constant FORMAT => 0;
43 use constant FMT_TYP => 0;
44 use constant FMT_TEMPLATE => 1;
46 use constant OUTPUT => 1;
47 use constant OUT_TYP => 0;
48 use constant OUT_DST => 1;
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;
59 my ($opt_mailto, $opt_filename, $opt_logfname, $opt_psfname, $opt_xml);
60 my ($config_name, $report, $outfh);
63 # list of [ report-spec, output-spec ]
64 my (@outputs, @output_queue);
66 ## Program subroutines
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]
76 [-i] [-M address] [-f output-file] [-l logfile] [-p postscript-file]
79 Amreport uses short options for use from shell scripts (e.g., amreport), or
80 long options for use on the command line.
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.
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.
94 my ( $error_msg, $exit_code ) = @_;
95 warning("error: $error_msg");
96 print STDERR "$error_msg\n";
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);
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.
119 my ($opt, $ref) = @_;
120 error("must pass scalar ref to opt_set_var", 1)
121 unless (ref($ref) eq "SCALAR");
128 # all short options are legacy options
129 set_mode(MODE_SCRIPT);
131 if (defined($$ref)) {
132 error("you may specify at most one -$op\n", 1);
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()";
151 # all queue-pushing options are command-line options
152 set_mode(MODE_CMDLINE);
154 push @output_queue, $output;
157 sub get_default_logfile
159 my $logdir = config_dir_relative(getconf($CNF_LOGDIR));
160 my $logfile = "$logdir/log";
165 } elsif ($mode == MODE_CMDLINE) {
167 $logfile = "$logdir/" . find_latest_log($logdir);
168 return $logfile if -f $logfile;
171 # otherwise, bail out
172 error("nothing to report on!", 1);
175 sub apply_output_defaults
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;
181 my $cfg_printer = getconf($CNF_PRINTER);
182 my $cfg_mailto = getconf_seen($CNF_MAILTO) ? getconf($CNF_MAILTO) : undef;
184 foreach my $job (@output_queue) {
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;
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
206 sub calculate_legacy_outputs {
207 # Part of the "options" is the configuration. Do we have a template? And a
208 # mailto? And mailer?
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;
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;
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)
223 # check that mailer is defined if we got an explicit -M, but go on
224 # processing (we will probably do nothing..)
226 warning("a mailer is not defined; will not send mail");
227 print "Warning: a mailer is not defined";
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 ] ];
239 # human/xml output to a file?
242 push @outputs, [ [ 'xml' ], [ 'file', $opt_filename ] ];
244 push @outputs, [ [ 'human' ], [ 'file', $opt_filename ] ];
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 ] ]
260 # postscript output to a file?
261 if ($opt_psfname and $cfg_template) {
262 push @outputs, [ [ 'postscript', $cfg_template ], [ 'file', $opt_psfname ] ];
266 sub legacy_send_amreport
269 my $cfg_send = getconf($CNF_SEND_AMREPORT_ON);
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");
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);
279 my $output_name = join(" ", @{ $output->[FORMAT] }, @{ $output->[OUTPUT] });
280 my $send_amreport = 0;
282 debug("testingamreport_send_on=$cfg_send, output:$output_name");
284 if ($cfg_send == $SEND_AMREPORT_STRANGE) {
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)) {
291 debug("send-amreport-on=$cfg_send, condition filled for $output_name");
296 debug("send-amreport-on=$cfg_send, condition not filled for $output_name");
300 } elsif ($cfg_send = $SEND_AMREPORT_ERROR) {
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)) {
308 debug("send-amreport-on=$cfg_send, condition filled for $output_name");
313 debug("send-amreport-on=$cfg_send, condition not filled for $output_name");
318 return $send_amreport;
321 sub open_file_output {
322 my ($report, $outputspec) = @_;
324 my $filename = $outputspec->[1];
325 $filename = Amanda::Util::get_original_cwd() . "/$filename"
326 unless ($filename eq "-" || $filename =~ m{^/});
328 if ($filename eq "-") {
331 open my $fh, ">", $filename or die "Cannot open '$filename': $!";
336 sub open_printer_output
338 my ($report, $outputspec) = @_;
339 my $printer = $outputspec->[1];
342 if ($printer and $Amanda::Constants::LPRFLAG) {
343 @cmd = ( $Amanda::Constants::LPR, $Amanda::Constants::LPRFLAG, $printer );
345 @cmd = ( $Amanda::Constants::LPR );
348 debug("invoking printer: " . join(" ", @cmd));
350 # redirect stdout/stderr to stderr, which is usually the amdump log
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) {
361 eval { $pid = open3($fh, ">&2", ">&2", @cmd); } or do {
362 ($pid, $fh) = (0, undef);
364 my $errstr = "error: $@: $!";
366 print STDERR "$errstr\n";
367 if ($mode == MODE_SCRIPT) {
379 my ($report, $outputspec) = @_;
380 my $mailto = $outputspec->[1];
382 if ($mailto =~ /[*<>()\[\];:\\\/"!$|]/) {
383 error("mail addresses have invalid characters", 1);
387 $report->get_program_info(
388 $report->get_flag("amflush_run") ? "amflush" :
389 $report->get_flag("amvault_run") ? "amvault" : "planner", "start" );
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 );
400 if ( !$report->get_flag("got_finish")
401 || $report->get_flag("dump_failed") != 0) {
403 } elsif ($report->get_flag("results_missing") != 0) {
405 } elsif ($report->get_flag("dump_strange") != 0) {
410 getconf($CNF_ORG) . $done
411 . ( $report->get_flag("amflush_run") ? " AMFLUSH" :
412 $report->get_flag("amvault_run") ? " AMVAULT" : " AMANDA" )
413 . " MAIL REPORT FOR "
416 my $cfg_mailer = getconf($CNF_MAILER);
418 my @cmd = ("$cfg_mailer", "-s", $subj_str, split(/ +/, $mailto));
419 debug("invoking mail app: " . join(" ", @cmd));
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) {
433 eval { $pid = open3($fh, ">&2", ">&2", @cmd) } or do {
434 ($pid, $fh) = (0, undef);
436 my $errstr = "error: $@: $!";
438 print STDERR "$errstr\n";
439 if ($mode == MODE_SCRIPT) {
452 my ($reportspec, $outputspec) = @$output;
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);
464 # TODO: add some generic error handling here. must be compatible
465 # with legacy behavior.
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,
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,
479 $rep->write_report($fh);
485 # clean up any subprocess
487 debug("waiting for child process to finish..");
490 warning("child exited with status $?");
496 ## Application initialization
498 Amanda::Util::setup_application("amreport", "server", $CONTEXT_CMDLINE);
500 my $config_overrides = new_config_overrides( scalar(@ARGV) + 1 );
502 debug("Arguments: " . join(' ', @ARGV));
503 Getopt::Long::Configure(qw/bundling/);
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),
513 "o=s" => sub { add_config_override_opt($config_overrides, $_[1]); },
515 ## trigger default amdump behavior
516 "from-amdump" => sub { set_mode(MODE_SCRIPT) },
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] ] ]); },
526 'version' => \&Amanda::Util::version_opt,
530 # set command line mode if no options were given
531 $mode = MODE_CMDLINE if ($mode == MODE_NONE);
533 if ($mode == MODE_CMDLINE) {
534 (scalar @ARGV == 1) or usage();
535 } else { # MODE_SCRIPT
536 (scalar @ARGV > 0) or usage();
539 $config_name = shift @ARGV; # only use first argument
540 $config_name ||= '.'; # default config is current dir
542 set_config_overrides($config_overrides);
543 config_init( $CONFIG_INIT_EXPLICIT_NAME, $config_name );
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 );
553 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
556 my $tl_file = config_dir_relative(getconf($CNF_TAPELIST));
557 my $tl = Amanda::Tapelist->new($tl_file);
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";
564 # shim for installchecks
565 $Amanda::Constants::LPR = $ENV{'INSTALLCHECK_MOCK_LPR'}
566 if exists $ENV{'INSTALLCHECK_MOCK_LPR'};
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)" : ""));
575 if ($mode == MODE_CMDLINE) {
576 debug("operating in cmdline mode");
577 apply_output_defaults();
578 push @outputs, [ ['human'], [ 'file', '-' ] ] if !@outputs;
580 debug("operating in script mode");
581 calculate_legacy_outputs();
584 ## Parse the report & set output
586 $report = Amanda::Report->new($logfile, $historical);
587 my $exit_status = $report->get_flag("exit_status");
589 ## filter outputs by errors & stranges
591 @outputs = grep { legacy_send_amreport($_) } @outputs;
593 for my $output (@outputs) {
594 debug("planned output: " . join(" ", @{ $output->[FORMAT] }, @{ $output->[OUTPUT] }));
599 for my $output (@outputs) {
603 Amanda::Util::finish_application();