#!/usr/local/bin/perl -w # This program is intended to parse the patchdiag.xref file downloaded from # the sunsolve ftp site. It parses the file, and returns a list of patches that # need to be evalutated for installation. # @(#) PatchReport_Solaris7 1.14@(#) (Shamblin) 05/01/99 13:48:23 # Modified by Waider Jan/Feb 2002 # Cleaned up warnings when run under Perl -w # Debugged contract operation # Handle patches that aren't zipped (.tar, .tar.Z) # NB this allows one script to be used for <= Sol 2.6 and >= Sol 7 # Disregard (for now) patches that don't show up in the checksum file # Optionally MD5-sum the local copy of a patch before fetching a new copy # of same. # Enable the -l flag # Move file fetches to a single function, to centralise option checking # and diagnostics # Allow preserving of downloaded xref & CHECKSUM files # Create -Z dir if it doesn't exist (otherwise files end up in /!) # Use a sub to do rm -rf # Added proxy support to non-contract mode # Allowed non-contract mode to use xref and CHECKSUM files # Handle missing patches (usu. non-contract mode) # Cleaned up the fastpatch output # Smarter default base URL # If -Q is specified, then imply -q # Moved file cleanups to an exit handler, so files get cleaned up if # the program dies # Added cookie-login support, including redirecting to Sun's choice of # URL if necessary # Bumped the version number # # TODO # Better error reporting on fetch failure (auth failed, etc.) # Is it possible to figure out the correct order to apply patches in, # to avoid multiple runs? # Use "patch-not-installed" errors to generate a machine-specific # exclude file, keyed against the contents file in /var/sadm/install # (so we know when it's potentially out of date) # Checksum subroutine # Clean up: remove unpacked dirs unconditionally # Clean up: remove old versions of a patch # Detect if xref or checksum file is trashed # # Copyright (c) 1997 by W. Joseph Shamblin. All rights reserved. # Permission is granted to reproduce and distribute this program # with the following restrictions: # 1) This copyright notice and the author identification below # must be left intact in the program and in any copies. # 2) Any modifications to the program must be clearly identified # in the source file. # # UNIX Systems Administrator # Department of Computer Science # Duke University, Durham, NC # Phone: 919.660.6582 # Email: wjs@cs.duke.edu # # # THIS SOFTWARE IS PROVIDED AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. YOU ARE RESPONSIBLE # FOR ANY DAMAGE THIS MIGHT DO TO YOUR MACHINES!!! IN NO EVENT SHALL THE # AUTHOR OF THIS PROGRAM BE LIABLE FOR DAMAGE THIS PROGRAM CAUSES. # Load all needed modules use English; use Digest::MD5; use Net::FTP; use LWP::UserAgent; use HTTP::Request; use HTTP::Cookies; # new Sun auth mechanism uses cookies use URI::Escape; # this will upset people. need an alternative. use FileHandle; use Getopt::Std; use strict; # muahah. autoflush STDERR 1; autoflush STDOUT 1; # All our config options use vars qw( $opt_A $opt_a $opt_b $opt_c $opt_d $opt_e $opt_E $opt_f $opt_F $opt_g $opt_h $opt_i $opt_k $opt_l $opt_L $opt_N $opt_n $opt_p $opt_Q $opt_q $opt_R $opt_r $opt_S $opt_s $opt_v $opt_X $opt_Z ); getopts('Aa:b:cdE:e:Ff:g:hikL:lN:np:Q:qRrS:s:vX:Z:'); # account name, only users with contract accounts at sunsolve can get # patchdiag.xref file. The account will be "ID/passwd". use vars qw( $account ); !defined $opt_a ? ($account = "PUT YOUR ACCOUNT HERE") : ($account = $opt_a ); #version number our $version_number = "1.25"; # Base url to fetch files from. Note this covers contract and # non-contract modes. my $baseurl = ( defined( $opt_b ) ? $opt_b : ( defined( $opt_n ) ? "ftp://sunsolve.sun.com/pub/patches" : "http://sunsolve.sun.com/private-cgi/patches" )); # Other flotsam and jetsam my ( $error, %nice_error_message, $patch_to_install, $patch_install_status, $skip_this_patch, $installing, %actual_checksum, %actual_name, %calculated_checksum, @needed, @excluded_patches, $needed_patches_fd, $answer_install_patch, $excluded_patches_fd, %patch_description, $retrieved_checksum, $md5, $subject, $patch_checksum, $patch_checksum_id, %showrev, @x_desc, $junk, $x_sec, $x_rec, $x_os, $x_arch, $x_obs ); # parse the options my $usage_message = <<"EOF"; USAGE: patchreport [-A] [-a "ID/passwd"] [-b "baseurl"] [-cd] [-E "/path/to/excluded_patches" (one patch per line)] [-e "103594 104117 105408 105616"] [-Ffghi] [-N "/path/to/recommended_list"] [-n] [-p "/path/to/patches"] [-Rr] [-S "/path/to/CHECKSUMS"] [-s "message"] [-X "/path/to/patchdiag.xref"] -A Prompt for account information -a SunSolve "ID/passwd" -c Prints patches which are current (UP) -d Debugging option -E "/path/to/excluded_patches" (one patch per line) -e Exclude patch IDs (e.g. 103594 sendmail patch for sparcs) -F Force patch installation without any questions -f Arguments to fastpatch, i.e. -f nsI for -n -s -I ( see fastpatch documentation ) Defaults for fast patch are the following: -n Never call installpatch (by default, fastpatch will fall back to installpatch when it can't find package matches) -s Save old files so the patch can be backed out. (Works for new style patches) -I Ignore backoutpatch failures (instead, the system state is updated as if the patch has been backed out) -g Grace period for shutdown (in seconds) -h Prints this message and exits -i Install patches -k Keep the downloaded patchdiag & checksum files -L "/path/to/file_with_list_of_patches" (one patch per line) -l Lazy mode - only retrieve patches that are needed -N "/path/to/recommended_list" -n No contract support (use Recommended patch list) -p "/path/to/patches" (default: /var/tmp/patches) -Q "/path/to/fastpatch" -q Use Casper Dik's fastpatch program to install patches -R Remove compressed patches and directories after installation, but not if uncompressing to a different directory ( -Z option ). In that case just clean up the uncompressed directory and leave compressed patch in place. -r Retrieve patches -S "/path/to/CHECKSUMS" -s Shutdown with "Message" -v Version number -X "/path/to/patchdiag.xref" -Z "/path/to/uncompress_patches"\n EOF # If the program is called with the -h option simply print # the usage message and exit. if (defined $opt_h) { print "$usage_message\n"; exit 0; } # If the program is called with the -v option simply print # the version, and the usage message and exit. if (defined $opt_v) { print "\n\tPatchReport version $version_number\n"; exit 0; } # If we are called with the -i (install option) make sure # that we have the appropriate permissions if ((defined $opt_i) and ($> or $< != 0)) { print "\n You must be root to install patches."; print "$usage_message\n"; exit 0; } # If no account info has been specified, then default to the public # site. This is the default mode of operation. if ($account eq "PUT YOUR ACCOUNT HERE" and !$opt_S and !$opt_X and !$opt_n and !$opt_A ) { $baseurl = ( defined( $opt_b ) ? $opt_b : "ftp://sunsolve.sun.com/pub/patches" ); } print STDERR "Using $baseurl as base\n" if $opt_d; # Let's figure out where fastpatch is located if possible. If not just # exit. my $INSTALL_PATCH_PROG; # if the user has specified the location of fastpatch, then default to # using it. if ( defined $opt_Q ) { $opt_q = 1; } if (defined $opt_q ) { if (defined $opt_Q) { $INSTALL_PATCH_PROG = $opt_Q; if ( ! -e $INSTALL_PATCH_PROG ) { print qq| Fastpatch was not found in that location. Please use -Q to the specify correct path to the fastpatch program.\n\n|; exit 1; } } elsif ( !defined $opt_Q) { $INSTALL_PATCH_PROG = `/bin/which fastpatch`; if ($INSTALL_PATCH_PROG =~ /no fastpatch in/) { print qq| Fastpatch program not found. Please use -Q to specify where the fastpatch program is located or add its directory to your path.\n\n|; exit 1; } } } else { $INSTALL_PATCH_PROG = "/usr/sbin/patchadd"; } # When summoned with the -A option the user will be asked # for the account name and password for SunSolve's FTP site if ( defined $opt_A) { print qq| Please provide the account and password in the form "ID/passwd" \n\naccount/passwd? |; chomp($account = ); } # The -p option allows for the output of the patches downloaded # to go into another directory. This is good for large sites that # share a common patch directory. my $patch_dir; if (!defined $opt_p) { $patch_dir = "/var/tmp/patches"; } elsif (defined $opt_p) { $patch_dir = "$opt_p"; } # The UserAgent used by the fetch process { package RequestAgent; use vars( '@ISA' ); @ISA = qw(LWP::UserAgent); sub new { my $self = LWP::UserAgent::new(@_); $self->agent("PatchReport/$main::version_number"); $self; } sub get_basic_credentials { my($self, $realm, $uri) = @_; return split(/\//, $main::account, 2); } } # set up web user agent # really should only do it for people with contract support, # but as long as we're requiring the LWP and HTTP modules above, # this won't hurt either. my $ua = new RequestAgent; my $cookie_jar = new HTTP::Cookies; # Pick up proxy settings from the environment $ua->env_proxy(); # Setp the arguments to fast patch. By default use -n -s -I # -n Never call installpatch (by default, fastpatch will # fall back to installpatch when it can't find package # matches) # -s Save old files so the patch can be backed out. # (Works for new style patches) # This should be optional for PatchReport # # -I Ignore backoutpatch failures # (instead, the system state is updated as if the patch # has been backed out) my $FAST_PATCH_ARGS; if ( defined $opt_f and defined $opt_q ) { map { $FAST_PATCH_ARGS = $FAST_PATCH_ARGS . "-$_ " } split(//,$opt_f); } elsif (defined $opt_q and !defined $opt_f ) { $FAST_PATCH_ARGS = "-n -s -I"; } # Get some information about who we are my @uname = split ' ',`uname -a`; # For Solaris 7 and later just use the last digit of $uname[2] # this is all a horrible kludge. curse you, sun. my ($old_part_prefix,$new_solaris_name) = split(/\./, $uname[2]); my $os; if ( $new_solaris_name < 7 ) { $new_solaris_name = "2.$new_solaris_name"; } if ($uname[5] eq "i386") { $os = $new_solaris_name . "_x86"; } elsif ($uname[5] eq "sparc") { $os = $new_solaris_name; } else { $os = $uname[2]; } # Print a nice little message to let the users know # what is going on when the script first starts up print<<"EOM"; Analyzing needed patches on your machine, this might take a minute or two depending on the options you chose, and/or your net connection. EOM # Cleanup handler # This gets called when the script exits, however the script exits. my @trashcan; END { print STDERR "The following files have been left on your system by patchreport:\n" if $opt_d; for my $f ( @trashcan ) { if ( !$opt_d ) { rm_rf( $f ); # XXX CAREFUL WHAT YOU PUT IN THE TRASH! } else { print STDERR "$f\n"; } } } ################################################ ############## File Retrieval ################## ################################################ # If the -X option or the option is used this means that the # patchdiag.xref file are stored locally. Since this is the # case we do not have to get the files from the net. my ( $xref_fd, $recommended_fd ); if (defined $opt_X and !$opt_n) { $xref_fd = new FileHandle "$opt_X", "r"; } elsif (!defined $opt_X and !defined $opt_n) { # -X was not used so we need to get the file from # Sunsolve's site die "xref: $!" unless $xref_fd = fetch( "patchdiag.xref", "/tmp/patchdiag_$$" ); } elsif (defined $opt_n and !defined $opt_N) { # If -n was used, and not -N then we need to retrieve the # file from the net. die "Recommended: $!" unless $recommended_fd = fetch( "$ {os}_Recommended.README", "/tmp/Recommended_$$" ); } elsif (defined $opt_n and defined $opt_N) { # if -n is used with -N then that means that the file is # stored locally. Set the file handle to the argument # given to -N. This should be the path to the # Recommended list. $recommended_fd = new FileHandle "$opt_N", "r"; } my $checksum_fd; if (!$opt_n) { # Make sure that we are not working in non-contract mode # if we are not, and the -S option is defined the # the path to the checksums file should be the argument # given to -S if (defined $opt_S) { $checksum_fd = new FileHandle "$opt_S", "r"; } else { die "CHECKSUM: $!" unless $checksum_fd = fetch( "CHECKSUMS", "/tmp/CHECKSUMS_$$" ); } } # play it again sam # if the -n option was not used putting us into non-contract mode # then we likely retreived the files from the net. We have to # go to the beginning to read the entire contents of the file. if (!defined $opt_n) { seek $xref_fd,0,0; seek $checksum_fd,0,0; } elsif ($opt_n) { seek $recommended_fd,0,0; } ################################################ ############ Formatting and parsing ############ ############ for the needed patches ############ ################################################ # Need to define these here for format to be happy my ( $x_id, $x_rev, $security, $recommended, $showrev, $x_desc); format patch_top = Patch-ID Security Recommended ID Description --------- -------- ----------- -- ------------------------ . ; format patch_out = @<<<<<<<< @<<<<<<< @<<<<<<<<<< @< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< "$x_id-$x_rev", $security, $recommended, $showrev{$x_id}||"-", $x_desc . ; $^="patch_top"; $~="patch_out"; # We need to get the current patches on the machine. the command showrev -p # will get the desired information. The map function will take every occurence # found in the showrev -p and perform the block operation on it. In this case # the operation is to double split the output, and then create an associative # array. map {my ($s_id,$s_rev) = split '-',(split)[1];$showrev{$s_id} = $s_rev} `showrev -p|sort`; # Show patches that apply to add-on programs like Disksuite and veritas if # we are called with the -o option #if (defined $opt_o) { #} # If we are in contract mode then we will need to take the output of the # patchdiag.xref file. We split the file, and then test to see if we have # the patch-id from the showrev -p array. We also do a little formatting # depending on whether or not the file is recommended or a security patch # or both. We also make use of the write function, to keep things formatted # nicely. if (!defined $opt_n) { while (<$xref_fd>) { # Skip comment lines next if /^#/; map {($x_id,$x_rev,$x_rec,$x_sec,$x_obs,$x_os,$x_arch,$x_desc) = (split(/\|/,$_))[0,1,3,4,5,7,8,10]} $_; if ( $opt_d ) { print STDERR "$x_id version $x_rev\n"; print STDERR " ",($showrev{$x_id} or "not"), " installed\n"; print STDERR " for $x_os (you have $os)\n"; print STDERR " is ", ($x_obs eq "O" ? "" : "not "), "obsolete\n"; } if ((!defined $showrev{$x_id} or $showrev{$x_id} < $x_rev) and ($x_arch =~ /$uname[5]\;|$uname[5]\.$uname[4]\;|all\;$uname[5]\;|all\;/) and ($x_os eq "$os") and ($x_obs ne "O")) { if ($x_rec eq "R") { $recommended = "Recommended "; } else { $recommended = " N/A "; } if ($x_sec eq "S") { $security = "Security "; } else { $security = " N/A "; } push @needed, "$x_id-$x_rev"; $patch_description{"$x_id-$x_rev"} = "$x_desc"; write; } # if we get a hit then that means we are current. So we should # print up in the patch revision place. elsif ((defined $showrev{$x_id} or $showrev{$x_id} = $x_rev) and ($x_arch =~ /$uname[5]\;|$uname[5]\.$uname[4]\;|all\;$uname[5]\;|all\;/) and ($x_os eq "$os") and defined $opt_c) { if ($x_rec eq "R") { $recommended = "Recommended "; } else { $recommended = " N/A "; } if ($x_sec eq "S") { $security = "Security "; } else { $security = " N/A "; } $showrev{$x_id} = "UP"; write; } } # If we are using the -n non-contract mode then don't do too much. # just parse the file, and get the basics like the patch-id } elsif (defined $opt_n) { while (<$recommended_fd>) { ($x_id,$x_rev) = map{split '-',(split)[0]} grep /^\d{6}/,$_ or next; ($junk,@x_desc) = split; $x_desc = join ' ', @x_desc; if (!$showrev{$x_id} or $showrev{$x_id} < $x_rev) { push @needed, "$x_id-$x_rev"; $security = ""; $recommended = ""; $patch_description{"$x_id-$x_rev"} = $x_desc; # crude, but effective, although it would be tidier to just pull # a directory listing of the site, perhaps. $actual_name{"$x_id-$x_rev"} = "$x_id-$x_rev" . ( $uname[ 2 ] <= 5.6 ? ".tar.Z" : ".zip" ); write; } # if we get a hit then that means we are current. So we should # print up in the patch revision place. elsif ((defined $showrev{$x_id} or $showrev{$x_id} = $x_rev) and defined $opt_c) { $showrev{$x_id} = "UP"; write; } } } ################################################ ############## MD5 checksum test ############### ################################################ # Now, if we weren't run with the -n option, we parse the checksums # file making an array of the values of patch-id to the actual # MD5 checksum as calculated by Sun. We need to go into the # multiline mode so we set the record separator (RS). if (!$opt_n) { $RS=''; map { if (($patch_checksum_id) = /^(\d{6}-\d{2}).(zip|tar(.Z)?)/m ) { $actual_name{$patch_checksum_id} = "$1.$2"; ($patch_checksum) = /MD5: (.*)/; $actual_checksum{$patch_checksum_id} = $patch_checksum if defined( $patch_checksum ); } } <$checksum_fd>; $RS="\n"; } my ( $get_status, $checksum_status ); format get_top = Patch-ID Checksum status Description --------- ------------------ -------------------- . format get_out = @<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< $get_status, $checksum_status,$patch_description{$_} . # If we are called with the -r switch get the patches, and check the # checksums for each file we will calculate our own checksums, and # compare then. If they match then they can be installed. If not # print an error message. This will be done for all of the needed # patches from, as determined from above. # If the patch already exists and -l has been specified, do an MD5 # check on the file. If it passes, don't fetch the file. $md5 = new MD5; if ( @needed and defined $opt_r ) { $checksum_status = "*NO CHECKSUMS*"; mkdir "$patch_dir",0755; mkdir "$opt_Z", 0755 if defined( $opt_Z ) && ! -d $opt_Z; print "\n**Retrieving Patches**\n"; print "Patch-ID Checksum status Description\n--------- ------------------ --------------------\n"; $^ = "get_top"; $~ = "get_out"; map { $get_status = "$_\t"; $~ = "get_out"; # It can happen that the checksum file is out of sync, so check # that we have a known name &c. for the file first. if ( !defined( $opt_n ) && !defined( $actual_name{$_})) { print STDERR "no name found for patch $_\n"; } else { # This is a bit of a mess and could be streamlined. But it works. # Wait! what if we already have the file? my $patch_fd; my $fetch = defined( $opt_r ); # This is to preserve original behaviour. if ( defined( $opt_l ) && defined( $opt_n ) && -f "$patch_dir/$actual_name{$_}" ) { $fetch = 0; } if ( -f "$patch_dir/$actual_name{$_}" && defined( $opt_l )) { $subject = new FileHandle "$patch_dir/$actual_name{$_}"; if (!defined $opt_n and defined( $subject )) { $md5->reset(); $md5->addfile($subject); $retrieved_checksum = $md5->hexdigest(); $calculated_checksum{$_} = $retrieved_checksum; if ($actual_checksum{$_} eq "$retrieved_checksum") { $checksum_status = "checksum match"; $fetch = 0; } else { $checksum_status = "*CHECKSUM FAILED*"; $fetch = 1; } } elsif ( !defined( $opt_n )) { warn "Failed to open $patch_dir/$actual_name{$_}: $!\n"; } else { # non-contract mode; we don't checksum. EOS. } } elsif ( ! -f "$patch_dir/$actual_name{$_}" ) { $checksum_status = "$patch_dir/$actual_name{$_} NOT FOUND"; $fetch = 1; } print STDERR "$checksum_status, \$fetch = $fetch\n" if $opt_d; if ( $fetch ) { $patch_fd = fetch( "$actual_name{$_}", "$patch_dir/$actual_name{$_}"); if ( !defined( $patch_fd )) { # REASONS FOR NOT FETCHING # 1. revision mismatch - we could recover from this, slightly # 2. file not available on public site $checksum_status = "*NOT FETCHED*"; } else { $patch_fd->close; if ( !defined $opt_n ) { $subject = new FileHandle "$patch_dir/$actual_name{$_}"; die "$actual_name{$_}: $!" unless defined( $subject ); if (!defined $opt_n and defined( $subject )) { $md5->reset(); $md5->addfile($subject); $retrieved_checksum = $md5->hexdigest(); $calculated_checksum{$_} = $retrieved_checksum; ($actual_checksum{$_} eq "$retrieved_checksum") ? ($checksum_status = "checksum match") : ($checksum_status = "*CHECKSUM FAILED*"); } } } } if ( defined( $opt_n )) { $checksum_status = "*NO CHECKSUMS*"; } write } } @needed; # We also need to check the checksum if the file is stored on # a local file system, hence called without the -r option. } elsif (@needed and defined $opt_i and !defined $opt_r) { map { $subject = new FileHandle "$patch_dir/$actual_name{$_}"; if (!defined $opt_n and $subject ne "") { $md5->reset(); $md5->addfile($subject); $retrieved_checksum = $md5->hexdigest(); $calculated_checksum{$_} = $retrieved_checksum; ($actual_checksum{$_} eq "$retrieved_checksum") ? ($checksum_status = "checksum match") : ($checksum_status = "*CHECKSUM FAILED*"); } } @needed; } # If we do not need patches, then you are pretty up on things. # print a nice message. if (!@needed) { print qq| Congratulations you do not need any patches installed. Send this note to your boss, and ask for a raise!!! |; } # Clean up the files that were downloaded from the net. if (!defined( $opt_n )) { if ( defined( $opt_k )) { # keep the files print STDERR "Preserving CHECKSUM & patchdiag files\n" if defined( $opt_d ); copy_file( "/tmp/patchdiag_$$", "$patch_dir/patchdiag.xref" ); copy_file( "/tmp/CHECKSUMS_$$", "$patch_dir/CHECKSUMS" ); } push @trashcan, "/tmp/patchdiag_$$"; push @trashcan, "/tmp/CHECKSUMS_$$"; } else { push @trashcan, "/tmp/Recommended_$$"; } format install_top = Patch-ID Install status Description --------- --------------- -------------------- . format install_out = @<<<<<<<<<< @<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< $installing ,$patch_install_status, $patch_description{$_} . ################################################ ############# Patch installation ############### ################################################ if (@needed and defined $opt_i and !defined $opt_n) { &question_patch_install_sub; } elsif (defined $opt_n and defined $opt_i) { &question_patch_install_sub; } ################################################ ############# Shutdown message ################# ################################################ if ( defined $opt_s) { print "\n\n**SHUTTING DOWN WITH MESSAGE: $opt_s\n\n"; `/usr/sbin/shutdown -y -g$opt_g -i6 "$opt_s" &`; } sub question_patch_install_sub { if (!defined $opt_F) { # Print an ominous message to let the user know this might # be a bad idea. If they still want to do it, they probably # know what they are doing print qq| ** Installing all patches without checking them first ** ** can have negative consequences. I am assuming that ** ** you know this, and think that all of these patches ** ** are a good idea. Using the -F option will turn off ** ** this message. ** \n|; if (!defined $opt_L) { # Get a confirmation or a list of patches to install print "Which patches do you want to install (all/none/list of patches) "; my $answer_install_patch; chomp( $answer_install_patch = ); if ( $answer_install_patch eq "all") { &install_patch_sub; } elsif ($answer_install_patch eq "none") { print "\n\tExiting install procedure\n"; exit 0; } elsif ($answer_install_patch =~ /^10/) { @needed = split ' ',$answer_install_patch; &install_patch_sub; } else { print "\n\tCan't determine answer, aborting installation procedure\n"; exit 0; } } elsif ( defined $opt_L) { print "Would you like to install all patches listed in $opt_L? (yes/no)\n"; my $answer_install_patch; chomp( $answer_install_patch = ); if ($answer_install_patch =~ "n") { print "\n\tExiting install procedure\n"; exit 0; } elsif ($answer_install_patch =~ "y") { &install_patch_sub; } } # If called with the -F flag then skip the formality, and just install # the patches. } elsif (defined $opt_F) { &install_patch_sub; } } sub install_patch_sub { if (defined $opt_q ) { print "\n\n**Installing patches with fastpatch**\n"; } else { print "\n**Installing Patches (this can take a while)**\n"; } if (!defined $opt_q ) { print "\nPatch-ID Install status Description\n"; print "--------- --------------- -------------------- \n"; } # for all of the patches left in the @needed array we do a regular old # installation. Just uncompress the patch, cd into the directory and run # the installpatch program. Also check the return code of the installpatch # program. If the return code is something other than 0 then grep the error # code from the installpatch program and print it on the install status # column of the output. $^ = "install_top"; $~ = "install_out"; if (defined $opt_E) { $excluded_patches_fd = new FileHandle "$opt_E", "r"; chomp(@excluded_patches = <$excluded_patches_fd>); } if (defined $opt_e) { push @excluded_patches,split ' ',$opt_e; } if (defined $opt_L) { $needed_patches_fd = new FileHandle "$opt_L", "r"; chomp(@needed = <$needed_patches_fd>); print "\n\nInstalling patches listed in $opt_L\n\n"; } foreach $patch_to_install (@needed) { # Make sure that we do not have any white space in the patch-id from the # possible input on the command line. $patch_to_install =~ s/\s//g; $skip_this_patch = 0; undef $patch_install_status; if (defined $opt_e or defined $opt_E) { map { (substr($patch_to_install,0,6) eq substr($_,0,6)) ? $skip_this_patch = 1 : ""} @excluded_patches; } # Verify checksum if we're in contract mode. my $good_to_go = 1; $installing = "$patch_to_install"; $patch_install_status = "*NOT INSTALLED*"; if ( !defined( $opt_n )) { if ( !defined( $actual_checksum{$patch_to_install})) { warn "$patch_to_install: no checksum found\n"; $good_to_go = 0; } if ( !defined( $calculated_checksum{$patch_to_install })) { warn "$patch_to_install: no checksum calculated\n"; $good_to_go = 0; } if ( $actual_checksum{$patch_to_install} ne $calculated_checksum{$patch_to_install} ) { warn "$patch_to_install: checksum failed\n"; $good_to_go = 0; } } if ( $good_to_go && ($patch_to_install ne "") && ($skip_this_patch != 1)) { if (defined $opt_Z) { chdir "$opt_Z"; uncompress( "$patch_dir/$actual_name{$patch_to_install}"); chdir "$opt_Z/$patch_to_install"; } else { chdir "$patch_dir"; uncompress( "$patch_dir/$actual_name{$patch_to_install}"); chdir "$patch_dir/$patch_to_install"; } $installing = "$patch_to_install"; if (defined $opt_q and defined $opt_Z) { `$INSTALL_PATCH_PROG -p $opt_Z $FAST_PATCH_ARGS $patch_to_install >/tmp/patch-$ {patch_to_install}-$$.out 2>/tmp/patch-$ {patch_to_install}-$$.err`; } elsif (defined $opt_q and !defined $opt_Z ) { `$INSTALL_PATCH_PROG -p $patch_dir $FAST_PATCH_ARGS $patch_to_install >/tmp/patch-$ {patch_to_install}-$$.out 2>/tmp/patch-$ {patch_to_install}-$$.err`; } elsif (!defined $opt_q) { # Removed -d, which prevents you from backing the patches out! `$INSTALL_PATCH_PROG . >/tmp/patch-$ {patch_to_install}-$$.out 2>/tmp/patch-$ {patch_to_install}-$$.err`; } # Determine what happened if ($? != 0 and !defined $opt_q) { $error = $?/256; &error_messages; } elsif ($? != 0 and defined $opt_q) { $patch_install_status = "*NOT INSTALLED*"; } else { $patch_install_status = "Patch installed\t"; if (defined $opt_R and !defined $opt_Z) { push @trashcan, "$patch_dir/$patch_to_install"; push @trashcan, "$actual_name{$patch_to_install}"; } elsif (defined $opt_R and defined $opt_Z) { push @trashcan, "$opt_Z/$patch_to_install"; } } # Fastpatch doesn't leave a useful error code (succeeds even if # the patch wasn't installed due to no applicable packages) my $output = -s "/tmp/patch-$ {patch_to_install}-$$.err"; $output ||= -s "/tmp/patch-$ {patch_to_install}-$$.out"; DUMP: for my $output ( 'err', 'out' ) { open( FILE, ") { if ( $line =~ /no applicable packages, skipped/ ) { $patch_install_status = "*NOT APPLICABLE*"; last DUMP; } # fixme this doesn't work # print "Messages for $patch_to_install:\n-----------------------------------------\n" if ( $output eq 'err' ) && ( $. = 0 ); print "$output: $line"; } close( FILE ); push @trashcan, "/tmp/patch-$ {patch_to_install}-$$.$output"; } } elsif ($skip_this_patch == 1) { $patch_install_status = "*EXCLUDED PATCH*"; $installing = "$patch_to_install"; } $_ = $patch_to_install; # ugh write if !$skip_this_patch; } } sub error_messages { # Let's give back a good error message # Exit Codes: # 0 No error # 1 Usage error # 2 Attempt to apply a patch that's already been applied # 3 Effective UID is not root # 4 Attempt to save original files failed # 5 pkgadd failed # 6 Patch is obsoleted # 7 Invalid package directory # 8 Attempting to patch a package that is not installed # 9 Cannot access /usr/sbin/pkgadd (client problem) # 10 Package validation errors # 11 Error adding patch to root template # 12 Patch script terminated due to signal # 13 Symbolic link included in patch # 14 NOT USED # 15 The prepatch script had a return code other than 0. # 16 The postpatch script had a return code other than 0. # 17 Mismatch of the -d option between a previous patch # install and the current one. # 18 Not enough space in the file systems that are targets # of the patch. # 19 $SOFTINFO/INST_RELEASE file not found # 20 A direct instance patch was required but not found # 21 The required patches have not been installed on the manager # 22 A progressive instance patch was required but not found # 23 A restricted patch is already applied to the package # 24 An incompatible patch is applied # 25 A required patch is not applied # 26 The user specified backout data can't be found # 27 The relative directory supplied can't be found # 28 A pkginfo file is corrupt or missing # 29 Bad patch ID format # 30 Dryrun failure(s) # 31 Path given for -C option is invalid # 32 Must be running Solaris 2.6 or greater # 33 Bad formatted patch file or patch file not found %nice_error_message = ( 0 => "No error", 1 => "Usage error", 2 => "Patch already applied", 3 => "UID not root", 4 => "Can't save files", 5 => "pkgadd failed", 6 => "Obsolete patch", 7 => "bad pkg directory", 8 => "Pkg not installed", 9 => "client problem", 10 => "validation errors", 11 => "Error adding patch", 12 => "end due to sig.", 13 => "Symbolic link included", 14 => "NOT USED", 15 => "prepatch script", 16 => "postpatch script", 17 => "Option mismatch", 18 => "No space", 19 => "File not found", 20 => "direct instance patch", 21 => "manager installation", 22 => "progressive instance", 23 => "restricted patch", 24 => "incompatible patch", 25 => "need req. patch", 26 => "no backout data" , 27 => "norelative directory", 28 => "bad pkginfo file", 29 => "Bad patch ID", 30 => "Dryrun failure(s)", 31 => "Bad path (-C)", 32 => "2.6 or greater", 33 => "bad formatting" ); $patch_install_status = $nice_error_message{$error}; } # This function handles all the downloading sub fetch { my ( $source, $file ) = @_; my ( $fd, $web_request, $web_response ); my $tried_auth = 0; RETRY: my $url = "$baseurl/$source"; # LWP::Request is smart enough to handle direct connection or # proxying for us as required. $web_request = HTTP::Request->new( GET => "$url" ); print STDERR "Fetching $url to $file\n" if $opt_d; $cookie_jar->add_cookie_header( $web_request ); $web_response = $ua->request( $web_request ); $cookie_jar->extract_cookies( $web_response ); print STDERR "Webserver replied: ", $web_response->code, "\n" if $opt_d; if ( $web_response->is_success ) { $fd = new FileHandle( $file, "w+" ) or return; print $fd $web_response->content; $fd->flush(); } else { if ( !$tried_auth and $web_response->code == 403 and ( $opt_a or $opt_A )){ print STDERR "Trying to authenticate with cookies\n" if $opt_d; $tried_auth = 1; my ( $site, $path ) = $baseurl =~ m|^(http://[^/]+)(/.*)$|; my ( $user, $pass ) = split( '/', $account ); REDIRECTED: # Pick up our cookies $web_request = new HTTP::Request GET => "$site/"; $cookie_jar->add_cookie_header( $web_request ); print STDERR "Getting cookies from $site\n" if $opt_d; $web_response = $ua->request( $web_request ); $cookie_jar->extract_cookies( $web_response ); # Build a login request $web_request = new HTTP::Request POST => "$site/LOGIN"; $web_request->add_content( "credential_0=" . uri_escape( $user )); $web_request->add_content( "&credential_1=" . uri_escape( $pass )); $web_request->add_content( "&destination=%2Fprivate-cgi%2Fshow.pl%3Ftarget%3Dhome_con" ); $web_request->header( 'Content-Type', 'application/x-www-form-urlencoded' ); $cookie_jar->add_cookie_header( $web_request ); print STDERR "Getting authentication from $site\n" if $opt_d; $web_response = $ua->request( $web_request ); $cookie_jar->extract_cookies( $web_response ); # Handle redirects if ( $web_response->code == 302 ) { my $redir = $site = $web_response->header( 'Location' ); $redir =~ s|^(http://.+)/.*$|$1|; if ( $redir =~ /^http/ ) { print STDERR "You have been Diverted.\n" if $opt_d; # patch $site and $baseurl XXX clean this up! $site = $redir; $baseurl =~ s|^http://[^/]+|$redir|; goto REDIRECTED; } else { # internal redirect => successful login } } if ( $web_response->is_success or $web_response->code == 302 ) { print STDERR "Authenticated! cookies:\n" if $opt_d; print STDERR $cookie_jar->as_string . "\n" if $opt_d; # Edsger W. Dijkstra can bite me goto RETRY } # else fall through } print STDERR "Failed to retrieve $source! (" . $web_response->code . ")\n" if $opt_d; return; } return $fd; } # Uncompress a file using the correct tool (based on file extension) sub uncompress { my ( $file ) = @_; # We're already in the directory we want to be in, so. if ( $file =~ /\.zip$/ ) { `/usr/bin/unzip -o $file`; } elsif ( $file =~ /\.tar\.(Z|gz)/) { `/usr/bin/gzip -dc $file | /usr/bin/tar xfv -`; } else { `/usr/bin/tar xvf $file`; } } # /bin/rm -rf $dir sub rm_rf { my ( $dir ) = @_; my @files; if ( -f $dir ) { unlink( $dir ); return; } opendir( DIR, $dir ) or return; # we don't care about failures. @files = readdir( DIR ); closedir( DIR ); for my $f ( @files ) { next if $f =~ /^\.\.?$/; if ( -d "$dir/$f" ) { rm_rf( "$dir/$f" ); } else { unlink( "$dir/$f" ) } } rmdir( $dir ); } # this is actually available as a module, but let's not require that. sub copy_file { my ( $file1, $file2 ) = @_; open( FROM, "<$file1" ) or die "$file1: $!"; open( TO, ">$file2" ) or die "$file2: $1"; binmode( FROM ); binmode( TO ); READ_LOOP: while ( 1 ) { my $buf; my $r = sysread( FROM, $buf, 1024 ); die "copy_file: $!" if !defined( $r ); last READ_LOOP if $r == 0; WRITE_LOOP: while ( $r ) { my $w = syswrite( TO, $buf, $r ); die "copy_file: $!" if !defined( $w ); $r -= $w; last WRITE_LOOP if $r <= 0; # should never be less than, but. } } close( FROM ); close( TO ); }