#!/opt/vdops/bin/perl # This script automates the process of upgrading an APC Web/SNMP Card or # Network Management Card # V Who When What # --------------------------------------------------------------------------- # 1.9.0 skendric 02-21-2011 Upgrade to Netops 1.4.0 # 1.8.2 skendric 11-24-2010 Add PDU support # 1.8.1 skendric 06-25-2010 Allow $error{$target} to be undef # 1.8.0 skendric 03-30-2010 Upgrade to perl 5.10.1 # 1.7.1 skendric 05-30-2008 Support Battery Management System # 1.7.0 skendric 04-27-2008 Handle various Net::FTP errors # 1.6.8 skendric 03-21-2007 Stylistic mods # 1.6.7 skendric 03-06-2007 Hand $wait to poll_by_ping # 1.6.6 skendric 02-27-2007 Define $wait locally # 1.6.5 skendric 10-23-2006 Replace Object Values with OIDs # 1.6.4 skendric 09-22-2006 Change variable names # 1.6.3 skendric 08-28-2006 Increase timeouts after uploading images # 1.6.2 skendric 11-05-2005 Upgrade to new WI::VDOPS module structure # 1.6.1 skendric 10-12-2005 Add more progress messages # 1.6.0 skendric 05-09-2005 Support Netops.pm-1.2 # 1.5.1 skendric 05-16-2004 Add error checking to validate image choice # 1.5.0 skendric 05-10-2004 Migrate common functions to Netops.pm # 1.4.0 skendric 04-30-2004 Enhance command-line options # 1.3.4 skendric 02-02-2004 Documentation fixes # 1.3.3 skendric 01-07-2004 Fix bug in poll routine # 1.3.2 skendric 11-16-2003 Use Net::Ping::External # 1.3.1 skendric 08-10-2003 Minor bug fixes # 1.3.0 skendric 03-16-2003 Tighted scoping, changed DEBUG to $debug # 1.2.0 skendric 12-13-2002 Added support for AP961x # 1.1.0 skendric 11-17-2002 Added @version, improved build_target() # 1.0.0 skendric 10-21-2002 First Version # # 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: # -Upload the new AOS image via ftp # -Upload the new overlay image via ftp # # Requirements: # -The target(s) must be pingable # # -The following MIB modules stashed in /opt/vdops/share/snmp/mibs, # or wherever it is that you store MIB modules: # PowerNet-MIB # # -PERL modules: the WI::Netops collection # # # # Assumptions: # # # Tested on: # -APC 9606 Web/SNMP card, APC 961x Network Management Card, # SmartUPS, Symmetra, Silcon # -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 # -Type "apc-upgrade" to see the command-line options # -Try it out # # # Caveats: # # # Known Bugs: # # # To do: # -Test username/password foo # # 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 English qw( -no_match_vars ); use Getopt::Std; use Net::FTP; use WI::Netops::APCTools 1.2.2; 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 $aos_image; # Name of AOS image to upload my %auth; # Hash of passwords keyed by usernames, reflecting # the locally defined possibilities of your APC units my $srcdir; # Directory holding files my $hardware; # Overlay type which we are upgrading: # Silcon, SmartUps/Matrix, Symmetra my $overlay_image; # Name of overlay image to upload my %new_aos; # Name of new AOS image (should be $aos_image!) my %new_overlay; # Name of new overlay image (should be # $overlay_image!) my %old_aos; # Name of current AOS image my %old_overlay; # Name of current overlay image my $password; # Password for ftp'ing my $username; # Username for ftp'ing my $wait; # Seconds to wait after uploading an image file # before giving up and skipping to the next device # Define global variables $program_name = 'apc-upgrade'; $usage = 'Usage: apc-upgrade -s {yes|no} [-d {integer}] -i {aos_image} -o {overlay_image} [-u {username}] [-p {passwd}] [-a | -e {expr} | -f {filename} | target1 target2 target3 ...]'; $version = '1.9.0'; # Define user-configurable variables # Directories $srcdir = '/tftpboot/whatever'; # Timing. This is how long I'll wait for the device to reboot after uploading # an image file $wait = 240; # Grab arguments getopts('ad:e:f:i:o:p:s:u:', \%option); @target = @ARGV; $aos_image = $option{i}; $overlay_image = $option{o}; $username = $option{u} if defined $option{u}; $password foo # Check arguments die 'Must specify -i aos_image' unless defined $aos_image; die 'Must specify -o overlay_image' unless defined $overlay_image; die 'Must specify -p password' unless defined $password; die 'Must specify -u username' unless defined $username; # Identify which overlay type we are upgrading given ($overlay_image) { when (/bms/) { $hardware = 'bms' } when (/dp3e/) { $hardware = 'silcon' } when (/rpdu/) { $hardware = 'pdu' } when (/sumx/) { $hardware = 'sumx' } when (/sy3p/) { $hardware = 'symmetra-3p' } when (/sy/) { $hardware = 'symmetra' } default { die 'Unknown UPS hardware type' } } # 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 Netops 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 major errors info_before(); # Gather more information print_before(); # Tell operator what I will do do_the_work(); # Reset devices info_after(); # Gather information print_after(); # Tell the operator what I did } ##### End Main Program ############################################### ######################################################################## # Ask $target whether or not the upload/download operation succeeded # Currently, I don't use this routine ######################################################################## sub check_ftp_load { my $result; # Status of upload operation my $target = shift; # Debug trace trace_location('begin') if $debug; # Every few seconds, check on the status of the upload operation # If it is inProgress, print a "!" character to entertain the operator # Otherwise, continue STATUS: for (my $t = 0; $t < 20; $t++) { my %arg; print $BANG if $mode eq 'interactive'; # inProgress sleep $short; say "Getting mfiletransferStatusLastTransferResult.0" if $debug > 3; %arg = (host => $target, oid => 'mfiletransferStatusLastTransferResult.0' ); $result = snmpGet(\%arg); last STATUS if defined $result; } say('') if $mode eq 'interactive'; # Figure out what happened and notify the operator given ($result) { when (undef) { die 'Upload may have failed: not responding'; } when ('iThinkThisMeansSuccessfulSK') { say 'Upload succeeded: Success-SK'; } when ('lastFileTransferResultSuccessful') { say 'Upload succeeded'; } when ('lastFileTransferResultNotAvailable') { die 'Upload may have failed: result not available'; } when ('lastFileTransferResultFailureUnknown') { die 'Upload failed: unknown reason'; } when ('lastFileTransferResultFailureServerInaccessible') { die 'Upload failed: server ServerInaccessible'; } when ('lastFileTransferResultFailureServerAccessDenied') { die 'Upload failed: AccessDenied'; } when ('lastFileTransferResultFailureFileNotFound') { die 'Upload failed: FileNotFound'; } when ('lastFileTransferResultFailureFileTypeUnknown') { die 'Upload failed: FileTypeUnknown'; } when ('lastFileTransferResultFailureFileCorrupted') { die 'Upload failed: FileCorrupted'; } default { die 'Upload failed: $result'; } } # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Do the work: upload images ######################################################################## sub do_the_work { my $dead; my $direction = 'put'; my $dstfile; my $responded_ref; my $srcfile; # Debug trace trace_location('begin') if $debug; # Loop through @target, uploading images for my $target (@target) { # Announce start say '--------------------------------------------------------' if $mode eq 'interactive'; print_it("Beginning to process $target"); # If this is for real if ($dome) { my (%arg, $result); # Build argument hash %arg = ( target => $target, username => $username, password foo local_file => "$srcdir/$aos_image", remote_file => $aos_image, direction => $direction ); # Upload AOS image $result = ftp_file(\%arg); if ($result) { print_it(" Uploaded AOS image: $aos_image"); } else { print_it(" Cannot upload AOS image: $aos_image"); goto FINISH; } sleep $short; # Give the device time to initiate a reboot # Poll device until it answers ($responded_ref, $dead) = poll_by_ping([$target], $wait); if (defined $dead) { print_it(" $target is no longer answering pings"); $error{$target} = 'Cannot ping'; goto FINISH; } sleep $long; sleep $long; # Give the device time to recover # At this point, the device has (hopefully) rebooted, using the # new AOS image. And it has deleted the old AOS image and the old # overlay image from its file system. Sometimes, the device can # use Radius for authentication, sometimes it falls back to its # locally defined accounts instead. Try both. # Upload overlay image $arg{local_file} = "$srcdir/$overlay_image"; $arg{remote_file} = $overlay_image; $result = ftp_file(\%arg); if ($result) { print_it(" Uploaded overlay image: $overlay_image"); } # Otherwise, try our hard-coded usernames/passwords else { # Walk through each of the username/passwords combinations # which the operator has hard-coded into the script AUTH: for my $uname (keys %auth) { $arg{username} = $uname; $arg{password} = $auth{$uname}; $result = ftp_file(\%arg); if ($result) { print_it(" Uploaded overlay image: $overlay_image"); sleep $short; # Give the device time to initiate a reboot last AUTH; } } # End 'AUTH' # Admit defeat unless ($result) { print_it(" Cannot upload overlay image: $overlay_image"); goto FINISH; } } # End 'Try hard-coded usernames/passwords' # Poll device until it answers ($responded_ref, $dead) = poll_by_ping([$target], $wait); if (defined $dead) { print_it(" $target is no longer answering pings"); $error{$target} = 'Not answering pings'; goto FINISH; } sleep $mid; # Give the device time to recover } else { sleep $short; } # Announce completion FINISH: print_it("Done processing $target"); say "-------------------------------------------------------\n\n" if $mode eq 'interactive'; } # Give the last device time to recover sleep $long; sleep $long; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Gather information after file uploads ######################################################################## sub info_after { # Debug trace trace_location('begin') if $debug; # Notify operator print_it('Querying targets for image versions'); # If we are running in test mode, pause briefly, then return unless ($dome) { sleep $short; return 1; } # Loop through targets, gathering file information TARGET: for my $target (@target) { # If we can't ping the target, skip to the next one unless (ping_it($target)) { print_it("\nCannot ping $target"); $new_aos{$target} = $DASH; $new_overlay{$target} = $DASH; next TARGET; } # Update @sysDescr $sysDescr{$target} = get_sys_descr($target); # Identify running image names ($new_aos{$target}, $new_overlay{$target}) = which_apc_image($target); unless ( defined $new_aos{$target} and defined $new_overlay{$target}) { print_it("\nUnknown images beneath $target"); $new_aos{$target} = $DASH; $new_overlay{$target} = $DASH; } } # Make things look pretty say "\n"; # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Gather information ... and perform more error checks ######################################################################## sub info_before { my @remove; # Debug trace trace_location('begin') if $debug; # Notify operator print_it('Gathering additional information...'); # Loop through targets, gathering more info TARGET: for my $target (@target) { # Identify running image names ($old_aos{$target}, $old_overlay{$target}) = which_apc_image($target); unless (defined $old_aos{$target} and defined $old_overlay{$target}) { print_it("\nUnknown images beneath $target, ignoring"); push @remove, $target; next TARGET; } # Entertain operator print $BANG if $mode eq 'interactive'; } # Make things look pretty say "\n" if $mode eq 'interactive'; # Remove entries which failed checks prune_local(@remove); prune_basic(@remove); # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Tell operator what I did ######################################################################## sub print_after { my $shit_happens = 0; # Debug trace trace_location('begin') if $debug; # If running from cron, don't print report return 1 if $mode eq 'batch'; say "\n# Here is what I did"; # If we are running in test mode, then return unless ($dome) { print "Running in test mode, cannot print a meaningful report\n\n"; return 1; } # Iterate through @target for my $target (@target) { log_it("Upgraded $target to $aos_image/$overlay_image") if $dome; $shit_happens++ if defined $error{$target}; } print "\n"; print < 0) { print "\n\n\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"; delete $old_aos{$remove}; delete $old_overlay{$remove}; push @nuked, $remove; } } # Debug trace return @nuked; } ######################################################################## # Sanity check ######################################################################## sub sanity_check { my @remove; my $type; # Debug trace trace_location('begin') if $debug; # Notify operator print_it('Sanity check...'); # Check images for issues check_image("$srcdir/$aos_image", 0444); check_image("$srcdir/$overlay_image", 0444); # Identify image type given ($overlay_image) { when (/bms/) { $type = 'bms' } when (/dp/) { $type = 'dp' } when (/rpdu/) { $type = 'pdu' } when (/sumx/) { $type = 'sumx' } when (/sy/) { $type = 'sy' } } # Loop through targets, removing non-APC devices TARGET: for my $target (@target) { # Identify manufacturer unless ($manufacturer{$target} eq 'APC') { say "\nManufacturer of $target is not APC, ignoring" if $debug; push @remove, $target; next TARGET; } # Skip AP963x cards if ($apc_card_model{$target} =~ /AP963/) { say "\n$target managed by $apc_card_model{$target}, which I don't support, ignoring"; 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}) { say "\n$target is not of type $box{$target[0]}, ignoring" if $debug; push @remove, $target; next TARGET; } # Correlate image with UPS model unless ($box{$target} =~ /$type/ or $box{$target} eq 'unknown') { say "\nI don't believe that $overlay_image will function correctly on $target, ignoring" if $debug; push @remove, $target; next TARGET; } # Entertain operator print $BANG if $mode eq 'interactive'; } # Make things look pretty say "\n" if $mode eq 'interactive'; # Remove entries which failed checks prune_basic(@remove); # Debug trace trace_location('end') if $debug; return 1; } ######################################################################## # Output help ######################################################################## sub HELP_MESSAGE { print <