#!/usr/bin/perl
# $Id: rpmsupd,v 1.2 2000/09/06 16:52:08 dmetz Exp $
# $crtd:  by  Derald Metzger  on  990901 $
# $cmnt:  rpms update of installed pkgs from a local mirror dir
#           Copyright (c) 1998 1999  Derald Metzger 
#  This file is part of the cfm pkg (GPL).
#  ftp.remotesensing.org/pub/rpms/cfm*
# See $usage below for usage info.
# $

use strict defs;

($cpu_type) = ($_ = `uname --machine`, chop);
#$curkver = ""; # Current kernel version
$hostname = `hostname`; chop $hostname;
$k_flg = 0;     # No kernel rpms by default. Use the -k opt.
$nkver = 0;     # Clear new kernel version
$o_flg - 0;     # Skip pkgs that appear older by default
@pats = (".");  # pattern of pkgs to update
#$upd_dir       # update dir, rqd param

$usage = "
NAME
  rpmsupd - rpms update
SYNOPSIS
  rpmsupd -k update_dir
  rpmsupd [-o] update_dir [pattern ...]
DESCR
  rpmsupd first examines the list of installed pkgs. Based on this
  list it looks for updates in the update_dir.
  - The 1st invocation will update only \`kernel-' pkgs. After display
  of the version comparison info for installed kernel pkgs it prompts
  the user for permission to make the necessary changes. The update
  images are put in place and lilo is modified to boot the new kernel
  as default with the old kernel as backup.
  - The 2nd invocation displays a version comparison for installed 
  pkgs that match the specified patterns and are present in update_dir.
  The user is prompted for permission to execute the update cmds.
  \`kernel-' rpms are excluded in this mode.
OPTIONS
  -k  - update the only the kernel pkgs
  -o  - prompt, give user an option to include versions that look older.
        This accomodates pkgs with alternate version naming conventions.
  update_dir - dir holding the updated rpms
  pattern    - regular expression used to id candidate rpms
               You may need to protect this from your shell.
               The default pattern is '.' (any char).
NOTE
  1. If you have installed the mirror pkg these simple cmds can be 
  used to keep a local mirror of the redhat-6.0 updates dir:
    cd <update_dir>
    mirror -d -d -grpmfind.net:/linux/redhat/updates/6.0/i386/
  You may want to keep a 2nd dir of other rpms you use on your site.
  2. Version comparisons are dependent on naming conventions. Where 
  different conventions are involved the comparison is shakey at best.
  For this reason you should always carefully examine the \`older' list
  to insure this program has correctly evaluated the version comparison
  before proceeding with the update.
ALSO
  See also cfm-cmds-list(8)
AUTHORS
           Copyright (c) 1998 1999  Derald Metzger 
  This file is part of the cfm pkg (GPL).
  This program was inspired by the upgradeRHKernel.pl 
  program written by Stephen Adler.
";

#######################   subrs & formats ############################
# Split the pkg name and the version-release
sub getrpm_nam_vr {
    my @tmplist = split(/-/,$_[0]);
    $#tmplist < 2 && do {
        print "### malformed rpm string: $_[0]\n";
        return 0;
    };
    my $name = join('-',@tmplist[0 .. ($#tmplist - 2)]);
    my $vr = join('-',@tmplist[-2],@tmplist[-1]);
    return ($name, $vr);
}
#--------------------------   formats   ---------------------------
format SAME =
@<<<<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$_, $same{$_}[0], $same{$_}[1], $same{$_}[2]
.
format OLDER =
@<<<<<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$_, $older{$_}[0], $older{$_}[1], $older{$_}[2]
.
format UPDATES =
@<<<<<<<<<<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<< @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$_, $updates{$_}[0], $updates{$_}[1], $updates{$_}[2]
.
#######################   end subs & formats  #########################

# Get the opts.
while($_ = $ARGV[0], /^-/)  {
  shift;
  /^-k$/ && do { $k_flg = 1; next; };
  /^-o$/ && do { $o_flg = 1; next; };
  die "\n### $0: Invalid option: $_ $usage";
}

# Must have an update dir to get rpms from
@ARGV || die "\n### $0: insufficient args $usage";
$upd_dir = $ARGV[0]; shift;
chdir $upd_dir || die "### Can't cd to update dir: $upd_dir\n    Die";

# Get patters if provided
if(@ARGV) { @pats = @ARGV; }

# Print a header.
print 
"==================================================================\n",
"===   Analysis on host $hostname of update dir\n",
"===   $upd_dir\n",
"-------------------------------------------------------------------\n";

# Make a hash of installed pkgs that match the pattern
ARGVLP:
foreach $pat (@pats) {
    if($k_flg) { $grep_str = "grep kernel-"; }
    else { $grep_str = "grep $pat | grep -v kernel-"; }
    open(CURPKGS,"rpm -qa | $grep_str |") 
        || die "### rpm -qa pipe failed";
    while(<CURPKGS>) {
        ($name, $vr) = getrpm_nam_vr($_); chop $vr;
        # Keep only newest version
        (!$curpkgs{$name}||($curpkgs{$name} lt $vr)) && ($curpkgs{$name}=$vr);
    }
    close CURPKGS;
}

# Now go thru the list of updates available.
#   sort matches into: updates, same_version, older_version ]
open(NEWPKGS, "ls *.rpm |");
while(<NEWPKGS>) {
    chop;
    $rpm_nam = $_;
    chop($pkg_namvr = `rpm -qp $_`);
    ($name, $vr) = getrpm_nam_vr($pkg_namvr);
    if($curpkgs{$name}) { 
        # For kernel- files take only our own karch
        $k_flg && ($name eq "kernel" || $name eq "kernel-smp")
            && ($rpm_nam !~ /$cpu_type/) && next;
        for ($curpkgs{$name} cmp $vr) {
            /-1/ && do {
                $updates{$name} && do {  # Duplicate pkg
                    print
                        "### Update dir contains multiple copies of pkg $name\n",
                        "    Please use \`rpms1' to purge the dir\n";
                    $bad_dir = 1;
                };
                $updates{$name} = [ ($curpkgs{$name},$vr,$rpm_nam) ]; next;
            };
            /0/ && do {
                $same{$name} = [ ($curpkgs{$name},$vr,$rpm_nam) ]; next;
            };
            /1/ && do {
                $o_flg && do {
                    print "
## $pkg_namvr:   pkg in dir rpm $rpm_nam.
   $name-$curpkgs{$name}:   already installed, LOOKS NEWER.
   Update to $pkg_namvr anyhow? [ y | n ](n)? ";
                    <STDIN> =~ /^(y|Y)$/ && do {
                        $updates{$name} = [ ($curpkgs{$name},$vr,$rpm_nam) ];
                        print "\n";
                        next;
                    };
                    print "\n";
                };
                $older{$name} = [ ($curpkgs{$name},$vr,$rpm_nam) ]; next;
            };
        }
    }
}

# Show user the status sets: updates, same, older
# Build rpm updates lists
@Uvh_lis = ();
@ivh_lis = ();
@rd_cmds = ();
print 
"===========      Same version installed. Already updated.\n",
"PKGNAME             CUR_VERSION NEW_VERSION  RPM\n",
"-------------------------------------------------------------------\n";
if(!%same) { print "none are the same \n"; }
else {
    open(SAME, ">&STDOUT");
    for (keys(%same)) { write SAME; }
    close SAME;
}
print
"-------------------------------------------------------------------\n",
"===========      Older versions?  Better check these over!\n",
"PKGNAME             CUR_VERSION NEW_VERSION  RPM\n",
"-------------------------------------------------------------------\n";
if(!%older) { print "none are older\n"; }
else {
    open(OLDER, ">&STDOUT");
    for (keys(%older)) { write OLDER; }
    close OLDER;
}
print
"-------------------------------------------------------------------\n",
"===================        Update these?\n",
"PKGNAME             CUR_VERSION NEW_VERSION  RPM\n",
"-------------------------------------------------------------------\n";
($bad_dir) && print 
    "### WARNING:\n",
    "    Udate Dir contains multiple copies of the same pkg.\n",
    "    Please use \`rpms1' to purge the dir.\n";
if(!%updates) { print "nothing to update\n"; }
else {  
    open(UPDATES, ">&STDOUT");
    for (keys(%updates)) {
        write UPDATES;
        if(/^kernel$/ || /^kernel-smp$/) {  # Doing a Kernel update
            # Kernel files are installed along side existing
            # Take ver from any kernel. Verify all are the same below.
            $curkver = $updates{$_}[0];  # current kernel version
            $newkver = $updates{$_}[1];  # new kernel version
            push( @ivh_lis, $updates{$_}[2] );  # separate install list
            # Make the ram disk hash.
            /^kernel$/ && push @rd_cmds,
                "/sbin/mkinitrd initrd-${newkver}.img $newkver";
            /^kernel-smp$/ && push @rd_cmds,
                "/sbin/mkinitrd initrd-${newkver}smp.img ${newkver}smp";
        } else {
            push( @Uvh_lis, $updates{$_}[2] );
        }
    }
    close UPDATES;
}
print "===================================================================\n";

# Show update cmds and prompt user
if(!%updates) { print "## No rpm cmds. Nothing to update.\n"; exit; }
if(!$k_flg) {  # Non-kernel updates
    @args = ("rpm","-Uvh", @Uvh_lis);
    print "   Update cmd will be:\n",
        "-------------------------------------------------------------\n",
        "rpm -Uvh ", join(' ',@Uvh_lis),"\n",
        "-------------------------------------------------------------\n",
        "## Proceed with updates  [y|n](n)? ";
    <STDIN> !~ /^(y|Y)$/ && die "## User elected abort";
    # Execute the update cmd
    ($rc = system(@args)) && do {
        print "### Failed to update cleanly",
              "\n    system(@args)";
        printf "\n    returned: %#04x\n", $rc;
    };
} else {  # We are doing the kernel
    # Verify a consistant kernel update version
    for (keys(%updates)) {
        ($updates{$_}[1] ne $newkver)
            && die "### Ambiguous update dir contents.\n",
                   "    More than one kernel version present.\n    Exit";
    }
    print "\n   Cmds to put new kernels in place and update support files:\n",
    "-------------------------------------------------------------\n";
    @ivh_lis && do {
        @iargs = ("rpm","-ivh", @ivh_lis);
        print "rpm -ivh ", join(' ',@ivh_lis),"\n";
    };
    @Uvh_lis && do {
        @Uargs = ("rpm","-Uvh", @Uvh_lis);
        print "rpm -Uvh ", join(' ',@Uvh_lis),"\n";
    };
    print "-------------------------------------------------------------\n",
    "## If these look OK lilo will be examined\n",
          "   OK to proceed  [y|n](n)? ";
    <STDIN> !~ /^(y|Y)$/ && die "## User elected abort";
    
    # Build new lilo.conf with image sections for the new kernel
    # Assumption: the image line for the current kernel contains its ver #
    @curimage = ();
    $in_cur_image = 0;
    open(CURLILO, "/etc/lilo.conf");
    open(NEWLILO, ">/tmp/lilo.conf_$$");
    while(<CURLILO>) {
        # Read a line, save, modify if for the current image, print.
        # when finished with an current image block print it as it was.
        (/image/ || /other/) && /=/ && do {  # Entering image block
            ($in_cur_image = 0);
            print NEWLILO @curimage;
            @curimage = ();
        };
        /image/ && /=/ && /$curkver/ && ($in_cur_image = 1);
        $in_cur_image && do {
            if(/label/) {
                chop($label=$_); push @curimage, $label . "-old\n"; }
            else { push @curimage, $_; }
            s/$curkver/$newkver/o;
        };
        print NEWLILO;
    }
    print NEWLILO @curimage;
    close CURLILO;
    close NEWLILO;

    # Show new lilo.conf
    print "\n   New /etc/lilo.conf (/tmp/lilo.conf_$$) to be installed\n",
    "-------------------------------------------------------------\n";
    @args = ("cat", "/tmp/lilo.conf_$$");
    ($rc = system(@args)) && die "### system call failed";
    print "-------------------------------------------------------------\n",
          "## If the above lilo.conf looks OK\n",
          "   the ramdisk cmds will be examined\n",
          "   OK to proceed  [y|n](n)? ";
    <STDIN> !~ /^(y|Y)$/ && die "## User elected abort";

    # Show ramdisk cmds
    print "\n   Cmds to make new init ramdisks (initrd)\n",
        "-------------------------------------------------------------\n",
        join("\n", @rd_cmds), "\n",
        "-------------------------------------------------------------\n",
        "## If these look ok we are ready to execute the update\n",
        "   1. rpm will put new kernels in place\n",
        "   2. rpm will update kernel support\n",
        "   3. mkinitrd will make new ramdisks\n",
        "   4. lilo.conf will be updated\n",
        "   5. lilo will be executed to enable boot of the new kernel.\n",
        "   OK to proceed  [y|n](n)? ";
    <STDIN> !~ /^(y|Y)$/ && die "## User elected abort";

    # Put new kernel files in place
    @iargs && do {
        print "\n", join(' ', @iargs), "\n";
        ($rc = system(@iargs)) && do {
            print "### rpm failed. Nothing should have been changed",
            "\n    system(@iargs)";
            printf "\n    returned: %#04x\n", $rc;
            die;
        };
    };
    # Update the kernel support files
    @Uargs && do {
        print join(' ', @Uargs), "\n";
        ($rc = system(@Uargs)) && do {
            print "### rpm failed. Nothing should have been changed",
            "\n    system(@Uargs)";
            printf "\n    returned: %#04x\n", $rc;
            die;
        };
    };
    # Make the ramdisk (initrd) files
    chdir( "/boot" ) || die "### Can't access /boot dir";
    for (@rd_cmds) {
        @args = $_;
        ($rc = system(@args)) && do {
            print "### Failed to create ram disk file",
            "\n    system(@args)";
            printf "\n    returned: %#04x\n", $rc;
            die;
        };
    }
    # Save lilo.conf. Unessary, it should be in cfmdb - but...
    # Put the new one in place and execute it.
    @args = ("cp","/etc/lilo.conf","/tmp/$$-lilo.conf");
    print join(' ', @args), "\n";
    system(@args) && die "### Failed to copy old lilo.conf to /tmp";
    @args = ("mv /tmp/lilo.conf_$$ /etc/lilo.conf");
    print join(' ', @args), "\n";
    system(@args) && die "### Failed to copy new lilo.conf to /etc";;   
    @args = ("/sbin/lilo -v");
    print join(' ', @args), "\n";
    system(@args) && die "### Failed to successfully execute lilo";

    print "\n## Final msg:\n",
          "   Time to reboot.\n",
          "   Default image is the new kernel.\n",
          "   linux-old should get the old one.\n";
}

#die "\n#### rpmsupd.pl program is incomplete\n",
#    "     Executed to current end-of-program\n     Die";
