2 # Copyright (c) 2010-2012 Zmanda, Inc. All Rights Reserved.
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.
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
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
17 # Contact information: Zmanda Inc., 465 S Mathlida Ave, Suite 300
18 # Sunnyvale, CA 94086, USA, or: http://www.zmanda.com
20 use lib '@amperldir@';
26 use Cwd qw( abs_path );
30 use Amanda::Config qw( :init :getconf config_dir_relative );
31 use Amanda::Util qw( :constants );
34 use Amanda::Constants;
35 use Amanda::Debug qw( debug warning );
37 use Amanda::Report::human;
38 use Amanda::Logfile qw( find_latest_log);
40 # constants for dealing with outputs
41 use constant FORMAT => 0;
42 use constant FMT_TYP => 0;
43 use constant FMT_TEMPLATE => 1;
45 use constant OUTPUT => 1;
46 use constant OUT_TYP => 0;
47 use constant OUT_DST => 1;
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;
58 my ($opt_mailto, $opt_filename, $opt_logfname, $opt_psfname, $opt_xml);
59 my ($config_name, $report, $outfh);
62 # list of [ report-spec, output-spec ]
63 my (@outputs, @output_queue);
65 ## Program subroutines
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]
75 [-i] [-M address] [-f output-file] [-l logfile] [-p postscript-file]
78 Amreport uses short options for use from shell scripts (e.g., amreport), or
79 long options for use on the command line.
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.
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.
93 my ( $error_msg, $exit_code ) = @_;
94 warning("error: $error_msg");
95 print STDERR "$error_msg\n";
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);
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.
118 my ($opt, $ref) = @_;
119 error("must pass scalar ref to opt_set_var", 1)
120 unless (ref($ref) eq "SCALAR");
127 # all short options are legacy options
128 set_mode(MODE_SCRIPT);
130 if (defined($$ref)) {
131 error("you may specify at most one -$op\n", 1);
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()";
150 # all queue-pushing options are command-line options
151 set_mode(MODE_CMDLINE);
153 push @output_queue, $output;
156 sub get_default_logfile
158 my $logdir = config_dir_relative(getconf($CNF_LOGDIR));
159 my $logfile = "$logdir/log";
164 } elsif ($mode == MODE_CMDLINE) {
166 $logfile = "$logdir/" . find_latest_log($logdir);
167 return $logfile if -f $logfile;
170 # otherwise, bail out
171 error("nothing to report on!", 1);
174 sub apply_output_defaults
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;
180 my $cfg_printer = getconf($CNF_PRINTER);
181 my $cfg_mailto = getconf_seen($CNF_MAILTO) ? getconf($CNF_MAILTO) : undef;
183 foreach my $job (@output_queue) {
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;
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
205 sub calculate_legacy_outputs {
206 # Part of the "options" is the configuration. Do we have a template? And a
207 # mailto? And mailer?
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;
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;
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)
222 # check that mailer is defined if we got an explicit -M, but go on
223 # processing (we will probably do nothing..)
225 warning("a mailer is not defined; will not send mail");
226 print "Warning: a mailer is not defined";
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 ] ];
238 # human/xml output to a file?
241 push @outputs, [ [ 'xml' ], [ 'file', $opt_filename ] ];
243 push @outputs, [ [ 'human' ], [ 'file', $opt_filename ] ];
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 ] ]
259 # postscript output to a file?
260 if ($opt_psfname and $cfg_template) {
261 push @outputs, [ [ 'postscript', $cfg_template ], [ 'file', $opt_psfname ] ];
265 sub legacy_send_amreport
268 my $cfg_send = getconf($CNF_SEND_AMREPORT_ON);
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");
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);
278 my $output_name = join(" ", @{ $output->[FORMAT] }, @{ $output->[OUTPUT] });
279 my $send_amreport = 0;
281 debug("testingamreport_send_on=$cfg_send, output:$output_name");
283 if ($cfg_send == $SEND_AMREPORT_STRANGE) {
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)) {
290 debug("send-amreport-on=$cfg_send, condition filled for $output_name");
295 debug("send-amreport-on=$cfg_send, condition not filled for $output_name");
299 } elsif ($cfg_send = $SEND_AMREPORT_ERROR) {
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)) {
307 debug("send-amreport-on=$cfg_send, condition filled for $output_name");
312 debug("send-amreport-on=$cfg_send, condition not filled for $output_name");
317 return $send_amreport;
320 sub open_file_output {
321 my ($report, $outputspec) = @_;
323 my $filename = $outputspec->[1];
324 $filename = Amanda::Util::get_original_cwd() . "/$filename"
325 unless ($filename eq "-" || $filename =~ m{^/});
327 if ($filename eq "-") {
330 open my $fh, ">", $filename or die "Cannot open '$filename': $!";
335 sub open_printer_output
337 my ($report, $outputspec) = @_;
338 my $printer = $outputspec->[1];
341 if ($printer and $Amanda::Constants::LPRFLAG) {
342 @cmd = ( $Amanda::Constants::LPR, $Amanda::Constants::LPRFLAG, $printer );
344 @cmd = ( $Amanda::Constants::LPR );
347 debug("invoking printer: " . join(" ", @cmd));
349 # redirect stdout/stderr to stderr, which is usually the amdump log
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) {
360 eval { $pid = open3($fh, ">&2", ">&2", @cmd); } or do {
361 ($pid, $fh) = (0, undef);
363 my $errstr = "error: $@: $!";
365 print STDERR "$errstr\n";
366 if ($mode == MODE_SCRIPT) {
378 my ($report, $outputspec) = @_;
379 my $mailto = $outputspec->[1];
381 if ($mailto =~ /[*<>()\[\];:\\\/"!$|]/) {
382 error("mail addresses have invalid characters", 1);
386 $report->get_program_info(
387 $report->get_flag("amflush_run") ? "amflush" :
388 $report->get_flag("amvault_run") ? "amvault" : "planner", "start" );
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 );
399 if ( !$report->get_flag("got_finish")
400 || $report->get_flag("dump_failed") != 0) {
402 } elsif ($report->get_flag("results_missing") != 0) {
404 } elsif ($report->get_flag("dump_strange") != 0) {
409 getconf($CNF_ORG) . $done
410 . ( $report->get_flag("amflush_run") ? " AMFLUSH" :
411 $report->get_flag("amvault_run") ? " AMVAULT" : " AMANDA" )
412 . " MAIL REPORT FOR "
415 my $cfg_mailer = getconf($CNF_MAILER);
417 my @cmd = ("$cfg_mailer", "-s", $subj_str, split(/ +/, $mailto));
418 debug("invoking mail app: " . join(" ", @cmd));
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) {
432 eval { $pid = open3($fh, ">&2", ">&2", @cmd) } or do {
433 ($pid, $fh) = (0, undef);
435 my $errstr = "error: $@: $!";
437 print STDERR "$errstr\n";
438 if ($mode == MODE_SCRIPT) {
451 my ($reportspec, $outputspec) = @$output;
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);
463 # TODO: add some generic error handling here. must be compatible
464 # with legacy behavior.
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,
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,
478 $rep->write_report($fh);
484 # clean up any subprocess
486 debug("waiting for child process to finish..");
489 warning("child exited with status $?");
495 ## Application initialization
497 Amanda::Util::setup_application("amreport", "server", $CONTEXT_CMDLINE);
499 my $config_overrides = new_config_overrides( scalar(@ARGV) + 1 );
501 debug("Arguments: " . join(' ', @ARGV));
502 Getopt::Long::Configure(qw/bundling/);
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),
512 "o=s" => sub { add_config_override_opt($config_overrides, $_[1]); },
514 ## trigger default amdump behavior
515 "from-amdump" => sub { set_mode(MODE_SCRIPT) },
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] ] ]); },
525 'version' => \&Amanda::Util::version_opt,
529 # set command line mode if no options were given
530 $mode = MODE_CMDLINE if ($mode == MODE_NONE);
532 if ($mode == MODE_CMDLINE) {
533 (scalar @ARGV == 1) or usage();
534 } else { # MODE_SCRIPT
535 (scalar @ARGV > 0) or usage();
538 $config_name = shift @ARGV; # only use first argument
539 $config_name ||= '.'; # default config is current dir
541 set_config_overrides($config_overrides);
542 config_init( $CONFIG_INIT_EXPLICIT_NAME, $config_name );
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 );
552 Amanda::Util::finish_setup($RUNNING_AS_DUMPUSER);
555 my $tl_file = config_dir_relative(getconf($CNF_TAPELIST));
556 my $tl = Amanda::Tapelist->new($tl_file);
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";
563 # shim for installchecks
564 $Amanda::Constants::LPR = $ENV{'INSTALLCHECK_MOCK_LPR'}
565 if exists $ENV{'INSTALLCHECK_MOCK_LPR'};
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)" : ""));
574 if ($mode == MODE_CMDLINE) {
575 debug("operating in cmdline mode");
576 apply_output_defaults();
577 push @outputs, [ ['human'], [ 'file', '-' ] ] if !@outputs;
579 debug("operating in script mode");
580 calculate_legacy_outputs();
583 ## Parse the report & set output
585 $report = Amanda::Report->new($logfile, $historical);
586 my $exit_status = $report->get_flag("exit_status");
588 ## filter outputs by errors & stranges
590 @outputs = grep { legacy_send_amreport($_) } @outputs;
592 for my $output (@outputs) {
593 debug("planned output: " . join(" ", @{ $output->[FORMAT] }, @{ $output->[OUTPUT] }));
598 for my $output (@outputs) {
602 Amanda::Util::finish_application();