#!/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 <waider@waider.ie> 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 = <STDIN>);
}

# 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 = <STDIN>);
      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 = <STDIN>);
      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, "</tmp/patch-$ {patch_to_install}-$$.$output" );
        while ( my $line = <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 );
}
