#!/bin/sh -- # Really perl eval 'exec perl -w $0 ${1+"$@"}' if 0; # PCA - Patch Check Advanced # # Version 1.3.1 (2004/09/01): # # Switch from FTP downloads to HTTP downloads for patchdiag.xref/patches. # Internal code redesign, now runs with "perl -w" and "use strict". # # Version 1.3 (2004/08/19): # # Added experimental function to show unbundled patches # Ignore comments at the end of lines in pca.ignore # Check for circular patch dependencies # Fix small bug in handling showrev -p output # Internal redesign of command line option handling # # 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/solaris/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. use strict; # Executables - only needed if download (-d/-x) or install (-p) is used. # my $wget = '/usr/sfw/bin/wget /usr/local/bin/wget /opt/csw/bin/wget'; my $unzip = '/usr/bin/unzip'; my $patchadd = '/usr/sbin/patchadd'; my $uncompress = '/usr/bin/uncompress'; my $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. # my $patchdir = ''; # You should not need to modify anything below this line. # Modules use Getopt::Std; # Variable declarations # my $show_installed=0; my $show_uninstalled=0; my $show_unbundled=0; my $show_rsonly=0; my $show_header=1; my $download_xref=0; my $download_files=0; my $apply_patches=0; my $first_header=1; my %p; my %instpkgs; my $ilist; # Check for command line options # my %opts; &getopts("iufraxdph", \%opts) || &usage && exit 1; if ($opts{i} || $opts{a}) { $show_installed=1; } if ($opts{u} || $opts{a}) { $show_uninstalled=1; } if ($opts{f}) { $show_unbundled=1; } if ($opts{r}) { $show_rsonly=1; } if ($opts{x}) { $download_xref=1; } if ($opts{d}) { $download_files=1; } if ($opts{p}) { $apply_patches=1; } if ($opts{h}) { $show_header=0; } # Set default option if ($show_installed == 0 && $show_uninstalled == 0 && $show_unbundled == 0) { $show_uninstalled=1; $show_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 my $missingexec=''; if ($download_xref || $download_files || $apply_patches) { my $found=''; foreach my $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. # my $xrefdir=$ENV{"XREF"}; if (! $xrefdir) { $xrefdir = $0; if ($xrefdir !~ /\//) { $xrefdir = '.'; } else { $xrefdir =~ s/\/[^\/]*$//; } } my $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 "http://patches.sun.com/reports/patchdiag.xref" -P $xrefdir`; if ($? == 0) { print "done\n"; } else { print "failed\n"; } } # Get architecture # open(UNAME, "/usr/bin/uname -p |"); my $arch = ; chop ($arch); close UNAME; # Read patchdiag.xref # open(XREF, "<$XREF") || die "Can't find xref file $XREF"; $/=""; my $xref = ; $/="\n"; close XREF; my @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}).*/) { &init_patch($1); $p{$1}{'irev'} = $2; } } 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; $_ =~ s/[ ]*#.*//; my ($id,$rev) = split(/-/,$_); $rev = "-1" unless $rev; &init_patch($id); $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 my $i (sort @xref) { my ($id, $crev, $reldate, $rFlag, $sFlag, $oFlag, $byFlag, $os, $archs, $pkgs, $synopsis ) = split( /\|/, $i); # Ignore comment lines next unless $id =~ m/^\d/; &init_patch($id); # 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'} != -1) { if (($oFlag eq "O") || ($byFlag =~ ".B")) { next; } } $p{$id}{'crev'}=$crev; $p{$id}{'reldate'}=$reldate; $p{$id}{'rflag'}=0; if ($rFlag eq 'R' ) { $p{$id}{'rflag'}=1; } $p{$id}{'sflag'}=0; if ($sFlag eq 'S' ) { $p{$id}{'sflag'}=1; } $p{$id}{'oflag'}=0; if ($oFlag eq 'O' ) { $p{$id}{'oflag'}=1; } $p{$id}{'bflag'}=0; if ($byFlag =~ ".B") { $p{$id}{'bflag'}=1; } $p{$id}{'yflag'}=0; if ($byFlag =~ "Y.") { $p{$id}{'yflag'}=1; } $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 my $r (split /\;/, $archs) { if ($r =~ m/\d{6}-\d{2}/) { $p{$id}{'requires'} .= "$r;"; } } } if ($show_installed == 1) { print_header ("INSTALLED"); foreach my $id (sort keys %p) { next unless ($p{$id}{'irev'} != '-1'); print_patch ($id); } } if ($show_uninstalled == 1) { if ($show_rsonly == 1) { print_header ("UNINSTALLED RECOMMENDED/SECURITY"); } else { print_header ("UNINSTALLED"); } # Read pkginfo open( PKGINFO, "/usr/bin/pkginfo -x|" ); while () { my $package; my $version; 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"; } my $pkgidx = $package.":".$version; $instpkgs{$pkgidx}=1; } # Check all patches whether they apply. # foreach my $id (sort keys %p) { # Ignore obsolete and bad patches if ($p{$id}{'oflag'} == 1) { next; } if ($p{$id}{'bflag'} == 1) { 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 "-1") { 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 =~ my $found=0; foreach my $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 my $j (split (/\;/, $p{$id}{'pkgs'})) { if ($instpkgs{$j} && ($instpkgs{$j} == 1)) { $found=1; } } if ($found == 0) { next; } $p{$id}{'applies'} = 1; } # List uninstalled patches # foreach my $id (sort keys %p) { # If the user chose to see R/S patches only, ignore others. if ($show_rsonly == 1) { if (($p{$id}{'rflag'} == 0) && ($p{$id}{'sflag'} == 0)) { 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'} == 0) { 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;"; } } if ($show_unbundled == 1) { if ($show_rsonly == 1) { print_header ("UNBUNDLED RECOMMENDED/SECURITY"); } else { print_header ("UNBUNDLED"); } # Check all patches whether they apply. # foreach my $id (sort keys %p) { # Check if patch is Unbundled and has an empy packages list. if (!(($p{$id}{'os'} eq "Unbundled") && ($p{$id}{'pkgs'} eq ""))) { next; } # Ignore obsolete and bad patches if ($p{$id}{'oflag'} == 1) { next; } if ($p{$id}{'bflag'} == 1) { next; } # Ignore patches in the ignore list. if ($p{$id}{'ignore'} eq "-1") { next; } if ($p{$id}{'ignore'} eq $p{$id}{'crev'}) { next; } # If the user chose to see R/S patches only, ignore others. if ($show_rsonly == 1) { if (($p{$id}{'rflag'} == 0) && ($p{$id}{'sflag'} == 0)) { next; } } # We do not check for required patches, as this would add patches # to the $ilist (patches to be downloaded and installed) - unbundled # patches can't be installed automatically. Also required patches # for unbundled patches are rare, and usually explicitely mentioned # in the README, and might require other precautions. # Print patch print_patch ($id); } } sub check_requires { my $id=$_[0]; return if ($p{$id}{'requires'} eq ''); foreach my $r (split (/;/, $p{$id}{'requires'})) { my ($r_id, $r_rev) = split (/-/, $r); # Check circular patch dependencies (only one level). This won't # catch patch A req B, B req C, and C req A. if ($p{$r_id}{'requires'} ne '') { foreach my $s (split (/;/, $p{$r_id}{'requires'})) { (my $s_id, my $s_rev) = split (/-/, $s); if ($id eq $s_id) { #print "WARNING: Circular patch dependency $id <-> $r_id\n"; return; } } } # 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'}) { # 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)) { my $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 { my $download_successful = 0; for my $ext ('.zip','.tar.Z') { `$wget -q "http://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) { my $mustreconfigure=0; my $mustreboot=0; if ($show_header == 1) { print "\nApplying patches\n"; print '-' x 78 . "\n"; } foreach my $id (split (/;/, $ilist)) { my $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? my $patchmustreboot = 0; my $patchmustreconfigure = 0; my $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 init_patch { my $id=$_[0]; # Every patch should be initialized only once. return if ($p{$id}{'init'}); $p{$id}{'irev'}=-1; $p{$id}{'crev'}=-1; $p{$id}{'synopsis'}='NOT FOUND IN CROSS REFERENCE FILE!'; $p{$id}{'rflag'}=0; $p{$id}{'sflag'}=0; $p{$id}{'oflag'}=0; $p{$id}{'bflag'}=0; $p{$id}{'yflag'}=0; $p{$id}{'os'}=''; $p{$id}{'pkgs'}=''; $p{$id}{'ignore'}=0; $p{$id}{'reldate'}=''; $p{$id}{'archs'}=''; $p{$id}{'requires'}=''; $p{$id}{'applies'}=0; $p{$id}{'shown'}=0; $p{$id}{'init'}=1; } sub print_patch { # Use %-.62s for synopsis if you want to limit it so that lines won't wrap. my $id=$_[0]; my $char; my $irev; my $crev; my $rflag; my $sflag; if ($p{$id}{'irev'} < $p{$id}{'crev'}) { ($char='<'); } if ($p{$id}{'irev'} == $p{$id}{'crev'}) { ($char='='); } if ($p{$id}{'irev'} > $p{$id}{'crev'}) { ($char='>'); } $irev = $p{$id}{'irev'}; if ($irev eq "-1") { $irev = '--' }; $crev = $p{$id}{'crev'}; if ($crev eq "-1") { $crev = '--' }; $rflag=' '; if ($p{$id}{'rflag'} == 1) { $rflag='R'; } $sflag=' '; if ($p{$id}{'sflag'} == 1) { $sflag='S'; } printf "%6d %2s %1s %2s %1s%1s %s\n", $id, $irev, $char, $crev, $rflag, $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 [-iufraxdph] [patch ...]\n"; print " -i Show installed patches\n"; print " -u Show uninstalled patches\n"; print " -f Show unbundled patches\n"; print " -r Show recommended/security uninstalled patches only\n"; print " -a Show all installed/uninstalled 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"; }