#!/bin/sh -- # Really perl eval 'exec perl -w $0 ${1+"$@"}' if 0; # PCA - Patch Check Advanced # # Version 1.4 (2004/09/21): # # Add experimental HTML output option (-b) # Add -R option to show the README of a patch # Add -D option to download one patch # Command line option to not show headers is now called -H # New command line option -h to print usage information # Internal code redesign # # 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_patches=0; my $apply_patches=0; my $show_html=0; my $first_header=1; my $readme_id=''; my $download_id=''; my $xrefdir; my %p; my %instpkgs; my $ilist; # Check for command line options # my %opts; &getopts("iufraxdpHR:D:bh", \%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_patches=1; } if ($opts{p}) { $apply_patches=1; } if ($opts{H}) { $show_header=0; } if ($opts{R}) { $readme_id=$opts{R}; } if ($opts{D}) { $download_id=$opts{D}; } if ($opts{b}) { $show_html=1; } if ($opts{h}) { &usage; exit 0; } # Set default option if ($show_installed == 0 && $show_uninstalled == 0 && $show_unbundled == 0) { $show_uninstalled=1; $show_rsonly=1; } # Check prerequisites (paths to needed binaries, root for patch install, etc.) &check_prerequisites; # Get architecture open(UNAME, "/usr/bin/uname -p |"); my $arch = ; chop ($arch); close UNAME; if ($readme_id) { &show_readme($readme_id); exit 0; } # Get list of installed patches &get_installed_patches; # Get list of current patches &get_current_patches; if ($download_id) { &do_download_patch($download_id); exit 0; } # Read ignore list &get_ignore_list; if ($show_installed == 1) { &do_show_installed; } if ($show_uninstalled == 1) { &do_show_uninstalled; } if ($show_unbundled == 1) { &do_show_unbundled; } if (($download_patches || $apply_patches) && $ilist) { &do_download_patches; } if ($apply_patches && $ilist) { &do_install_patches; } sub do_show_installed { print_header ("INSTALLED"); foreach my $id (sort keys %p) { next unless ($p{$id}{'irev'} != '-1'); print_patch ($id); } &print_footer; } sub do_show_uninstalled { 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;"; } &print_footer; } sub do_show_unbundled { 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); } &print_footer; } 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;"; } } sub do_download_patches { if ($show_header == 1) { print "\nDownloading Patches into $patchdir\n"; print '-' x 78 . "\n"; } foreach my $id (split (/;/, $ilist)) { &do_download_patch("$id-$p{$id}{'crev'}"); } } sub do_download_patch { my $id=$_[0]; # If there is no explicit patch revision given, we have to read current # patches information to get the most current revision. # if ($id =~ /^\d\d\d\d\d\d$/ ) { &init_patch($id); #&get_current_patches; if ($p{$id}{'crev'} == -1) { die "Unknown patch-id $id"; } $id= "$id-$p{$id}{'crev'}"; } if ($id =~ /^\d\d\d\d\d\d-\d\d$/) { print "Retrieving patch $id..."; if (-f "$patchdir/$id.zip" || -f "$patchdir/$id.tar.Z" || -f "$patchdir/$id.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/$id$ext" -P $patchdir`; if ($? == 0) { $download_successful = 1; last; } } if ($download_successful) { print "done\n"; } else { print "failed\n"; } } return; } die "Invalid patch-id $id"; } sub do_install_patches { 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 check_prerequisites { # 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_patches || $apply_patches || $readme_id || $download_id) { 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) { die "The following executables are missing:\n$missingexec"; } # Check and set patch download directory, and check if writable. $patchdir='.' unless (-d $patchdir); if ($download_patches || $apply_patches || $download_id) { if (! -w $patchdir) { print "Can't write to patch download directory ($patchdir)\n"; exit 1; } } } sub get_installed_patches { # 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; } sub get_ignore_list { return unless (-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; } sub get_current_patches { # Get patchdiag.xref location. # $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"; } } # Read patchdiag.xref # open(XREF, "<$XREF") || die "Can't find xref file $XREF"; $/=""; my $xref = ; $/="\n"; close XREF; my @xref = split( /\n/, $xref ); # 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;"; } } } } 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 $h_char; my $irev; my $crev; my $rflag; my $sflag; if ($p{$id}{'irev'} < $p{$id}{'crev'}) { $char='<'; $h_char='<'; } if ($p{$id}{'irev'} == $p{$id}{'crev'}) { $char='='; $h_char='='; } if ($p{$id}{'irev'} > $p{$id}{'crev'}) { $char='>'; $h_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'; } if ($show_html == 0) { printf "%6d %2s %1s %2s %1s%1s %s\n", $id, $irev, $char, $crev, $rflag, $sflag, $p{$id}{'synopsis'}; } else { # The patch download link will only work for patches in zip format, # there is no way to determine if it's in zip or tar.Z here. # printf ""; printf "%6d", $id; printf "%2s%1s%2s%1s%1s", $irev, $h_char, $crev, $rflag, $sflag; printf "%s\n", $p{$id}{'synopsis'}; } } sub print_header { return unless ($show_header == 1); if ($first_header == 1) { $first_header=0; } else { print "\n\n"; } if ($show_html == 0) { print "$_[0] PATCHES\n"; print "Patch IR CR RS Synopsis\n"; print "------ -- - -- -- " . '-' x 60 . "\n"; } else { print "

$_[0] PATCHES

\n\n"; print "\n"; } } sub print_footer { return unless ($show_html == 1); print "
PatchIRCRRSSynopsis
\n"; } sub show_readme { my $id=$_[0]; # If there is no explicit patch revision given, we have to read current # patches information to get the most current revision. # if ($id =~ /^\d\d\d\d\d\d$/ ) { &init_patch($id); &get_current_patches; if ($p{$id}{'crev'} == -1) { die "Unknown patch-id $id"; } $id= "$id-$p{$id}{'crev'}"; } if ($id =~ /^\d\d\d\d\d\d-\d\d$/) { system ("$wget -q -O - http://patches.sun.com/all_unsigned/$id.README"); if ($? != 0) { die "Could not find README for patch-id $id"; } return; } die "Invalid patch-id $id"; } sub usage { print "Usage: $0 [-iufraxdpHRDbh] [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"; print " -R Show patch README\n"; print " -D Download patch\n"; print " -b Show HTML output\n"; print " -h Print this help\n"; }