#!/opt/vdops/bin/perl # This script automates the process of upgrading an IOS device # V Who When What # --------------------------------------------------------------------------- # 2.1.0 skendric 02-21-2011 Upgrade to Netops 1.4.0 # 2.0.1 skendric 06-25-2010 Allow $error{$target} to be undef # 2.0.0 skendric 03-07-2010 Upgrade to perl-5.10.1 # 1.9.9 skendric 12-20-2009 Skip dfc devices # 1.9.8 skendric 10-07-2009 Remove bootldr support # 1.9.7 skendric 08-09-2009 Fix tftp directory permissions check # 1.9.6 skendric 04-06-2009 Enhance debug output # 1.9.5 skendric 02-11-2009 Add command-line switch for forcing the format # of all flash devices # 1.9.4 skendric 12-31-2008 Handle errors around boot stanza # 1.9.3 skendric 12-30-2008 Support new snmpSet format # 1.9.2 skendric 04-30-2008 Whine if a target contains a Field Upgradeable # Device (service module) # 1.9.1 skendric 04-30-2008 Consider effect of services modules # 1.9.0 skendric 12-29-2007 Error checking, report fiddles # 1.8.9 skendric 08-17-2007 Cosmetic changes to output # 1.8.8 skendric 03-21-2007 Stylistic mods # 1.8.7 skendric 12-01-2006 Add 'boot system tftp' to boot lines # 1.8.6 skendric 11-05-2006 Add check for read/write status of flash device # 1.8.5 skendric 10-23-2006 Replace Object Values with OIDs # 1.8.4 skendric 09-25-2006 Change variable names # 1.8.3 skendric 11-05-2005 Upgrade to new WI::VDOPS module structure # 1.8.2 skendric 07-12-2005 Fix bug in use of upload_image # 1.8.1 skendric 05-22-2005 Fix bug in use of touch_file # 1.8.0 skendric 05-09-2005 Support Netops.pm-1.2 # 1.7.1 skendric 07-21-2004 Prettified error msg printing # 1.7.0 skendric 05-09-2004 Migrate common functions to Netops.pm # 1.6.0 skendric 04-30-2004 Enhance command-line options # 1.5.6 skendric 03-26-2004 Exclude WAPs, syntactic clean-up # 1.5.5 skendric 12-01-2003 Autodetect local IP address # 1.5.4 skendric 11-02-2003 Add support for NativeIOS on C6K # 1.5.3 skendric 10-31-2003 Use Net::Ping::External # 1.5.2 skendric 08-10-2003 Minor bug fixes # 1.5.1 skendric 06-08-2003 Additional support for cisco7200 series # 1.5.0 skendric 03-28-2003 Numerous minor updates # 1.4.4 skendric 02-24-2003 Started to add support for Aironet # 1.4.3 skendric 02-23-2003 Fix permissions on boot and mica files # 1.4.2 skendric 01-26-2003 Tightened scoping, changed DEBUG to $debug # 1.4.1 skendric 01-05-2003 Check IOS version for SNMP/TFTP support # 1.4.0 skendric 11-20-2002 Added @version, improved build_target() # 1.3.1 skendric 08-05-2002 Additional error handling # 1.3.0 skendric 08-04-2002 Add support for 2600, 3600, 7200 # 1.2.0 skendric 07-22-2002 Handle 'boot system flash' mods via tftp # rather than telnet # 1.1.0 skendric 05-31-2002 Auto-detect running from cron # 1.0.1 skendric 05-10-2002 Logs username of operator # 1.0.0 skendric 04-28-2002 First Version - only MSFC # Author: Stuart Kendrick, sbk {put at sign here} skendric {put dot here} 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 # # # This script takes the following approach: # -Find a suitable location for the new image. If none, format all devices # -Upload the new image # -If flash was formatted, upload 'portware', IOS 'boot' image, and # the old image, as applicable # -Set the boot variable # # Image location: # -Try to put all the images on the flash device whose name contains # the string "flash" # -If they don't all fit there, then overflow onto remaining flash # devices, if any # # Requirements: # -The tftp server and the target(s) must be pingable # # -The script must have file system access to the tftp directory # # -The new image must be sitting on the tftp server # # -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 WI::Netops collection # # # Assumptions: # -No config files on flash devices ... this script erases all flash # devices, without saving anything but the currently running # image (and 'boot' images and portware images) # # # Tested on: # -MSFC1, MSFC2, C5KRSM, cat6506, cisco2620, cisco3640, cisco7206VXR # cat4500 w/SupV # -perl-5.12.2 # -net-snmp-5.6 # # # Instructions: # -Customize the script for your site: find the 'user-configurable # variables' section and modify as appropriate # -Try it out # # # Caveats: # -Each hardware platform is different. If you want to run this # against an untested hardware platform, I predict that this script # won't work. # (If you can figure out a way to make your favorite hardware # platform accessible to me over the 'Net, I'm willing to try # adding support for it to this script) # # # 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 Data::Dumper; use Data::Dump::Streamer; use English qw( -no_match_vars ); use File::stat; use Getopt::Std; use WI::Netops::CiscoTools 1.4.3; use WI::Netops::HostTools 1.0.4; use WI::Netops::NetopsTools 2.2.3; use WI::Netops::NetopsData 1.4.0; use WI::Netops::PingTools 1.1.7; use WI::Netops::SNMPTools 1.5.3; use WI::Netops::Utilities 1.4.4; # Declare global variables my %boot_image_dev; # Device holding boot image, after flash format my %new_image_dev; # Device holding new image, after flash format my %old_image_dev; # Device holding old image, after flash format my %actual_boot; # Final/actual 'boot system' lines my %desired_boot; # Desired 'boot system' lines my $format_flash; # Boolean for forcing the formatting of all flash # devices my %formatted; # Boolean for the success/falure of format_flash my %mica_device; # Devices holding portware files my %mica_file; # Names of portware files my %new_boot; # New 'boot system' lines my $new_image; # Name of image to upload my %old_boot; # Original 'boot system' lines my %old_device; # Device from which $target[$i} booted my @targets_with_modules; # List of targets which contain service modules my %uploaded_boot_image; # Boolean for the success/failure of uploading # the boot image my %uploaded_mica_image; # Boolean for the success/failure of uploading # the mica image my %uploaded_old_image; # Boolean for the success/failure of uploading # the old image my %uploaded_new_image; # Boolean for the success/failure of uploading # the new image my $wait; # Seconds to wait for management agent to recover # Define global variables $program_name = 'upgrade-ios'; $usage = 'Usage: upgrade-ios -s {yes|no} [-d {integer}] -i {image} [-g] [-a | -e {expr} | -f {filename} | target1 target2 target3 ...]'; $version = '2.1.0'; # Define user-configurable variables # TFTP Stuff $tftp_server = $EMPTY_STR; $tftp_server = get_my_ipaddr() if $tftp_server eq $EMPTY_STR; $tftp_server_name = get_nodename($tftp_server); # The number of seconds I'm willing to wait for various operations, like # flash file system formats and file uploads, before giving up $wait = 1800; # If set to '1' (see command-line switch -g), I will format all flash devices, # even if they already have enough free space to contain the images $format_flash = 0; # Grab arguments getopts('ad:e:f:gi:s:', \%option); $format_flash = 1 if defined $option{g}; @target = @ARGV; # Check arguments die "$usage\n" unless defined $option{i}; $new_image = $option{i}; # Set mode if ($option{r}) { $mode = 'report' } elsif (-t STDIN) { $mode = 'interactive' } else { $mode = 'batch' } ##### Begin Main Program ############################################### { check_args(); # Check arguments read_config(); # Read config file 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 check_image_file(); # Look for error conditions in image files print_debug(); # Debugging aid print_before(); # Tell operator what I will do do_the_work(); # Go for it info_after(); # Gather information modify_boot_lines(); # Modify boot stanza print_after(); # Tell the operator what I did } ##### End Main Program ############################################### ######################################################################## # Check image files for problems ######################################################################## sub check_image_file { # Debug trace trace_location('begin') if $debug; # Notify operator print_it('Image file check...'); # Check new image file for problems check_image("$tftp_dir/$new_image"); # Loop through targets, looking for problems for my $target (@target) { # Check boot image file for problems if (defined $boot_image{$target}) { check_image("$tftp_dir/$boot_image{$target}"); } # Check old image file for problems check_image("$tftp_dir/$old_image{$target}"); # Correlate image name and hardware type correlate_image($target, $new_image); # Entertain operator print $BANG if $mode eq 'interactive'; } # Make things look pretty say('') if $mode eq 'interactive'; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Figure out if this device stores a fall-back image or not. Often, # flash on an IOS device contains a "boot" image, an image with the # string "boot" in its name which is used as a last-ditch image in case # the primary image fails to load. This "boot" image is feature-poor, # not containing enough code to allow the device to function in a # production environment, but containing enough features to allow # the device to be remotely managed: to allow a more useful image # to be uploaded by the administrator and to allow the device to # be remotely rebooted in an effort to run said image. Alternatively, if # the device is a C6K w/MSFC1 running IOS, then this image acts as a # bootstrapper for the actual image: i.e. we gotta have it. If such an # image exists on any flash device, we assume that we need it and we keep it. ######################################################################## sub find_boot_image { my $boot_device; # The device on which $boot_image resides my $boot_image; # IOS image with 'boot' in its name my $host = shift; # Debug trace trace_location('begin') if $debug; # Determine whether or not a "boot" image exists on flash # Walk flash devices FLASH_DEVICE: for my $j (sort keys %{$flash_device_name{$host}}) { next FLASH_DEVICE if $flash_device_status{$host}->{$j} eq 'undefined'; next FLASH_DEVICE unless $flash_device_size{$host}->{$j} > 0; next FLASH_DEVICE unless $num_flash_files{$host}->{$j} > 0; # Walk flash files FLASH_FILE: for my $k (sort keys %{$flash_file_name{$host}->{$j}}) { if ($flash_file_name{$host}->{$j}->{$k} =~ /boot/ ) { if ($flash_file_status{$host}->{$j}->{$k} eq 'valid') { $boot_device = $flash_device_name{$host}->{$j}; $boot_image = $flash_file_name{$host}->{$j}->{$k}; } } } # End 'Walk flash files' } # End 'Walk flash devices' # Debug info if ($debug) { if (defined $boot_device and defined $boot_image) { say " For $host, found boot image $boot_device:$boot_image"; } else { say " For $host, found no boot image"; } } # Debug trace trace_location('end') if $debug; return ($boot_device, $boot_image); } ######################################################################## # Do the work: format flash, upload images ######################################################################## sub do_the_work { my $alive_ref; # List of targets which responded to snmpget my $dead; # Target which wasn't responding when I quit # waiting # Debug trace trace_location('begin') if $debug; # Loop through @target, formatting flash and uploading images TARGET: for my $target (@target) { my $spacious_device; # Announce start say '--------------------------------------------------------' if $mode eq 'interactive'; print_it("Beginning to process $target"); # If the operator wants us to format all flash systems, then do it if ($format_flash) { say 'Operator requested a format of flash devices' if $debug > 1; $formatted{$target} = format_flash($target, $wait); if ($formatted{$target}) { say 'Successfully formatted flash devices, proceeding' if $debug > 1; } else { print_it('Unable to format flash, skipping'); $error{$target} = 'Unable to format flash devices'; next TARGET; } } # Otherwise, look for space on flash file systems. If there isn't enough # space to hold $new_image, then format all the file systems # first. Formatting everything is crude, but I'm feeling lazy else { $spacious_device = find_likely_flash_device($target, $new_image); if (defined $spacious_device) { say "I will upload to device $spacious_device" if $debug > 1; } else { say "Not enough space for new image, formatting" if $debug > 1; $formatted{$target} = format_flash($target, $wait); if ($formatted{$target}) { say 'Successfully formatted flash devices, proceeding' if $debug > 1; } else { print_it('Unable to format flash, skipping'); $error{$target} = 'Unable to format flash devices'; next TARGET; } } } # End 'Otherwise, look for space on flash file systems' # Upload new image $uploaded_new_image{$target} = upload_image( {host => $target, image => $new_image, wait => $wait} ); if ($uploaded_new_image{$target}) { say 'Successfully uploaded image' if $debug > 1; } else { print_it("Could not upload $new_image"); $error{$target} = "Could not upload $new_image"; } # If a 'mica-modem' image existed, upload it if (defined $mica_file{$target} and defined $formatted{$target}) { $uploaded_mica_image{$target} = upload_image( {host => $target, image => $mica_file{$target}, wait => $wait} ); } # If a 'boot' image existed, upload it if (defined $boot_image{$target} and defined $formatted{$target}) { $uploaded_boot_image{$target} = upload_image( {host => $target, image => $boot_image{$target}, wait => $wait} ); } # If the old image is different from the new image, upload it if ($old_image{$target} ne $new_image and defined $formatted{$target}) { $uploaded_old_image{$target} = upload_image( { host=> $target, image => $old_image{$target}, wait => $wait} ); } # Announce completion print_it("Done processing $target"); say "-------------------------------------------------------\n\n" if $mode eq 'interactive'; } # Poll management agents until they recover (use sysDescr.0) ($alive_ref, $dead) = poll_by_snmp(\@target, '.1.3.6.1.2.1.1.1.0', $wait); # Process results if (@$alive_ref eq @target) { print_it("All targets are responding\n"); } else { print_it("Host $dead is not responding\n"); } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Get 'boot' lines from startup-config ######################################################################## sub get_boot_lines { my @boot; # 'boot system' lines on $target my $file; # File handle my $random = int(rand 9999); my $host = shift; my $temp_file; # Place to store startup-config # Debug trace trace_location('begin') if $debug; # Define local variables $temp_file = "$host.$random.$$"; # Create temporary file to hold startup-config die "Cannot touch $tftp_dir/$temp_file\n" unless touch_file("$tftp_dir/$temp_file", 0666); # Write startup-config to tftp server download_config( { host => $host, file => "$tftp_path/$temp_file"} ); unless (-s "$tftp_dir/$temp_file" > 0) { die "Unable to write config file to $temp_file\n"; } # Find boot statements open $file, '<', "$tftp_dir/$temp_file" or die "Fatal: Cannot open $tftp_dir/$temp_file\n"; while (my $line = <$file>) { push @boot, $line if $line =~ /^boot /; } close $file or warn "Cannot close $temp_file: $!\n"; unless ($debug) { unlink ("$tftp_dir/$temp_file") or warn "Cannot erase $temp_file: $!"; } # Debug info if ($debug) { say "For $host, the 'boot' lines are:"; for my $line (@boot) { print " $line"; } } # Debug trace trace_location('end') if $debug; return \@boot; } ######################################################################## # Gather information after do_the_work() ######################################################################## sub info_after { my $alive_ref; # List of targets which responded to snmpget my $bootline; # Temporary holder for next 'boot system' line my $dead; # Target which wasn't responding when I quit # waiting # Debug trace trace_location('begin') if $debug; # Notify operator print_it("Figuring out which files landed where"); # Loop through targets, gathering file information for my $host (@target) { # Gather information about the flash devices characterize_flash_devices($host); # Find the devices where I have placed images # Walk flash devices FLASH_DEVICE: for my $j (sort keys %{$flash_device_name{$host}}) { next FLASH_DEVICE if $flash_device_status{$host}->{$j} eq 'undefined'; next FLASH_DEVICE if $flash_device_name{$host}->{$j} =~ /'dfc'/; next FLASH_DEVICE unless $flash_device_size{$host}->{$j} > 0; next FLASH_DEVICE unless $num_flash_files{$host}->{$j} > 0; # Walk flash files for my $k (sort keys %{$flash_file_name{$host}->{$j}}) { my ($boot_device, $boot_image, $old_image, $this_device, $this_image); $boot_device = $boot_image_dev{$host} if defined $boot_image_dev{$host}; $boot_image = $boot_image{$host} if defined $boot_image{$host}; $this_device = $flash_device_name{$host}->{$j}; $this_image = $flash_file_name{$host}->{$j}->{$k}; $old_image = $old_image{$host} if defined $old_image{$host}; # Pick out the (first) device on which this image lives if (defined $boot_image and $this_image eq $boot_image) { $boot_image_dev{$host} = $this_device unless defined $boot_image_dev{$host}; } if (defined $new_image and $this_image eq $new_image) { $new_image_dev{$host} = $this_device unless defined $new_image_dev{$host}; } if (defined $old_image and $this_image eq $old_image) { $old_image_dev{$host} = $this_device unless defined $old_image_dev{$host}; } } # End 'Walk flash files' } # End 'Walk flash devices' # If I can't find where I put $new_image, complain # This is likely a fatal problem ... how could I complain more # loudly? if (not defined $new_image_dev{$host}) { print_it("\nUnable to find $new_image on $host"); $error{$host} = "Problem finding $new_image on flash"; } # Add new image to the boot stanza if (defined $new_image_dev{$host}) { $bootline = 'boot system flash ' . "$new_image_dev{$host}:$new_image"; push @{$new_boot{$host}}, $bootline; } # Add old image to the boot stanza if (defined $old_image_dev{$host}) { $bootline = 'boot system flash ' . "$old_image_dev{$host}:$old_image{$host}"; push @{$new_boot{$host}}, $bootline; } # Add boot image to the boot stanza if (defined $boot_image_dev{$host}) { $bootline = 'boot system flash ' . "$boot_image_dev{$host}:$boot_image{$host}"; push @{$new_boot{$host}}, $bootline; } # Sanity check confess 'Unable to create boot stanza' unless @{$new_boot{$host}} > 0; # Debug info if ($debug) { say "host = $host"; if (defined $new_image_dev{$host}) { say "new_image_dev{$host} = $new_image_dev{$host}"; } if (defined $old_image_dev{$host}) { say "old_image_dev{$host} = $old_image_dev{$host}"; } if (defined $boot_image_dev{$host}) { say "boot_image_dev{$host} = $boot_image_dev{$host}"; } } # Entertain operator print $BANG if $mode eq 'interactive'; } # Make things look pretty say('') if $mode eq 'interactive'; # Poll management agents until they recover ($alive_ref, $dead) = poll_by_snmp(\@target, 'sysDescr.0', $wait); # Process results if (@$alive_ref eq @target) { print_it("All targets are responding\n"); } else { print_it("Host $dead is not responding\n"); } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Determine whether or not there are any 'mica-modem' files on flash. # These files, also called 'portware', are auto-downloaded to digital # modems on boot -- typically found on AS5200s and Cisco 36xx. # Also, I'm only smart enough to keep the highest numbered # mica-modem file: I will erase the rest. ######################################################################## sub find_mica_modem { my $device; # Device name: either 'flash' or 'bootflash' my $flash_num; # Device number of 'flash' or 'bootflash' my @mica_image; # Images with 'mica-modem' in their name my $mica_image; # Iterates across @mica_image my $highest; # Highest version number found to date my $survivor; # The mica-modem file I will preserve my $host = shift; my $ver; # Iterates through version numbers # Debug trace trace_location('begin') if $debug; # Find the flash device called "flash" FLASH_DEVICE: for my $j (sort keys %{$flash_device_name{$host}}) { if ( $flash_device_name{$host}->{$j} =~ /flash$/ ) { $device = $flash_device_name{$host}->{$j}; $flash_num = $j; last FLASH_DEVICE; } } # Identify the names of any 'mica-modem' files on $device for my $k (sort keys %{$flash_file_name{$host}->{$flash_num}}) { if ($flash_file_name{$host}->{$flash_num}->{$k} =~ /mica-modem/ ) { push (@mica_image, $flash_file_name{$host}->{$flash_num}->{$k}); } } # If more than one 'mica-modem' file exists, figure out which is the # most recent $highest = 0; if (@mica_image > 1) { for $mica_image (@mica_image) { ($ver) = ($mica_image) =~ s/\.//; $ver =~ s/mica-modem-pw//; $ver =~ s/bin//; if ($ver > $highest) { $highest = $ver; $survivor = $mica_image; } } } else { $survivor = $mica_image[0]; } # Debug info if ($debug) { if (defined $survivor) { say " The mica-modem image I will preserve is: $device:$survivor"; } else { say ' Found no mica-modem image'; } } # Debug trace trace_location('end') if $debug; return ($device, $survivor); } ######################################################################## # Modify 'boot system' and 'bootldr' lines ######################################################################## sub modify_boot_lines { # Debug trace trace_location('begin') if $debug; # Notify operator say 'Modifying boot lines' if $mode eq 'interactive'; # Loop through targets, uploading a config snippet to update the boot stanza TARGET: for my $target (@target) { my ($random, $temp_file); $random = int(rand 9999); # Define temp_file $temp_file = "$target.$random.$$"; # Verify existence and permissions touch_file("$tftp_dir/$temp_file", 0664) or die "Cannot touch $tftp_dir/$temp_file modify_boot_lines\n"; # Open config file open my $file, '>', "$tftp_dir/$temp_file" or die "Cannot open $temp_file: $!\n"; # Add the 'no boot' lines if (defined $old_boot{$target}[0]) { my $boot_ref = $old_boot{$target}; for (my $j = 0; $j < @$boot_ref; $j++) { print {$file} 'no ' . $boot_ref->[$j]; } print {$file} "no boot system\n"; } # Add the new 'boot system flash' lines if (defined $new_boot{$target}[0]) { my $boot_ref = $new_boot{$target}; LINE: for (my $j = 0; $j < @$boot_ref; $j++) { next LINE if $boot_ref->[$j] eq $CR; push @{$desired_boot{$target}}, $boot_ref->[$j]; print {$file} $boot_ref->[$j] . "\n"; } } # # Add the bootldr line # if (defined $boot_image{$target}) { # if ($formatted{$target} and $uploaded_boot_image{$target}) { # my $bootldr = "boot bootldr $boot_device{$target}:$boot_image{$target}"; # push @{$desired_boot{$target}}, "$bootldr\n"; # print {$file} "$bootldr\n"; # } # } # Be neat print {$file} "end\n"; # Close the file close $file or warn "Cannot close $temp_file: $!\n"; # Debug info if ($debug > 1) { my $config = "$tftp_dir/$temp_file"; say 'I will upload the following config snippet to running-config'; open my $fh, '<', $config or die "Cannot open $file: $!\n"; for my $line (<$fh>) { print $line; } close $fh or warn "Cannot close $config: $!"; say 'desired_boot looks like this:'; for my $target (sort keys %desired_boot) { for my $line (@{$desired_boot{$target}}) { say $line; } } } # Sanity check unless (@{$desired_boot{$target}} > 0) { print_it("\nBoot stanza for $target is broken"); next TARGET; } # Modify running-config if (upload_config($target, "$tftp_path/$temp_file")) { say 'Successfully uploaded boot changes' if $debug; } else { print_it("\nFailed to upload boot changes"); $error{$target} .= ' Failed to upload boot changes'; next TARGET; } # Erase the file unless ($debug) { unlink "$tftp_dir/$temp_file" or print_it("Cannot erase $temp_file\n"); } # Save changes if (write_mem($target)) { say 'Successfully copied running-config to startup-config' if $debug; } else { print_it("\nFailed to save config file changes"); $error{$target} .= ' Failed to save config file changes'; next TARGET; } # Now that I believe I have modified the 'boot' lines, go back and get them my $boot_ref = get_boot_lines($target); for (my $j = 0; $j < @$boot_ref; $j++) { $actual_boot{$target}[$j] = $boot_ref->[$j]; chomp $actual_boot{$target}[$j]; } # Verify that the actual boot lines ended up being what I want them to be for (my $j = 0; $j < @{$desired_boot{$target}}; $j++) { unless ($desired_boot{$target}[$j] eq $actual_boot{$target}[$j]) { print_it("\nBoot lines are broken"); $error{$target} .= ' Boot lines are broken'; if ($debug) { say "Desired boot line is: $desired_boot{$target}[$j]"; say "Actual boot line is: $actual_boot{$target}[$j]"; } next TARGET; } } # Entertain the operator print $BANG if ($mode eq 'interactive' and not $debug);; } # Make things look pretty say "\n" if $mode eq 'interactive'; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Tell operator what I did ######################################################################## sub print_after { my $j; # Iterates over flash devices my $k; # Iterates over file names on a flash device 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 report if ($dome) { say "\n# Here is what I did\n"; } else { say "\n# Here is what I would have done, had you been serious"; } # Iterate through @target for my $host (@target) { log_it("Upgraded $host to $new_image"); $shit_happens++ if (defined $error{$host} and $error{$host} ne $EMPTY_STR); print <{$j} eq 'undefined'; next FLASH_DEVICE if $flash_device_name{$host}->{$j} =~ /dfc/; next FLASH_DEVICE unless $flash_device_size{$host}->{$j} > 0; next FLASH_DEVICE unless $num_flash_files{$host}->{$j} > 0; FLASH_FILE: for my $k (sort keys %{$flash_file_name{$host}->{$j}}) { next FLASH_FILE if $flash_file_status{$host}->{$j}->{$k} eq 'deleted'; printf "%-60s %-15s\n", $flash_device_name{$host}->{$j} . $COLON . $flash_file_name{$host}->{$j}->{$k}, $flash_file_status{$host}->{$j}->{$k}; } } # End 'Walk flash devices' } # End 'Iterate through @target' # If an error occured, talk about it if ($shit_happens > 0) { print "\n\n\n\n"; print < 0) { say(''); say 'The following targets contain services modules. I did not flash these modules; you must perform that step yourself:'; say join $SPACE, @targets_with_modules; } # Make things look pretty say "\nEnding $PROGRAM_NAME" if $mode eq 'interactive'; log_it("Ending $PROGRAM_NAME"); # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Tell the operator what I will do ######################################################################## sub print_before { # Debug trace trace_location('begin') if $debug; # If running from cron, don't print report return 1 if $mode eq 'batch'; print "\n"; print < 0) { say "The following targets contain services modules. I will not flash these modules; you must perform that step yourself: @targets_with_modules\n"; } if ($dome) { say "This is the real thing: I will upgrade devices"; } else { say "This is a dry run, no devices will be upgraded"; } say "Stalling for $long seconds to give you time to cancel ...\n"; sleep $long; say "OK, let's roll\n"; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Print debug report ######################################################################## sub print_debug { my $j; # Iterates over flash devices my $k; # Iterates over file names on a flash device return 1 unless $debug; # Debug trace trace_location('begin') if $debug; print <{$j} eq 'undefined'; next FLASH_DEVICE if $flash_device_status{$host}->{$j} =~ 'dfc'; next FLASH_DEVICE unless $flash_device_size{$host}->{$j} > 0; next FLASH_DEVICE unless $num_flash_files{$host}->{$j} > 0; FLASH_FILE: for my $k (sort keys %{$flash_file_name{$host}->{$j}}) { printf "# %-15s %-50s\n", $flash_device_name{$host}->{$j}, $flash_file_name{$host}->{$j}->{$k}; } } } print "#\n#\n"; print < 0) { # Make things look pretty say('') if $mode eq 'interactive'; # Remove entries which failed checks for my $remove (@remove) { say "Removing $remove" if $debug > 3; delete $old_device{$remove}; delete $old_image{$remove}; push @nuked, $remove; } } # Debug trace trace_location('end') if $debug; return @nuked; } ######################################################################## # Check for error conditions ######################################################################## sub sanity_check { my $dir_mode; # Permissions on $tftp_dir my @remove; # List of hosts which failed checks my $st; # File::stat object # Debug trace trace_location('begin') if $debug; # Notify operator print_it('Sanity check...'); # Check tftp directory permissions die "Bad permissions on $tftp_dir\n" unless check_dir_permissions($tftp_dir); $st = stat($tftp_dir) or confess "$tftp_dir does not exist: $!"; $dir_mode = sprintf "%lo", $st->mode & 07777; die "Fatal: $tftp_dir has mode $dir_mode, must be 777\n" unless $dir_mode eq '777'; # Loop through targets for the first time, looking for obvious problems say 'Round One sanity check' if $debug; TARGET: for my $target (@target) { my $version; # 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; } # Verify that this device understands how to copy config files via SNMP. # Ideally, I would ask the device a sample question. But I can't think # how to do this. So I'll just check version: Cisco introduced this # capability in 12.0 ($version) = ($os_version{$target} =~ /^(\d+)\./); if ($version < 12) { log_it("$target does not support SNMP/TFTP copy, ignoring"); say "$target does not support SNMP/TFTP copy, ignoring" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } # If $target's hardware is not the same as that of $target[0], ignore if ($box{$target[0]} ne $box{$target}) { # Just because the hardware types aren't identical doesn't mean that # these devices can't use the same image. if the hardware types share # the same initial string plus first two digits, assume that they are # the same, for the purposes of running images. This scheme incorrectly # flags cat4500s and cat4900s as belonging to different clades. And it # probably incorrectly flags some devices as belonging to the same clade # when in fact they don't my ($target0, $targetN); $target0 = $box{$target[0]}; $targetN = $box{$target}; given ($target0) { when (/catalyst/) { $target0 = substr $target0, 0, 10; $targetN = substr $targetN, 0, 10; } when (/cat/) { $target0 = substr $target0, 0, 5; $targetN = substr $targetN, 0, 5; } when (/cisco/) { $target0 = substr $target0, 0, 7; $targetN = substr $targetN, 0, 7; } } if ($target0 eq $targetN) { say "Hardware types $target0 and $targetN are basically the same, continuing" if $debug > 3; } else { log_it("$target is not of type $box{$target[0]}, skipping"); say "$target is not of type $box{$target[0]}, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } } # If target's hardware is a WAP, skip if ($box{$target} =~ /AIRAP/i) { log_it("$target is a WAP, and I don't support WAPs, skipping"); say "target is a WAP, and I don't support WAPs, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } # Entertain operator print $BANG if $mode eq 'interactive'; } # End first loop # Make things look pretty say('') if $mode eq 'interactive'; # Remove entries which failed checks prune_basic(@remove); undef @remove; # Loop through targets for second time, analyzing flash devices and images say 'Round Two sanity check' if $debug; TARGET: for my $target (@target) { # Gather information about the flash devices if (characterize_flash_devices($target)) { say "\nSuccessfully characterized flash devices" if $debug > 2; } else { log_it("Unable to characterize flash on $target, skipping"); say "Unable to characterize flash on $target, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } # Identify running image file name and device ($old_device{$target}, $old_image{$target}) = which_image($target); if ($old_device{$target} eq 'unknown' or $old_image{$target} eq 'unknown') { log_it("Unknown device:image beneath $target, skipping"); say "Unknown device:image beneath $target, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } # If the old image isn't on the tftp server, download it from $target unless (-s "$tftp_dir/$old_image{$target}") { say "$tftp_dir/$old_image{$target} doesn't exist, downloading from $target" if $debug; my %arg = ( device => $old_device{$target}, host => $target, image => $old_image{$target}, wait => $wait ); unless (download_image(\%arg)) { log_it("Unable to download old image from $target, skipping"); say "Unable to download old image from $target, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } } # Entertain operator print $BANG if $mode eq 'interactive'; } # End second loop # Make things look pretty say('') if $mode eq 'interactive'; # Remove entries which failed checks prune_local(@remove); prune_basic(@remove); undef @remove; # Loop through targets for third time, analyzing boot lines say 'Round Three' if $debug; TARGET: for my $target (@target) { my $boot_ref; # Identify 'boot' lines $boot_ref = get_boot_lines($target); if (defined $boot_ref) { say "\nSuccessfully acquired boot lines" if $debug; } else { log_it("Unable to acquire boot lines from $target, skipping"); say "Unable to acquire boot lines from $target, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } $old_boot{$target} = $boot_ref; # Determine whether or not a boot image exists ($boot_device{$target}, $boot_image{$target}) = find_boot_image($target); # If the 'boot' image isn't on the tftp server, download it from $target if (defined $boot_image{$target}) { unless (-s "$tftp_dir/$boot_image{$target}") { say "$tftp_dir/$boot_image{$target} doesn't exist, downloading from $target" if $debug; my %arg = ( device => $boot_device{$target}, host => $target, image => $boot_image{$target}, wait => $wait ); unless (download_image(\%arg)) { log_it("Unable to download boot image from $target, skipping"); say "Unable to download boot image from $target, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } } } # Fix permissions on boot image check_image("$tftp_dir/$boot_image{$target}") if defined $boot_image{$target}; # Entertain operator print $BANG if $mode eq 'interactive'; } # End third loop # Make things look pretty say('') if $mode eq 'interactive'; # Remove entries which failed checks prune_local(@remove); prune_basic(@remove); undef @remove; # Loop through targets for fourth time, looking for miscellaneous issues say 'Round Four' if $debug; TARGET: for my $target (@target) { # Determine whether or not there are any mica-modem files ($mica_device{$target}, $mica_file{$target}) = find_mica_modem($target); # If the mica-modem image isn't on the tftp server, download it # from $target if (defined $mica_file{$target}) { unless (-s "$tftp_dir/$mica_file{$target}") { say "$tftp_dir/$mica_file{$target} doesn't exist, downloading from $target" if $debug; unless (download_image($target, $mica_device{$target}, $mica_file{$target})) { log_it("Unable to download mica modem file from $target, skipping"); say "Unable to download mica modem file from $target, skipping" if $debug; print $DOT if $mode eq 'interactive'; push @remove, $target; next TARGET; } } } # Fix permissions on mica-modem file check_image("$tftp_dir/$mica_file{$target}") if defined $mica_file{$target}; # Notice whether or not there are any SPA modules if ($box{$target} =~ /cat65/) { my $model_ref; # Walk CISCO-STACK-MIB::moduleModel say 'Walking CISCO-STACK-MIB::moduleModel' if $debug > 3; $model_ref = snmpWalk( {host => $target, oid => 'moduleModel'} ); # Count the number of SPA modules for my $varbind (@$model_ref) { push @targets_with_modules, $target if $varbind->{val} =~ /SPA|SSC/; } } # Entertain operator print $BANG if $mode eq 'interactive'; } # End fourth loop # Make things look pretty say "\n" if $mode eq 'interactive'; # Remove entries which failed checks prune_local(@remove); prune_basic(@remove); undef @remove; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Output help ######################################################################## sub HELP_MESSAGE { print <