#!/bin/sh -- # Really perl eval 'exec perl -w $0 ${1+"$@"}' if 0; # PCA - Patch Check Advanced # # Version 1.5.0 (2005/08/08): # Add experimental support for downloading contract-only patches # Fix handling of IDR patches # Add support for installpatch (Solaris <= 2.5.1) # # Version 1.4.7 (2005/07/08): # Fix error handling # Fix handling of required patches # # Version 1.4.6 (2005/06/02): # Add option to specify directory location of patchdiag.xref (-X) # Add host name to header # Fix HTML code generation # Add option to run patchadd without backing up files (-k) # Fix usage information # # Version 1.4.5 (2005/05/25): # Fix handling of tar patches # Add missing options to usage information # Add option to show pca version information (-v) # # Version 1.4.4 (2005/02/07): # Fix handling of multiple installations of the same package # # Version 1.4.3 (2005/02/01): # Fix bug in patchdiag.xref reading code to handle inconsistencies # Fix bug which made pca ignore some unbundled patches # # Version 1.4.2 (2004/12/22): # Add option to only apply patches which do not require a reboot (-n) # # Version 1.4.1 (2004/10/27): # Add option to read uname/showrev/pkginfo from files (-F) # # 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; # SunSolve login data # my $sunsolve_user = ''; my $sunsolve_passwd = ''; # 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'; my $uname = '/usr/bin/uname'; # 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 $from_files=0; my $apply_noreboot_only=0; my $patchadd_options=''; my $first_header=1; my $readme_id=''; my $download_id=''; my $xrefdir; my $arch=''; my $hostname=''; my %p; my %instpkgs; my $ilist; # Check for command line options # my %opts; &getopts("iufraxX:dpnkHR:D:bFhv", \%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{n}) { $apply_noreboot_only=1; } if ($opts{k}) { $patchadd_options='-d'; } 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{F}) { $from_files=1; } if ($opts{h}) { &usage; exit 0; } if ($opts{v}) { &version; 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 if ($from_files == 1) { open(UNAME, "); ($hostname && $arch) || die "Can't parse uname ouput"; 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; } if ($show_html) { print "\n\n"; } 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 if ($from_files == 1) { open( PKGINFO, ") { my $package; my $version; if ($_ =~ m/^(\S+) /) { $package=$1; # Removing trailing .2/.3/... - this happens when multiple versions # of a package with the same name are installed (like SPRO*). $package =~ s/\..*//; } 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 if required patch are in our database. Normally we should # stop with an error here, but maybe information in patchdiag.xref # is wrong and the patch will apply without the missing required patch. if ($p{$r_id}{'crev'} == -1) { #print "WARNING: Required patch $r_id unknown\n"; next; } # 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','.tar') { `$wget -q "http://patches.sun.com/all_unsigned/$id$ext" -P $patchdir`; if ($? == 0) { $download_successful = 1; last; } } # Retry failed download from alternate URL with Sunsolve login. # Warning - this will work for *.zip patches (which modern Solaris # version use) only. if ($download_successful == 0) { if (($sunsolve_user ne '') && ($sunsolve_passwd ne '')) { `$wget -q --http-user=$sunsolve_user --http-passwd=$sunsolve_passwd "http://sunsolve.sun.com/private-cgi/pdownload.pl?target=$id&method=h" --output-document=$id.zip`; if ($? == 0) { $download_successful = 1; } } } 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; } if ($apply_noreboot_only == 1 && ($patchmustreconfigure || $patchmustreboot)) { print "skipping (reboot required)\n"; } else { # If the patchadd command doesn't exist, try installpatch, which # comes with patches for Solaris <= 2.5.1. $patchadd="$patchdir/$patchname/installpatch" unless (-x $patchadd); die "Cannot execute patchadd/installpatch" unless (-x $patchadd); `$patchadd $patchadd_options $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 ($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. # if ($from_files == 1) { open(SHOWREV, ") { if (($_ =~ m/^Patch:\s+(\d{6})-(\d{2}).*/) || ($_ =~ m/^Patch:\s+IDR(\d{6})-(\d{2}).*/)) { &init_patch($1); $p{$1}{'irev'} = $2; } else { die "Can't parse showrev output\n"; } } 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= $opts{X} || $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}{'synopsis'}=$synopsis; # Patch requires are coded into the archs field - separate them. $p{$id}{'archs'}=''; $p{$id}{'requires'}=''; foreach my $r (split /\;/, $archs) { if ($r =~ m/\d{6}-\d{2}/) { $p{$id}{'requires'} .= "$r;"; # We run init_patch here for required patches because they might # be missing in the xref file, and would be uninitialized later. my ($r_id, $r_rev) = split (/-/, $r); &init_patch($r_id); } else { $p{$id}{'archs'} .= "$r;"; } } # Patch incompatibilities are coded into the pkgs field $p{$id}{'pkgs'}=''; $p{$id}{'incompatible'}=''; foreach my $r (split /\;/, $pkgs) { if ($r =~ m/\d{6}-\d{2}/) { $p{$id}{'incompatible'} .= "$r;"; my ($r_id, $r_rev) = split (/-/, $r); &init_patch($r_id); } else { $p{$id}{'pkgs'} .= "$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}{'incompatible'}=''; $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; my $synopsis; 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'; } $synopsis = $p{$id}{'synopsis'}; if ($show_html == 0) { printf "%6d %2s %1s %2s %1s%1s %s\n", $id, $irev, $char, $crev, $rflag, $sflag, $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. # $synopsis =~ s/\&/\&/; printf ""; printf "%6d", $id; printf "%2s%1s%2s%1s%1s", $irev, $h_char, $crev, $rflag, $sflag; printf "%s\n", $synopsis; } } sub print_header { return unless ($show_header == 1); if ($show_html == 0) { if ($first_header == 1) { $first_header=0; } else { print "\n\n"; } print "$_[0] PATCHES for $hostname\n"; print "Patch IR CR RS Synopsis\n"; print "------ -- - -- -- " . '-' x 60 . "\n"; } else { if ($first_header == 1) { print "\n"; print "\n\n"; print "PCA report for $hostname\n"; print "\n\n"; $first_header=0; } print "

$_[0] PATCHES for $hostname

\n\n"; print ""; print ""; print ""; print ""; 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 [-iufraxdpnkHbFhv] [-X dir] [-R patch-id] [-D patch-id]\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 " -X Set location of xref-file\n"; print " -d Download patches\n"; print " -p Apply patches (after downloading)\n"; print " -n Only apply patches which do not require a reboot\n"; print " -k Make patchadd not back up files to be patched\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 " -F Read uname/showrev/pkginfo output from files\n"; print " -h Print this help\n"; print " -v Print pca version information\n"; } sub version { print "pca 1.5.0 (2005/08/08)\n"; }