#!/bin/sh -- # Really perl eval 'exec perl $0 ${1+"$@"}' if 0; # PCA - Patch Check Advanced # # Version 1.2 (2004/07/05): # # Check prerequisites of patches and list them, too (in the correct order) # Speedup of factor 2.5 in comparison to PCA 1.1 # Allow specification of multiple paths to wget # Fix small bug in patchdiag.xref download code # Fix handling of tar/tar.Z patches # # Version 1.1 (2004/06/16): # # Added pca.ignore file to ignore patches (contributed by Damian Hole) # Added patchdiag.xref download function # Added patch download function (contributed by Damian Hole) # Added patch install function (contributed by Damian Hole) # # Version 1.0 (2003/09/09): # # First release # # Author: Martin Paul # Home : http://www.par.univie.ac.at/~martin/pca/ # # PCA is a replacement for Sun's Patch Check tool. It shows lists of # installed and uninstalled patches. With the help of PCA the system # administrator can easily find out which patches should/need to be # applied to a system. # Executables - only needed if download (-d/-x) or install (-p) is used. # $wget = '/usr/sfw/bin/wget /usr/local/bin/wget /opt/csw/bin/wget'; $unzip = '/usr/bin/unzip'; $patchadd = '/usr/sbin/patchadd'; $cat = '/usr/bin/cat'; $uncompress = '/usr/bin/uncompress'; $tar = '/usr/sbin/tar'; # Patches are downloaded and uncompressed into this directory, so it # must exist and be writable for the user running pca with -d and/or -p. # If empty (''), or if it doesn't exist, the current directory (.) will # be used. Only needed if patch download (-d) or install (-p) is used. # $patchdir = ''; # You should not need to modify anything below this line. # Modules use Getopt::Std; # Check for command line options # $show_installed=0; $show_uninstalled=0; $show_uninstalled_rsonly=0; $show_header=1; &getopts("iuraxdph") || &usage && exit 1; if ($opt_i || $opt_a) { $show_installed=1; } if ($opt_u || $opt_a) { $show_uninstalled=1; } if ($opt_r) { $show_uninstalled=1; $show_uninstalled_rsonly=1; } if ($opt_x) { $download_xref=1; } if ($opt_d) { $download_files=1; } if ($opt_p) { $apply_patches=1; } if ($opt_h) { $show_header=0; } # Set default option if ($show_installed == 0 && $show_uninstalled == 0) { $show_uninstalled=1; $show_uninstalled_rsonly=1; } # Must be root to apply patches if ($apply_patches && ($< != 0)) { die "You must be root to apply patches.\n"; } # Check for missing executables and directories if ($download_xref || $download_files || $apply_patches) { foreach $i (split (/ /, $wget)) { next unless ($found eq ''); if (-x $i) { $found = $i; } } if ($found eq '') { $missingexec = " wget ($wget)" } else { $wget = $found; } } if ($apply_patches) { $missingexec .= " patchadd ($patchadd)" unless (-x $patchadd); } if ($missingexec) { print "The following executables are missing:\n"; print "$missingexec\n"; exit 1; } # Check and set patch download directory, and check if writable. $patchdir='.' unless (-d $patchdir); if ($download_files || $apply_patches) { if (! -w $patchdir) { print "Can't write to patch download directory ($patchdir)\n"; exit 1; } } # Get patchdiag.xref location. # $xrefdir=@ENV{"XREF"}; if ($xrefdir eq "") { $xrefdir = $0; if ($xrefdir !~ /\//) { $xrefdir = '.'; } else { $xrefdir =~ s/\/[^\/]*$//; } } $XREF="$xrefdir/patchdiag.xref"; # Download most recent patchdiag.xref, if requested # if ($download_xref == 1) { print "Retrieving xref-file to $xrefdir/patchdiag.xref..."; `$wget -qN "ftp://patches.sun.com/patchroot/reports/patchdiag.xref" -P $xrefdir`; if ($? == 0) { print "done\n"; } else { print "failed\n"; } } # Get architecture # open(UNAME, "/usr/bin/uname -p |"); $arch = ; chop ($arch); close UNAME; # Read patchdiag.xref # open(XREF, "<$XREF") || die "Can't find xref file $XREF"; $/=""; $xref = ; $/="\n"; close XREF; @xref = split( /\n/, $xref ); # Read showrev -p output. Sort it, to always store the newest revision only. # open(SHOWREV, "/usr/bin/showrev -p | sort |"); while () { if ($_ =~ m/^Patch:\s+(\d{6})-(\d{2}).*/) { $p{$1}{'irev'} = $2; $p{$1}{'crev'} = '--'; $p{$1}{'synopsis'}='NOT FOUND IN CROSS REFERENCE FILE!'; } else { die "Can't parse showrev -p output"; } } close SHOWREV; # Read ignore list # if (-f "$xrefdir/pca.ignore") { open(IGNORE, "$xrefdir/pca.ignore") || die "Can't open $xrefdir/pca.ignore: $!\n"; while() { next unless /^\d/; chomp; ($id,$rev) = split(/-/,$_); $rev = "--" unless $rev; $p{$id}{'ignore'} = $rev; } close IGNORE; } # Build our patch information table from the xref file. # patchdiag.xref is sorted, so if multiple revisions of a patch are listed, # the one with the highest revision comes last. # foreach $i (sort @xref) { ($id, $crev, $reldate, $rFlag, $sFlag, $oFlag, $byFlag, $os, $archs, $pkgs, $synopsis ) = split( /\|/, $i); # Ignore comment lines next unless $id =~ m/^\d/; # If we already have data for this patch ID (a lower revision), # and the higher revision is OBS or BAD, don't put it into our table. # if ($p{$id}{'crev'} && ( $p{$id}{'crev'} ne "--")) { if (($oFlag eq "O") || ($byFlag =~ ".B")) { next; } } $p{$id}{'crev'}=$crev; $p{$id}{'reldate'}=$reldate; $p{$id}{'rflag'}=$rFlag; $p{$id}{'sflag'}=$sFlag; $p{$id}{'oflag'}=$oFlag; $p{$id}{'byflag'}=$byFlag; $p{$id}{'os'}=$os; $p{$id}{'archs'}=$archs; $p{$id}{'pkgs'}=$pkgs; $p{$id}{'synopsis'}=$synopsis; # Patch requires are coded into the archs field foreach $r (split /\;/, $archs) { if ($r =~ m/\d{6}-\d{2}/) { $p{$id}{'requires'} .= "$r;"; } } # If the patch is not installed, set $irev to "--" if (!$p{$id}{'irev'}) { $p{$id}{'irev'}='--'; } } $first_header=1; if ($show_installed == 1) { print_header ("INSTALLED"); foreach $i (sort keys %p) { if ($p{$i}{'irev'} != '--') { print_patch ($i); } } } if ($show_uninstalled == 1) { if ($show_uninstalled_rsonly == 1) { print_header ("UNINSTALLED RECOMMENDED/SECURITY"); } else { print_header ("UNINSTALLED"); } # Read pkginfo open( PKGINFO, "/usr/bin/pkginfo -x|" ); while () { if ($_ =~ m/^(\S+) /) { $package=$1; } else { die "Can't parse pkginfo output\n"; } $_ = ; if ($_ =~ m/ (\S+)$/) { $version=$1; } else { die "Can't parse pkginfo output\n"; } $pkgidx = $package.":".$version; $instpkgs{$pkgidx}=1; } # Check all patches whether they apply. # foreach $id (sort keys %p) { # Ignore obsolete and bad patches if ($p{$id}{'oflag'} eq "O") { next; } if ($p{$id}{'byflag'} =~ ".B") { next; } # Ignore patches which are installed in their current revision # Sometimes installed patch revisions are higher than patchdiag.xref, # therefore we use ">=" instead of "==". if ($p{$id}{'irev'} >= $p{$id}{'crev'}) { next; } # Ignore patches in the ignore list. if ($p{$id}{'ignore'} eq "--") { next; } if ($p{$id}{'ignore'} eq $p{$id}{'crev'}) { next; } # Check if patch is for our architecture. If not, ignore it. # The architecture naming in the xref file isn't perfect, we have to use =~ $found=0; foreach $j (split (/\;/, $p{$id}{'archs'})) { if (($j =~ $arch) || ($j =~ "all")) { $found=1; } } if ($found == 0) { next; } # Check if patch is for a package we have installed. If not, ignore it. $found=0; foreach $j (split (/\;/, $p{$id}{'pkgs'})) { if ($instpkgs{$j} == 1) { $found=1; } } if ($found == 0) { next; } $p{$id}{'applies'} = 1; } # List uninstalled patches # foreach $id (sort keys %p) { # If the user chose to see R/S patches only, ignore others. if ($show_uninstalled_rsonly == 1) { if ($p{$id}{'rflag'} ne 'R' && $p{$id}{'sflag'} ne 'S') { next; } } # Ignore patches which have been shown already (via check_requires). if ($p{$id}{'shown'} == 1) { next; } # Ignore patches that don't apply. if (! $p{$id}{'applies'}) { next; } # Check prerequisites (recursively), show patch, mark it as shown # and add it to the list of patches to be possibly installed later. # check_requires ($id); print_patch ($id); $p{$id}{'shown'}=1; $ilist .= "$id;"; } } sub check_requires { my $id=$_[0]; return unless ($p{$id}{'requires'}); foreach $r (split (/;/, $p{$id}{'requires'})) { (my $r_id, my $r_rev) = split (/-/, $r); # Ignore patches already in our list, or already installed. if ($p{$r_id}{'shown'} == 1) { next; } if ($p{$r_id}{'irev'} >= $r_rev) { next; } # Check if the required patch can be applied if ($p{$r_id}{'applies'} != 1) { # See README (2004/07/02) why we ignore this. #print "WARNING: $id requires $r_id-$r_rev, does not apply\n"; next; } # So we require a patch which exists, but isn't listed yet. # We recursively check the requires for the patch, and then print it. # check_requires ($r_id); print_patch ($r_id); $p{$r_id}{'shown'}=1; $ilist .= "$r_id;"; } } if (($download_files || $apply_patches) && $ilist) { if ($show_header == 1) { print "\nDownloading Patches into $patchdir\n"; print '-' x 78 . "\n"; } foreach my $id (split (/;/, $ilist)) { $patchname = "$id-$p{$id}{'crev'}"; print "Retrieving patch $patchname..."; if (-f "$patchdir/$patchname.zip" || -f "$patchdir/$patchname.tar.Z" || -f "$patchdir/$patchname.tar") { print "skipping (file exists)\n"; } else { $download_successful = 0; for my $ext ('.zip','.tar.Z') { `$wget -q "ftp://patches.sun.com/all_unsigned/$patchname$ext" -P $patchdir`; if ($? == 0) { $download_successful = 1; last; } } if ($download_successful) { print "done\n"; } else { print "skipping (download failed)\n"; } } } } if ($apply_patches && $ilist) { if ($show_header == 1) { print "\nApplying patches\n"; print '-' x 78 . "\n"; } foreach my $id (split (/;/, $ilist)) { $patchname = "$id-$p{$id}{'crev'}"; print "Applying $patchname..."; if (-f "$patchdir/$patchname.zip") { `$unzip -n $patchdir/$patchname.zip -d $patchdir 2>&1`; } elsif (-f "$patchdir/$patchname.tar.Z" || -f "$patchdir/$patchname.tar") { `$uncompress $patchdir/$patchname.tar.Z` if (-f "$patchdir/$patchname.tar.Z"); `cd $patchdir; $tar -xf $patchdir/$patchname.tar`; } if ($? != 0) { print "skipping (uncompress failed)\n"; next; } if (-d "$patchdir/$patchname") { # Do we need a reboot? $patchmustreboot = 0; $patchmustreconfigure = 0; $readme = "$patchdir/$patchname/README.$patchname"; if (-f $readme) { open(README,$readme) || die "Can't open $readme: $!\n"; while() { if (/Reconfigure after installation/ || /Reconfigure immediately after patch is installed/) { $patchmustreconfigure = 1; } elsif (/Reboot after installation/ || /Reboot immediately after patch is installed/) { $patchmustreboot = 1; } } close README; } `$patchadd $patchdir/$patchname 2>&1`; if ($? == 0) { print "done"; if ($patchmustreconfigure) { print " (reconfigure required)"; $mustreconfigure = 1; } elsif ($patchmustreboot) { print " (reboot required)"; $mustreboot = 1; } print "\n"; } else { print "failed\n"; } } else { print "skipping (patch directory missing)\n"; } } if ($mustreconfigure) { print "\nPlease reconfigure to complete patch process.\n"; } elsif ($mustreboot) { print "\nPlease reboot for patches to take affect.\n"; } } sub print_patch { # Use %-.62s for synopsis if you want to limit it so that lines won't wrap. my $id=$_[0]; my $char; if ($p{$id}{'irev'} < $p{$id}{'crev'}) { ($char='<'); } if ($p{$id}{'irev'} == $p{$id}{'crev'}) { ($char='='); } if ($p{$id}{'irev'} > $p{$id}{'crev'}) { ($char='>'); } printf "%6d %2s %1s %2s %1s%1s %s\n", $id, $p{$id}{'irev'}, $char, $p{$id}{'crev'}, $p{$id}{'rflag'}, $p{$id}{'sflag'}, $p{$id}{'synopsis'}; } sub print_header { return unless ($show_header == 1); if ($first_header == 1) { $first_header=0; } else { print "\n\n"; } print "$_[0] PATCHES\n"; print "Patch IR CR RS Synopsis\n"; print "------ -- - -- -- " . '-' x 60 . "\n"; } sub usage { print "Usage: $0 [-iuradph]\n"; print " -i Show installed patches\n"; print " -u Show uninstalled patches\n"; print " -r Show uninstalled recommended/security patches only (default)\n"; print " -a Show all patches (combines -iu)\n"; print " -x Download xref-file\n"; print " -d Download patches\n"; print " -p Apply patches (after downloading)\n"; print " -h Don't show descriptive headers\n"; }