#!/usr/bin/perl use strict; use warnings; my($app_svnid) = '$HeadURL$ $LastChangedRevision$'; ## no critic (RequireInterpolationOfMetachars) use lib substr(`ade-config ade_share_prefix`,0,-1) . '/include'; ## no critic (ProhibitBacktickOperators) use ADE; use lib substr(`fad-config fad_share_prefix`,0,-1) . '/include'; ## no critic (ProhibitBacktickOperators) use FAD; use Getopt::Long qw(:config no_ignore_case); use Sys::Hostname; use Data::Dumper; use Fatal qw( close unlink umask ); # obviate checking close()'s return code use experimental 'smartmatch'; use File::Basename; # ade-add-config no longer allows -config to include settings that # don't adhere to format {|_}_prefix, which means we # can no longer put FADSCAN_STATE_DIR in there and get its value here. # So we need to frig it a little: we take FAD_STATE_DIR, strip '/fad' # off the end and add '/fadscan'. Same goes for FADSCAN_LOG_DIR. my($fad_state_prefix, $fad_log_prefix); eval `fad-config --format=perl fad_state_prefix fad_log_prefix`; ## no critic (ProhibitStringyEval, ProhibitBacktickOperators, RequireCheckingReturnValueOfEval) my($fadscan_state_prefix) = File::Basename::dirname($fad_state_prefix) . "/fadscan"; my($fadscan_log_prefix) = File::Basename::dirname($fad_log_prefix) . "/fadscan"; my(%fadscan_defined_errors) = ( fadscan_err_misc => { fmt => '%s' }, fadscan_err_access => { fmt => '%s: can\'t %s' }, ); # Options my($opt_mode, $opt_statedir, $opt_logdir, $opt_nocrcs); my($fileset); my($all_cnt, $del_cnt, $own_cnt, $grp_cnt, $mde_cnt, $typ_cnt, $lnk_cnt); my($crc_cnt, $mmn_cnt, $add_cnt, $sym_cnt, $schedflag_file, $hostname); # The names of the days and monthes, used in date conversion my(@day_names) = ( qw(Sun Mon Tue Wed Thu Fri Sat) ); # Log and state files should not be readable by common users umask 077; sub fadscan { my($errstack_ref) = @_; my($rc, $lc_dayname); my($oldfad_stem, $log_file, $in_file); my(%headers); my($nowsec, $nowmin, $nowhour, $nowmday, $nowmon, $nowyear, $nowyday, $nowisdst, $nowwday); my($in_handle); # Set ADE options ADE::register_error_types(\%fadscan_defined_errors); # Defaults for options $opt_mode = undef; $opt_logdir = $fadscan_log_prefix; $opt_statedir = $fadscan_state_prefix; $opt_nocrcs = 0; # Register options if (($rc=ADE::register_options($errstack_ref, 'icrs', 'mode-init,mode-check,mode-refresh,mode-schedule,log-dir:s,state-dir:s,no-crcs', 'main::fadscan_opt_handler_%s')) != $ADE::OK) { return($rc); } # Register handler functions if (($rc=ADE::set_callbacks($errstack_ref, \&fadscan_usage_help, \&fadscan_version, \&fadscan_paths)) != $ADE::OK) { return($rc); } # Process options ADE::debug($errstack_ref, 10, 'fadscan: processing options ...'); if (($rc=ADE::process_options($errstack_ref)) != $ADE::OK) { return($rc); } # Process arguments if (defined $opt_mode and $opt_mode eq 'schedule') { ADE::show_bad_usage($errstack_ref) if (not defined $ARGV[0] or defined$ARGV[1]); $fileset = $ARGV[0]; } else { ADE::show_bad_usage($errstack_ref) if (not defined $ARGV[1] or defined $ARGV[2]); $fileset = $ARGV[0]; $in_file = $ARGV[1]; } # Sanity checks and derivations if (! -d $opt_logdir) { ADE::error($errstack_ref, 'fadscan_err_access', $opt_logdir, 'stat'); return($ADE::FAIL); } if (! -d $opt_statedir) { ADE::error($errstack_ref, 'fadscan_err_access', $opt_statedir, 'stat'); return($ADE::FAIL); } # Set some global variables that we'll want to access everywhere. $hostname = Sys::Hostname::hostname(); $hostname or ADE::error($errstack_ref, 'fadscan_err_misc', 'can\'t determine hostname'); ($nowsec, $nowmin, $nowhour, $nowmday, $nowmon, $nowyear, $nowwday, $nowyday, $nowisdst) = localtime; $oldfad_stem = "$opt_statedir/$fileset-basesnap"; ($lc_dayname = $day_names[$nowwday]) =~ tr/A-Z/a-z/; $log_file = "$opt_logdir/$fileset-$lc_dayname.gz"; $schedflag_file = "$opt_statedir/$fileset-scheduled"; ADE::debug($errstack_ref, 10, sprintf 'fadscan: log_file=%s, in_file=%s, schedflag_file=%s, opt_mode=%s', $log_file, defined $in_file ? $in_file : 'undef', $schedflag_file, $opt_mode); # Guts # Don't allow scheduling of non-existent filesets. A non-existing fileset # is one which has not yet a base snapshot file. (This is FAD#148.) if ($opt_mode eq 'schedule' and not -f "$oldfad_stem.gz") { ADE::error($errstack_ref, 'fadscan_err_misc', "$fileset: no such fileset"); return($ADE::FAIL); } # And here we make do all the work. ADE::register_temp_file($errstack_ref, $log_file); if (($rc=ADE::manage_rolling_status($errstack_ref, $opt_mode, $schedflag_file, \&fadscan_scan_cb, \&fadscan_compare_cb, $oldfad_stem, '.gz', $log_file, { no_crcs => $opt_nocrcs, in_file => $in_file })) != $ADE::OK) { return($rc); } ADE::debug($errstack_ref, 4, 'returned from ADE::manage_rolling_status()'); # No need to delete it we get this far. ADE::deregister_temp_file($errstack_ref, $log_file); # Ensure sensible return code return($ADE::OK); } sub fadscan_opt_handler_i ## no critic (RequireArgUnpacking) { return(fadscan_opt_handler_mode_init(@_)); } sub fadscan_opt_handler_c ## no critic (RequireArgUnpacking) { return(fadscan_opt_handler_mode_check(@_)); } sub fadscan_opt_handler_r ## no critic (RequireArgUnpacking) { return(fadscan_opt_handler_mode_refresh(@_)); } sub fadscan_opt_handler_s ## no critic (RequireArgUnpacking) { return(fadscan_opt_handler_mode_schedule(@_)); } sub fadscan_opt_handler_mode_init { my($errstack_ref) = @_; $opt_mode = 'init'; return($ADE::OK); } sub fadscan_opt_handler_mode_check { my($errstack_ref) = @_; $opt_mode = 'check'; return($ADE::OK); } sub fadscan_opt_handler_mode_refresh { my($errstack_ref) = @_; $opt_mode = 'refresh'; return($ADE::OK); } sub fadscan_opt_handler_mode_schedule { my($errstack_ref) = @_; $opt_mode = 'schedule'; return($ADE::OK); } sub fadscan_opt_handler_log_dir { my($errstack_ref, $logdir) = @_; $opt_logdir = $logdir; return($ADE::OK); } sub fadscan_opt_handler_state_dir { my($errstack_ref, $statedir) = @_; $opt_statedir = $statedir; return($ADE::OK); } sub fadscan_opt_handler_no_crcs { my($errstack_ref) = @_; $opt_nocrcs = 1; return($ADE::OK); } sub fadscan_version { my($errstack_ref, $version_text_ref) = @_; return(ADE::extract_version($errstack_ref, $app_svnid, $version_text_ref)); } sub fadscan_paths { my($errstack_ref, $pathlist_text_ref) = @_; my($rc); ${$pathlist_text_ref} = "Log-Directory: $opt_logdir\n" . "State-Directory: $opt_statedir"; return($ADE::OK); } sub fadscan_usage_help { my($errstack_ref, $usage_text_short_ref, $usage_text_long_ref) = @_; ${$usage_text_short_ref} = '{ -[icr] | -s }'; ${$usage_text_long_ref} = " -i | --mode-init operate in 'initialise' mode\n" . " -c | --mode-check operate in 'check' mode\n" . " -r | --mode-refresh operate in 'refresh' mode\n" . " -s | --mode-schedule operate in 'schedule' mode\n" . " --log-dir= use alternate log directory\n" . " --state-dir= use alternate state info directory\n" . " --no-crcs do not calculate CRCs\n"; return($ADE::OK); } sub fadscan_compare_cb { my ($errstack_ref, $oldfad_file, $newfad_file, $comparison_report_handle) = @_; my ($rc, $fmtdatestart, $fmtdateend, $oldfad_handle, $newfad_handle); my (%old_fad_headers, %headers, %new_fad_headers); my ($version, %old_store, %new_store); ADE::debug($errstack_ref, 10, "fadscan_compare_cb: oldfad_file=$oldfad_file, newfad_file=$newfad_file"); # Get the time of the report. $fmtdatestart = localtime; # Open the two input files ADE::debug($errstack_ref, 10, 'fadscan_compare_cb: opening the two fad files for comparison ...'); if (($rc=ADE::open_compressed_file_for_reading($errstack_ref, $oldfad_file, \$oldfad_handle)) != $ADE::OK) { ADE::error($errstack_ref, 'fadscan_err_access', $oldfad_file, 'open'); return($rc); } if (($rc=ADE::open_compressed_file_for_reading($errstack_ref, $newfad_file, \$newfad_handle)) != $ADE::OK) { ADE::error($errstack_ref, 'fadscan_err_access', $newfad_file, 'open'); return($rc); } # Load fad files. ADE::debug($errstack_ref, 5, 'fadscan_compare_cb: loading first FAD file ...'); return($rc) if (($rc=FAD::fad_load($errstack_ref, $oldfad_handle, \%old_store, \%old_fad_headers, undef)) != $ADE::OK); ADE::debug($errstack_ref, 5, 'fadscan_compare_cb: loading second FAD file ...'); return($rc) if (($rc=FAD::fad_load($errstack_ref, $newfad_handle, \%new_store, \%new_fad_headers, undef)) != $ADE::OK); ADE::debug($errstack_ref, 10, 'fadscan_compare_cb: old_store: ' . Dumper(\%old_store)); ADE::debug($errstack_ref, 10, 'fadscan_compare_cb: new_store: ' . Dumper(\%new_store)); # Write report header. ADE::debug($errstack_ref, 10, 'fadscan_compare_cb: writing a report header ...'); print $comparison_report_handle "Host: $hostname\n"; print $comparison_report_handle "Fileset: $fileset\n"; print $comparison_report_handle "Start-Time: $fmtdatestart\n"; print $comparison_report_handle 'Last-Refresh-Time: ' . localtime($old_fad_headers{'Unix-Time'}) . "\n"; if (($rc=fadscan_version($errstack_ref, \$version)) != $ADE::OK) { return($rc); } print $comparison_report_handle "Version: $version\n"; print $comparison_report_handle "\n"; # Initialise some counters that are used to generate a report summary $all_cnt = 0; $del_cnt = 0; $own_cnt = 0; $grp_cnt = 0; $mde_cnt = 0; $typ_cnt = 0; $lnk_cnt = 0; $crc_cnt = 0; $mmn_cnt = 0; $add_cnt = 0; $sym_cnt = 0; # Actually work out the differences processing them via a callback. ADE::debug($errstack_ref, 10, 'fadscan_compare_cb: calling fadstreams_to_diffstream() to diff the opened fad files ...'); if (($rc=FAD::fad_diff($errstack_ref, \%old_store, \%new_store, \&fadscan_processdiff_cb, { out_handle => $comparison_report_handle, del_cnt_ref => \$del_cnt, own_cnt_ref => \$own_cnt, grp_cnt_ref => \$grp_cnt, mde_cnt_ref => \$mde_cnt, typ_cnt_ref => \$typ_cnt, lnk_cnt_ref => \$lnk_cnt, crc_cnt_ref => \$crc_cnt, mmn_cnt_ref => \$mmn_cnt, add_cnt_ref => \$add_cnt, sym_cnt_ref => \$sym_cnt, all_cnt_ref => \$all_cnt })) != $ADE::OK) { close $newfad_handle; close $oldfad_handle; ADE::error($errstack_ref, 'fadscan_err_misc', 'failed to work out differences'); } # We've finished reading the fad files now. ADE::debug($errstack_ref, 10, 'fadscan_compare_cb: comparison dome, closing fad files ...'); close $newfad_handle; close $oldfad_handle; print $comparison_report_handle "\n"; # Write report footer. ADE::debug($errstack_ref, 10, 'fadscan_compare_cb: writing a report footer ...'); print $comparison_report_handle "Total-Files-Changed: $all_cnt\n"; print $comparison_report_handle "Added: $add_cnt\n"; print $comparison_report_handle "Deleted: $del_cnt\n"; print $comparison_report_handle "Owner-Changed: $own_cnt\n"; print $comparison_report_handle "Group-Changed: $grp_cnt\n"; print $comparison_report_handle "Mode-Changed: $mde_cnt\n"; print $comparison_report_handle "Links-Changed: $lnk_cnt\n"; print $comparison_report_handle "Checksum-Changed: $crc_cnt\n"; print $comparison_report_handle "Type-Changed: $typ_cnt\n"; print $comparison_report_handle "Symlink-Dest-Changed: $sym_cnt\n"; print $comparison_report_handle "Major-Minor-Changed: $mmn_cnt\n"; $fmtdateend = localtime; print $comparison_report_handle "End-Time: $fmtdateend\n"; # We've finished writing to the report return($ADE::OK); } sub fadscan_scan_cb { my ($errstack_ref, $out_handle, $param) = @_; my ($rc, %store1, $collected_info_ref, $nocrcs, $in_file, $in_handle, $instack_flag); # Unpack the two parameters from the one the callback interface allowed to pass. $nocrcs = ${$param}{'no_crcs'}; $in_file = ${$param}{'in_file'}; ADE::debug($errstack_ref, 10, "fadscan: opening $in_file ..."); if ($in_file eq '-') { $in_handle = \*STDIN; } elsif (!open $in_handle, '<', $in_file) { ADE::error($errstack_ref, 'fadscan_err_access', $in_file, 'open'); return($rc); } ADE::debug($errstack_ref, 10, 'fadscan_scan_cb: reading file list ...'); while (<$in_handle>) { chomp; # It's possible the file disappears before we get to it; this should not # be an error, but only a warning. if (($rc=FAD::fad_collect_info($errstack_ref, $_, $nocrcs, \$collected_info_ref)) != $ADE::OK and (ADE::search_error_stack($errstack_ref, 'fad_err_access', \$instack_flag),defined $instack_flag)) { # Really here we want to say "without adding anything to the stack, display the # stack showing all entries as warnings" but this requires using functions inside # ADE.pm which I have made private. Hmm ... Should I make them public again? Maybe # I have to but we'll work around the limitation for the moment. I do this by # resetting the stack and then raising a *new* warning. ADE::reset_error_stack($errstack_ref, stack=>$errstack_ref); ADE::warning($errstack_ref, 'ade_err_access', $_, 'access (did it disappear?)'); next; } elsif ($rc != $ADE::OK) { close $in_handle if ($in_file ne '-'); return($rc); } if (($rc=FAD::fad_insert_info($errstack_ref, \%store1, $collected_info_ref)) != $ADE::OK) { close $in_handle if ($in_file ne '-'); return($rc); } } close $in_handle if ($in_file ne '-'); # Dump store to already-opened file. ADE::debug($errstack_ref, 10, 'fadscan_scan_cb: dumping store to stdout (which is redirected) ...'); if (($rc=FAD::fad_dump($errstack_ref, $out_handle, \%store1)) != $ADE::OK) { return($rc); } return($ADE::OK); } sub fadscan_processdiff_cb { my($errstack_ref, $param, $change_ref) = @_; my($oldstate, $newstate, $changetype, $file); my($i); my($out_handle, $del_cnt_ref, $own_cnt_ref, $grp_cnt_ref, $mde_cnt_ref, $typ_cnt_ref, $lnk_cnt_ref, $crc_cnt_ref, $mmn_cnt_ref, $add_cnt_ref, $sym_cnt_ref, $all_cnt_ref); my($txt_typ, $txt_own, $txt_grp, $txt_mde, $txt_add, $txt_del, $txt_mmn, $txt_crc, $txt_lnk, $txt_sym); # Unpack the many parameters from the one the callback interface allowed to pass. $out_handle = ${$param}{'out_handle'}; $del_cnt_ref = ${$param}{'del_cnt_ref'}; $own_cnt_ref = ${$param}{'own_cnt_ref'}; $grp_cnt_ref = ${$param}{'grp_cnt_ref'}; $mde_cnt_ref = ${$param}{'mde_cnt_ref'}; $typ_cnt_ref = ${$param}{'typ_cnt_ref'}; $lnk_cnt_ref = ${$param}{'lnk_cnt_ref'}; $crc_cnt_ref = ${$param}{'crc_cnt_ref'}; $mmn_cnt_ref = ${$param}{'mmn_cnt_ref'}; $add_cnt_ref = ${$param}{'add_cnt_ref'}; $sym_cnt_ref = ${$param}{'sym_cnt_ref'}; $all_cnt_ref = ${$param}{'all_cnt_ref'}; # Each output line will consist of all of these fields but with possibly # a couple of them not blank. $txt_typ = ' '; $txt_own = ' '; $txt_grp = ' '; $txt_mde = ' '; $txt_add = ' '; $txt_del = ' '; $txt_mmn = ' '; $txt_crc = ' '; $txt_lnk = ' '; $txt_sym = ' '; # For each sort of difference detected for the one file for which this callback was called ... foreach my $i (0..$#{@{$change_ref}{'changes'}}) { # Create some aliases $changetype = ${$change_ref}{'changes'}[$i]{'type'}; $oldstate = ${$change_ref}{'changes'}[$i]{'old'}; $newstate = ${$change_ref}{'changes'}[$i]{'new'}; $file = ${$change_ref}{'file'}; if ($changetype ~~ [ $FAD::CT_ADDED, $FAD::CT_DELETED ]) { ADE::debug($errstack_ref, 4, "fadscan_processdiff_cb:: changetype=$changetype, file=$file"); } else { ADE::debug($errstack_ref, 4, "fadscan_processdiff_cb:: changetype=$changetype, oldstate=$oldstate, newstate=$newstate, file=$file"); } # Now turn on the appropriate bits of text if ($changetype eq $FAD::CT_OWNER) { ${$own_cnt_ref}++; $txt_own = 'own'; } elsif ($changetype eq $FAD::CT_GROUP) { ${$grp_cnt_ref}++; $txt_grp = 'grp'; } elsif ($changetype eq $FAD::CT_MODE) { ${$mde_cnt_ref}++; $txt_mde = 'mde'; } elsif ($changetype eq $FAD::CT_LINKS) { ${$lnk_cnt_ref}++; $txt_lnk = 'lnk'; } elsif ($changetype eq $FAD::CT_DELETED) { ${$del_cnt_ref}++; $txt_del = 'del'; } elsif ($changetype eq $FAD::CT_TYPE) { ${$typ_cnt_ref}++; $txt_typ = 'typ'; } elsif ($changetype eq $FAD::CT_ADDED) { ${$add_cnt_ref}++; $txt_add = 'add'; } elsif ($changetype eq $FAD::CT_CONT_CRC) { ${$crc_cnt_ref}++; $txt_crc = 'crc'; } elsif ($changetype eq $FAD::CT_CONT_SYMLINK) { ${$sym_cnt_ref}++; $txt_sym = 'sym'; } elsif ($changetype eq $FAD::CT_CONT_MAJMIN) { ${$mmn_cnt_ref}++; $txt_mmn = 'mmn'; } else { ADE::internal($errstack_ref, "fadscan_processdiff_cb: unexpected CT: $changetype"); } } ${$all_cnt_ref}++; # and write the record with the file name tagged on the end. print $out_handle "$txt_add $txt_del $txt_typ $txt_own $txt_grp $txt_mde $txt_lnk $txt_crc $txt_sym $txt_mmn $file\n"; return($ADE::OK); } ADE::main(\&fadscan);