#!/opt/vdops/bin/perl # This script automates the process of saving switch/router config files # to the changelog tree. If startup-config has changed from the version # sitting on disk, both old and new config files are saved to the # changetree # V Who When What # --------------------------------------------------------------------------- # 1.7.0 skendric 04-21-2010 Upgrade to perl 5.10.1 # 1.6.6 skendric 04-19-2009 Whine accurately # 1.6.5 skendric 04-15-2009 Whine about devices which encountered errors # 1.6.4 skendric 04-06-2009 More robust way to handle unwriteable tftp dir # 1.6.3 skendric 03-04-2009 More robust way to create temp files # 1.6.2 skendric 12-31-2008 Change write_tftp to download_config # 1.6.1 skendric 04-02-2008 Fix bug in use of File::stat # 1.6.0 skendric 03-31-2008 Double-check modification time on updated files # 1.5.8 skendric 02-27-2008 Remove temp file when write_tftp fails # 1.5.7 skendric 04-11-2007 Move create_temp_file_name to NetopsTools.pm # 1.5.6 skendric 03-21-2007 Stylistic mods # 1.5.5 skendric 11-16-2006 Replace Object Values with OIDs # 1.5.4 skendric 09-25-2006 Change variable names # 1.5.3 skendric 11-05-2005 Upgrade to new FHCRC::VDOPS module structure # 1.5.2 skendric 09-11-2005 Fixed show-stopper in which_changed # 1.5.1 skendric 05-16-2005 Log message if no targets have changed # 1.5.0 skendric 05-09-2005 Support Netops.pm-1.2 # 1.4.8 skendric 03-03-2005 Add error checking to check_tftp_copy # 1.4.7 skendric 01-03-2005 Add error checking to check_tftp_copy # 1.4.6 skendric 08-08-2004 Ignore lines with 'No configuration change' # 1.4.5 skendric 07-28-2004 Fixed bug in tell_staff # 1.4.4 skendric 07-21-2004 Prettified error msg printing # 1.4.3 skendric 05-30-2004 Fix bug affecting NativeIOS targets # 1.4.2 skendric 05-24-2004 Restore e-mail notification of changes # 1.4.1 skendric 05-17-2004 Respond gracefully to read-only config files # 1.4.0 skendric 05-11-2004 Migrate common functions to Netops.pm # 1.3.0 skendric 04-30-2004 Enhance command-line options # 1.2.5 skendric 12-13-2003 Make create_temp_file_name more portable # 1.2.4 skendric 12-01-2003 Autodetect local IP address # 1.2.3 skendric 11-16-2003 Use Net::Ping::External # 1.2.2 skendric 09-09-2003 Ignore 'portinstancecost' in CatOS 7.x configs # 1.2.1 skendric 08-10-2003 Minor bug fixes # 1.2.0 skendric 03-28-2003 Numerous minor updates # 1.1.7 skendric 02-18-2003 Set permissions on archive directories # 1.1.6 skendric 02-18-2003 Don't send blank lines to syslog # 1.1.5 skendric 02-10-2003 Check for blank msgs going to syslog # 1.1.4 skendric 01-26-2003 Tightened scoping, changed DEBUG to $debug # 1.1.3 skendric 01-05-2003 Check IOS version for SNMP/TFTP support # 1.1.2 skendric 12-18-2002 Added support for Aironet # 1.1.1 skendric 12-13-2002 Check for empty $msg in to_syslog # 1.1.0 skendric 11-13-2002 Added @version, improved build_target(), # improved error handling # 1.0.2 skendric 10-20-2002 Fixed bug in use of $grab_hosts # 1.0.1 skendric 10-14-2002 Fiddled w/e-mail notification # 1.0.0 skendric 08-20-2002 First version # # # Author: Stuart Kendrick, sbk@skendric .com # # Source: http://www.skendric.com/device # # This software is available under the GNU GENERAL PUBLIC LICENSE, see # http://www.fsf.org/licenses/gpl.html # # I borrowed heavily from the following URLs in order to write this script: # http://www.cisco.com/warp/public/477/SNMP/copy_configs_snmp.shtml # http://www.cisco.com/warp/public/477/SNMP/11.html # http://www.cisco.com/warp/public/477/SNMP/move_files_images_snmp.html # # In some ways, this script is a hacked version of 'save-config', designed # to be run from cron # # This script takes the following approach: # -Instructs the target device to save startup-config to # the tftp server, using a temporary file name # -Performs a 'diff', comparing the old version to this latest version # and ignoring lines which change dynamically ('spantree portcost' under # the CatOS and "clock drift" under the IOS, for example) # -If anything is left, the script saves the 'before' and 'after' # config files, ala 'save-config' style # # # Requirements: # -The target(s) must be pingable # # -The script must have file system access to the tftp directory # # -The following MIB modules stashed in /opt/vdops/share/snmp/mibs, # or wherever it is that you store MIB modules: # CISCO-PRODUCTS-MIB.my # # -PERL modules: the FHCRC::Netops collection # # -IOS boxes must be running 12.0 or better # # # Assumptions: # # # Tested on: # -perl-5.10.1 # -net-snmp-5.5 # # # Instructions: # -Customize the script for your site: find the 'user-configurable # variables' section and modify as appropriate # -Try it out # # # Caveats: # # # Known Bugs: # # # To do: # -Add support for SNMPv3 # # Begin script # Load modules use strict; use warnings; use feature 'say'; use feature 'switch'; use Carp qw(carp cluck croak confess); use Cwd; use Data::Dumper; use English qw( -no_match_vars ); use File::Copy 'cp'; use File::Path; use File::stat; use File::Temp; use Getopt::Std; use IO::File; use Mail::Send; use Text::Diff; use Time::localtime; use FHCRC::Netops::CiscoTools 1.3.1; use FHCRC::Netops::HostTools 1.0.3; use FHCRC::Netops::NetopsData 1.3.0; use FHCRC::Netops::NetopsTools 2.0.7; use FHCRC::Netops::PingTools 1.1.5; use FHCRC::Netops::SNMPTools 1.3.9; use FHCRC::Netops::Utilities 1.3.9; # Declare global variables my $archive; # Archive directory my $arch_root; # Root of archive directory my @changed; # List of devices whose configs have changed my $date; # year-day-month my %mtime; # Hash of modify times of config files which # we plan to update my $time; # hour:minute # Define global variables $debug = 0; # 10 = Logging # 9 = Database SELECT operations # 8 = Per IP/MAC/Port processing # 7 = Database INSERT/UPDATE/DELETE # 6 = Dump SNMP var # 5 = Dump snmp_packets # 4 = Grody: print big var # 3 = Verbose: print mid var # 2 = Simple: print small var # 1 = Basic: subroutine trace # 0 = Disable debugging $program_name = 'auto-save'; $usage = 'Usage: auto-save -s {yes|no} [-d {integer}] [-r] [-a | -e {expr} | -f {filename} | target1 target2 target3 ...]'; $version = '1.7.0'; # Binaries $grab_hosts = '/bin/cat /etc/hosts'; # Directories $arch_root = '/home/netops/logs/router'; # Pause parameters $long = 30; $mid = 10; $short = 5; # Ping Stuff $ping_count = 3; $ping_timeout = 1; # Report stuff $report_recipients = 'bsmith@widgets.com'; $report_subject = 'Auto-Save Report'; # SNMP Stuff # Optimize performance by sorting your community strings and SNMP version # list, most frequently used to the left, least frequently used to the right @mib_dir = qw(/opt/vdops/share/snmp/mibs); @mib_file = qw/ALL/; @snmp_read_list = qw/public/; @snmp_write_list = qw/secret/; @snmp_version_list = qw/2/; $snmp_port = 161; $snmp_retries = 3; $snmp_timeout = 2000000; # Syslog stuff $syslog_facility = 'local5'; $syslog_host = 'localhost'; $syslog_port = 514; $syslog_priority = 'info'; $syslog_socket = 'unix'; # Other possibilites include 'udp' and # 'stream'; depending on the flavor of Unix, # I've employed each of these # Target details @skip_name = qw/swamp/; @suffixes = qw/-agw -brg -dgw -esx -rtr/; # TFTP Stuff $tftp_dir = '/tftpboot/whatever'; $tftp_path = '/whatever'; $tftp_server = $EMPTY_STR; $tftp_server = get_my_ipaddr() if $tftp_server eq $EMPTY_STR; $tftp_server_name = get_nodename($tftp_server); # Grab arguments getopts('ad:e:f:s:', \%option); @target = @ARGV; # Set mode if ($option{r}) { $mode = 'report' } elsif (-t STDIN) { $mode = 'interactive' } else { $mode = 'batch' } ### Begin Main Program ############################################### { check_args(); # Check arguments compile_mibs(); # Compile MIB files build_target(); # Populate @target target_check(); # Look for errors in @target basic_info(); # Gather information sanity_check(); # Check for error conditions which_changed(); # Which configs have changed prep_run(); # Prepare to run print_before(); # Tell operator what I will do do_the_work(); # Do it clean_up(); # Chores print_after(); # Tell the operator what I did tell_staff(); # Send e-mail to staff } ##### End Main Program ################################################# ######################################################################## # Save the new config file in $tftp_dir to the change tree as # "$target-config.after" ######################################################################## sub after_write { my $config_file = shift; my $mtime; my $new_config_file = shift; my $sb; my $target = shift; # Debug trace trace_location('begin') if $debug; # Check for brain damage (-e "$tftp_dir/$config_file") or print_it("$tftp_dir/$config_file doesn't exist"); # If modify time hasn't changed, whine $sb = stat("$tftp_dir/$config_file"); $mtime = $sb->mtime; if ($mtime == $mtime{$target}) { print_it("$config_file modify time has not changed, copy did not happen"); $error{$target} = 'TFTP write did not succeed'; } # Otherwise, copy new config file to $archive else { if (cp ("$tftp_dir/$config_file", "$archive/$new_config_file") ) { chmod 0644, "./$new_config_file"; print_it("$tftp_dir/$config_file copied to $archive/$new_config_file"); } else { print_it("Could not copy $tftp_dir/$config_file to $archive/$new_config_file"); $error{$target} = "Could not copy $new_config_file"; } } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Examine the two config file for differences ######################################################################## sub any_diffs { my @delete; # Temp array to hold indices to lines we # don't want my @diff; # Temp array to hold differences my $first = shift; my $j; # Local iterator my $numLines; # Numer of lines in @diff my $result; # Boolean identifying whether or not # this routine sees significant differences my $second = shift; # Debug trace trace_location('begin') if $debug; # Sanity check unless (-e $first) { say "$first does not exist"; return 0; } unless (-e $second) { say "$second does not exist"; return 0; } # Identify raw differences @diff = split ('\n', diff "$first", "$second", { CONTEXT => 0 } ); say 'Number of raw differing lines = ', scalar @diff if $debug; # Remove irrelevant differences for ($j = 0; $j < @diff; $j++) { # diff dreck $delete[$j] = 1 if $diff[$j] =~ /^\-\-\-/; $delete[$j] = 1 if $diff[$j] =~ /^\+\+\+/; $delete[$j] = 1 if $diff[$j] =~ /^\@\@/; $delete[$j] = 1 if $diff[$j] =~ /\A\n\z/; # CatOS dynamic changes $delete[$j] = 1 if $diff[$j] =~ /#time/; $delete[$j] = 1 if $diff[$j] =~ /set spantree portcost/; $delete[$j] = 1 if $diff[$j] =~ /set spantree portinstancecost/; $delete[$j] = 1 if $diff[$j] =~ /set spantree portvlancost/; # IOS dynamic changes $delete[$j] = 1 if $diff[$j] =~ /\!/; $delete[$j] = 1 if $diff[$j] =~ /Last configuration change/; $delete[$j] = 1 if $diff[$j] =~ /No configuration change/; $delete[$j] = 1 if $diff[$j] =~ /NVRAM config last updated/; $delete[$j] = 1 if $diff[$j] =~ /ntp clock-period/; # Debug info if ($debug > 1) { say 'Differing lines:'; say $diff[$j]; given ($delete[$j]) { when (defined $_) { say 'This line will be deleted' } default { say 'This line will be kept' } } } } # Remove lines which failed checks $numLines = @diff - 1; for ($j = $numLines; $j >= 0; $j--) { splice @diff, $j, 1 if defined $delete[$j]; } # Declare result if (@diff > 0) { $result = 1 } else { $result = 0 } # Debug info if ($debug) { say 'diff ='; say @diff; } # Debug trace trace_location('end') if $debug; return $result; } ######################################################################## # Save the config file currently in $tftp_dir to the change tree as # "$target-config.before" ######################################################################## sub before_write { my $config_file = shift; my $old_config_file = shift; my $target = shift; # Debug trace trace_location('begin') if $debug; # Sanity check confess 'Must define config_file' unless defined $config_file; confess 'Must define old_config_file' unless defined $old_config_file; confess 'Must define target' unless defined $target; # Copy current config file to $archive if (cp ("$tftp_dir/$config_file", "$archive/$old_config_file") ) { chmod 0644, "./$old_config_file"; print_it("$tftp_dir/$config_file copied to $archive/$old_config_file"); } else { print_it("Could not copy $tftp_dir/$config_file to $archive/$old_config_file"); $error{$target} = "Could not copy $old_config_file"; } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Set directory permissions. In our environment, we want the change tree # to be group-writeable ... we forget to do this manually, when we # create directories. This subroutine does this for us, in case # we've forgotten ######################################################################## sub clean_up { my $file; # Iterates thru directory my $mode; # mode of current directory, in octal my $parent_dir; # directory above starting directory my $start_dir; # starting directory my $stat; # File::stat object # Debug trace trace_location('begin') if $debug; # Change to archive directory unless (chdir "$archive") { print_it("Cannot change to $archive: $!"); return 0; } # Assign variables for starting directory $start_dir = getcwd(); $stat = stat $start_dir; $mode = $stat->mode(); $mode &= 07777; $mode = sprintf "%lo", $mode; # If starting directory isn't group writable, make it so unless ($mode eq '770') { chmod 0770, $start_dir or print_it("$start_dir is not group-writeable: $!"); } # Ensure that all files are flagged 440 opendir (DIR, $start_dir) or print_it("Cannot opendir $start_dir: $!"); FILE: while (defined ($file = readdir(DIR)) ) { next FILE if $file =~ /^\.\.?$/; chmod 0440, "$start_dir/$file" or print_it("Cannot chmod files in $start_dir: $!"); say " file = $file" if $debug; } closedir DIR; # Assign variables for parent directory chdir ".." or print_it("Cannot examine parent directory: $!"); $parent_dir = getcwd(); $stat = stat $parent_dir; $mode = $stat->mode(); $mode &= 07777; $mode = sprintf "%lo", $mode; # If parent directory isn't group writable, make it so unless ($mode eq '770') { chmod 0770, $parent_dir or print_it("$parent_dir is not group-writeable: $!"); } # Return to original directory chdir "$start_dir"; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Copy old config files to the change tree, instruct the devices to save # their current config files to the tftp server, copy these new config # files to the changetree ######################################################################## sub do_the_work { my $config_file; # Name of current config file my $old_config_file; # Name of old config file my $new_config_file; # Name of new config file # Debug trace trace_location('begin') if $debug; # Make the archive directory mkpath($archive, 0, 0770) or die "Cannot create $archive\n"; # Loop through the list of targets TARGET: for my $target (@target) { # Define file names $config_file = $target . '-config'; $old_config_file = $config_file . '.before'; $new_config_file = $config_file . '.after'; # Announce start say '--------------------------------------------------------' if $mode eq 'interactive'; say "Beginning to process $target" if $mode eq 'interactive'; # If permissions aren't correct, whine unless ( touch_file("$tftp_dir/$config_file", 0666) ) { print_it("Cannot write to $tftp_dir/$config_file"); next TARGET; } # If we're playing for keeps if ($dome) { # Save old config file to change tree before_write($config_file, $old_config_file, $target); # Save startup-config to tftp server download_config({ host => $target, file => "$tftp_path/$config_file"} ); # Save new config file to change tree after_write($config_file, $new_config_file, $target); } # Otherwise, just pretend else { sleep $short; } # Announce completion say "Done processing $target" if $mode eq 'interactive'; say "-------------------------------------------------------\n\n" if $mode eq 'interactive'; } # Make things look pretty say "\n" if $mode eq 'interactive'; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Prepare to run ######################################################################## sub prep_run { my $config_file; # Name of configuration file in $tftp_dir my $date; # Year-month-day my @remove; # List of hosts we handed to prune_basic my $time; # hh:mm # Debug trace trace_location('begin') if $debug; # If no entries remain in @target, then no configuration files changed # and we don't need to do any work, so quit if (@target == 0) { print_it("\n\nNo configuration files have changed, ending"); } # Otherwise, loop through the list of targets else { TARGET: for my $target (@target) { my $sb; # Define file names $config_file = $target . '-config'; # Verify existence and permissions unless (touch_file("$tftp_dir/$config_file", 0666)) { push @remove, $target; next TARGET; } # Save mtime $sb = stat("$tftp_dir/$config_file"); $mtime{$target} = $sb->mtime; } # Make things look pretty say('') if $mode eq 'interactive'; # Remove entries which failed checks prune_basic(@remove); } # End 'loop through list of targets' # Define the archive directory $time = get_time(); ($time) = ($time) =~ /^(\d\d:\d\d)/; $date = get_date(); $archive = "$arch_root/$date/$time"; say "archive directory is $archive" if $debug; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Tell operator what I did ######################################################################## sub print_after { my $dir = getcwd(); my $shit_happens = 0; # Did errors occur? # Debug trace trace_location('begin') if $debug; # If running from cron, don't print report return 1 if $mode eq 'batch'; print "\n# Here is what I did\n\n"; print < 0) { print <mode & 07777; die "Fatal: $tftp_dir has mode $mode, must be 777\n" unless $mode eq '777'; # Loop through targets, looking for problems TARGET: for my $target (@target) { # This script only supports Cisco gear unless ($manufacturer{$target} eq 'Cisco') { log_it("$target not manufacturered by Cisco, skipping"); say "$target not manufacturered by Cisco, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } # Entertain operator print $BANG if $mode eq 'interactive'; } # Make things look pretty say('') if $mode eq 'interactive'; # Remove entries which failed checks prune_basic(@remove); # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Tell staff that config files changed ######################################################################## sub tell_staff { my $fh; my $msg; # Debug trace trace_location('begin') if $debug; # Unless I'm being watched already, send staff e-mail unless ($mode eq 'interactive') { # Build message header $msg = new Mail::Send; $msg->to($report_recipients); $msg->subject($report_subject); # Build message body $fh = $msg->open; # Identify the devices which changed print $fh "The following configs were saved in $archive by $0. Please add appropriate entries to the changes wiki\n\n"; for my $target (@target) { print $fh "$target\n"; } # Identify the devices for which we encountered errors for my $target (@target) { if (defined $error{$target} and $error{$target} ne $EMPTY_STR) { print $fh "I encountered errors processing the following devices:\n\n"; for my $target (sort keys %error) { print $fh "$target: $error{$target}\n"; } } print $fh "\n"; } # Send message $fh->close; } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Figure out which config files have changed ######################################################################## sub which_changed { my %changed; # Hash of diffs keyed by $target my $old_file; # File name of file in $tftp_dir my @remove; # List of targets to remove (because they # haven't changed) # Debug trace trace_location('begin') if $debug; # Loop through the list of targets TARGET: for my $target (@target) { my $temp_file; # Keep the operator entertained print_it("Analyzing $target"); # If the old config file exists, compare its contents to the running # config on the device $old_file = $target . '-config'; if (-e "$tftp_dir/$old_file") { my $tftp_file; say "Found old config file in $tftp_dir/$old_file" if $debug > 1; # Create a temporary file to hold the contents of the running config $temp_file = File::Temp::tempnam($tftp_dir, "$target-$program_name-"); die "$temp_file exists, and I wanted to use that name\n" if -e $temp_file; die "Cannot create $temp_file\n" unless touch_file($temp_file, 0666); # Replace file system path with tftp path ($tftp_file) = ($temp_file) =~ /$tftp_path\/(.*?)$/; $tftp_file = $tftp_path . $SLASH . $tftp_file; # Save startup-config to tftp server say " Saving config file to $tftp_file" if $debug > 1; my $result = download_config( {host => $target, file => $tftp_file} ); unless ($result) { print_it("download_config failed, skipping $target"); unlink $temp_file or warn "Cannot delete $temp_file: $!"; push @remove, $target; next TARGET; } # Examine for differences $changed{$target} = any_diffs("$tftp_dir/$old_file", $temp_file); push @remove, $target if $changed{$target} == 0; # Clean-up unlink $temp_file or warn "Cannot delete $temp_file: $!"; } # Otherwise, we have no copy of the last config file, so, from our # point of view, of course the running config is different from # our saved config else { say "Old config file $tftp_dir/$old_file does not exist" if $debug > 1; $changed{$target} = 1; } # Debug info if ($debug) { say "$target has changed" unless $changed{$target} == 0; } } # If no targets have changed, quit if ((@target - @remove) == 0) { print_it("\nNo targets have changed, exitting"); exit 1; } # Otherwise, remove any targets which didn't change and continue else { prune_basic(@remove); } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Output help ######################################################################## sub HELP_MESSAGE { print <