#!/usr/bin/perl # RCS: $Id: rpmlevel,v 1.3 2000/12/15 22:46:49 root Exp $ #----------------------------------------------------------------------- # rpmlevel (c)1999-2000 Didimo Emilio Grimaldo Tunon #----------------------------------------------------------------------- # AUTHOR: D. Emilio Grimaldo T. grimaldo@coralys.com # USAGE: rpmlevel {mode} [options] [database] # DESCRIPTION: # Other Un*x have the capability of using a command to find # out the current patch levels and updates applied to the # system. On the Linux variants that use RPM it is possible # to do lots of different queries but not the above one. # This capability allows the administrator to check its # current upgrades against the OS provider as well as keep # track of 3rd party (RPM) packages. # RpmLevel does just that. The first time the distribution # CD is scanned for 'official' RPMs and a small flat file # database is built. Periodically (every night or so) RpmLevel # can be invoked to resync this database and detect what has # changed. # RpmLevel also allows to perform the following high-level # queries: # * Non-istalled packages # * Upgraded packages # * Downgraded packages # * Extra packages (not from the original distro) # modes: # --help Help # --init Initialize (from CD listing) and check # --report [nude] Which reports are wanted (one or more). If # none specified all (nude) is assumed. # --sync Check system and synchronize DB # --regroup WHAT Used to populate group field using a reference # --compare [amrs] Compare two releases of the same distro # and list Additions, Mutations, Removals & Same # modifiers: # --nosync Can be used at --init to prevent syncing # --nosave Don't write changes to DB (--init, --sync) # --append Append to DB (--init, useful for SuSE) # --quiet Don't notify of what I am about to do # --cdpath WHAT Where the distribution RPMs are located (override) # # If no database is specified 'default' is assumed, which could # actually be a symbolic link to the current one. # Examples: # First time, say we just installed Red Hat 6.0 # atlantis$ rpmlevel --init redhat60 [--nosync] # Then every night (in a cron job) # atlantis$ rpmlevel --sync redhat60 # Check what has been upgraded with respect to the distro # atlantis$ rpmlevel --report {nude} redhat60 # Compare Red Hat 6.0 to 6.1 # atlantis$ rpmlevel --compare ./redhat60 ./redhat61 # Regroup suse61 using redhat60 # atlantis$ rpmlevel --regroup suse61 redhat60 # NOTE: Does not keep track of removals # --------------------------------------------------------------------- # I should really stop writing free stuff and utilities, it does # not pay the bills! besides I didn't even get one share of Red Hat # stock... # --------------------------------------------------------------------- # ********** I N C L U D E S ********** #use strict; use DirHandle; use Getopt::Long; # ********* ********************* ********* # ********* CONFIGURATION SECTION ********* my %Cfg = ( 'rpm-dist-dir' => '/mnt/cdrom/RedHat/RPMS', # see --cdpath 'mandrake-cd' => '/mnt/cdrom/Mandrake/RPMS', 'redhat-cd' => '/mnt/cdrom/RedHat/RPMS', 'suse-cd' => '/cdrom/full-names/i386', 'lib-dir' => '/usr/local/lib/rpmlevel', 'level-dir' => '/var/local/rpmlevel'); # ********* ********************* ********* # ********** LOCAL DATA SECTION ********** my $cvsId = '$Revision: 1.3 $'; my $build = 1; use constant FLAG_ACTION_SET => 1; use constant FLAG_ACTION_UPDATE => 2; use constant FLAG_NONE => 0x00; use constant FLAG_SRC => 0xfc; use constant FLAG_SRC_DISTRO => 0x01; use constant FLAG_SRC_EXTRA => 0x02; use constant FLAG_STA => 0x03; use constant FLAG_STA_ORIGINAL => 0x04; use constant FLAG_STA_UPGRADED => 0x08; use constant FLAG_STA_DOWNGRADED => 0x10; use constant FLAG_STA_NOTAVAIL => 0x20; use constant PKG_UNKNOWN => 0x00; use constant PKG_UNCHANGED => 0x01; use constant PKG_DOWNGRADE => 0x02; use constant PKG_UPGRADE => 0x03; use constant PKG_EXTRA => 0x04; use constant PKG_REMOVED => 0x05; use constant DB_UPDATE_VER => 1; my ($optHelp, $optInit, $optReport, $optSync, $optStats, $optIdentify); my ($optRegroup, $optCompare, $optRT, $modNoSave, $modAppend, $modQuiet); my ($modNoSync, $modCdPath, $modLines, $version); my $VersionComparePtr; # ********* ********************* ********* # <<<<<<<<<<<<<<<< U T I L I T Y F U N C T I O N S >>>>>>>>>>>>>> sub splitname { my $fqn = shift; my $rName = shift; my (@attr, $pos); $$rName{'name'} = ''; $$rName{'version'} = ''; $$rName{'release'} = ''; # Remove all known architecture qualifiers and the extension $fqn =~ s/\.noarch\.rpm//; $fqn =~ s/\.i386\.rpm//; # Split into name-version-release $pos = rindex($fqn, '-') + 1; $$rName{'release'} = substr($fqn, $pos); $fqn = substr($fqn, 0, $pos - 1); $pos = rindex($fqn, '-') + 1; $$rName{'version'} = substr($fqn, $pos); $fqn = substr($fqn, 0, $pos - 1); $$rName{'name'} = $fqn; return $fqn; } # Convert a raw data to something we can handle more easily sub convert2hash { my $raw = shift; my %rec; my @d = split(/;/, $raw); $rec{'name'} = $d[0]; $rec{'version'} = $d[1]; $rec{'release'} = $d[2]; $rec{'size'} = $d[3]; $rec{'time'} = $d[4]; $rec{'group'} = $d[5]; $rec{'flgsrc'} = $d[6]; $rec{'flgsta'} = $d[7]; return %rec; } # Convert the program representation into a raw format to # store in the DB file sub convert2raw { my $rep = shift; my (%rec, $raw); %rec = %$rep; $raw = join(';', $rec{'name'}, $rec{'version'}, $rec{'release'}, $rec{'size'}, $rec{'time'}, $rec{'group'}, $rec{'flgsrc'}, $rec{'flgsta'}); return $raw; } # Get all the information we need from the RPM file # whether installed or not sub getRpmInfo { my $name = shift; my ($otheropt, $option, $result); # We can query for an installed package or from an uninstalled one $option = '-q'; $option .= 'p' if ($name =~ m/\.rpm$/i); $otheropt = '--queryformat "%{NAME};%{VERSION};%{RELEASE};%{SIZE};%{INSTALLTIME};%{GROUP};0;0"'; open(RPM, "rpm $option $otheropt $name 2>&1 |"); $result = ; print $result chomp($result); close(RPM); return "" if ($result =~ m/^package\s+/i); return $result; } # Just get the 'installedtime' information from an RPM file sub getRpmTime { my $name = shift; my ($otheropt, $result); $otheropt = '--queryformat "%{INSTALLTIME}"'; open(RPM, "rpm -q $otheropt $name 2>&1 |"); $result = ; close(RPM); chomp($result); return 0 if ($result =~ m/^package\s+/i); return $result; } sub getRpmTimeSize { my $name = shift; my $rtime = shift; my $rsize = shift; my ($otheropt, $result); $otheropt = '--queryformat "%{INSTALLTIME};%{SIZE}"'; open(RPM, "rpm -q $otheropt $name 2>&1 |"); $result = ; close(RPM); chomp($result); $$rtime = 0; $$rsize = 0; # Defaults if it goes wrong if (!($result =~ m/^package\s+/i)) { ($$rtime, $$rsize) = split(/;/, $result, 2); } } sub versionNormalize { my $overs1 = shift; my $overs2 = shift; my ($i, @v1, @v2, $rsmall, $rbig, $n1, $n2, $isfirst); @v1 = split(/\./, $$overs1); @v2 = split(/\./, $$overs2); if ($#v1 > $#v2) { $rsmall = \@v2; $rbig = \@v1; $isfirst= 0; } else { $rsmall = \@v1; $rbig = \@v2; $isfirst= 1; } # Make them have the same amount of branches while ($#$rsmall < $#$rbig) { push(@$rsmall, '.0'); } # Make sure each branch has the same length foreach $i (0 .. $#$rsmall) { my $l1 = length $$rsmall[$i]; my $l2 = length $$rbig[$i]; my $diff; if ($l1 < $l2) { $$rsmall[$i] = ' ' x ($l2 - $l1) . $$rsmall[$i]; } else { $$rbig[$i] = ' ' x ($l1 - $l2) . $$rbig[$i]; } } # Now both must have the same length! $n1 = join('.', @$rsmall); $n2 = join('.', @$rbig); if (!$isfirst) { $i = $n1; $n1 = $n2; $n2 = $i; } # print "$$overs1 --> $n1\n$$overs2 --> $n2\n"; $$overs1 = $n1; $$overs2 = $n2; } # ====================================================================== # Just checks to see if both start with regular digit version numbers # If they do, this means we should probably do a digit compare on it # We are going to make single digit versions into string compares # # Returns -1 if this is purely a string # Returns 0 if this starts with a digit version # Return 1 if this is all digits sub areDigitVersions { my $num1 = shift; my $num2 = shift; # I am going to say that if a num starts with a decimal number # then this should be used as a starting point my $retValue = -1; if (($num1 =~ /^\d+[\.]\d+/) && ($num2 =~ /^\d+[\.]\d+/)) { $retValue = 0; } my @tmp = split(/\./, $num1); # =============================== # Check to see if all values # are digits # If not then return 0; foreach (@tmp) { if (/^\d+(.*)/) { if ($1 ne "") { return $retValue; } } else { return $retValue; } } @tmp = split(/\./, $num2); foreach (@tmp) { if (/^\d+(.*)/) { if ($1 ne "") { return $retValue; } } else { return $retValue; } } # if we get here then all are digits return 1; } # versionCompare # A simple string comparison is not good enough, for example # with string compare 1.0.8-8 -> 1.0.53-1 is seen as a # downgrade because '.8' gt '.53'. We must take care all # branches have the same length. # versionCompare($v1, $v2) checks the transition v1 -> v2 # Returns: # -1 Downgrade # 0 No change # +1 Upgrade sub versionCompare { my $v1 = shift; my $v2 = shift; my $rule = shift; my (%hv1, %hv2); if ($v1 eq 'ident' || $v2 eq 'ident') { return "DEGT 1.2"; } $$rule = 0; if ($v1 eq $v2) { return 0; } # Rule 0 &splitname("dummy-$v1", \%hv1); &splitname("dummy-$v2", \%hv2); &versionNormalize(\$hv1{'version'}, \$hv2{'version'}); $$rule++; if ($hv1{'version'} gt $hv2{'version'}) { return -1; } # Rule 01 $$rule++; if ($hv1{'version'} lt $hv2{'version'}) { return +1; } # Rule 02 # The battle of releases $$rule = 50; if ($hv1{'release'} gt $hv2{'release'}) { return -1; } # Rule 50 $$rule = 51; return +1; # Rule 51 } sub newVersionCompare { my $v1 = shift; my $v2 = shift; my $rule = shift; my (%hv1, %hv2); if ($v1 eq 'ident' || $v2 eq 'ident') { return "AG 0.9"; } $$rule = 0; if ($v1 eq $v2) { return 0; } # Rule 0 &splitname("dummy-$v1", \%hv1); &splitname("dummy-$v2", \%hv2); my $retValue = areDigitVersions($hv1{version}, $hv2{version}); if ($retValue >= 0) { my $version1 = $hv1{version}; my $version2 = $hv2{version}; my $rest1 = 0; my $rest2 = 0; my $found1 = 0; my $found2 = 0; my $found3 = 0; my $found4 = 0; if (!$retValue) { if ((($found1,$found2) = $version1 =~ /^(\d+[\.]\d+)(.*)/) && (($found3, $found4) = $version2 =~ /^(\d+[\.]\d+)(.*)/)) { $version1 = $found1; $version2 = $found3; $rest1 = $found2; # non-digit parts of the version $rest2 = $found4; } } # =========================================== # we have well behaved version numbers # So the case of these version number is: # 3.3.1 < 3.3.1.1 # 3.3.3.3.3.3 < 3.4 # my @tmp1 = split(/\./, $version1); my @tmp2 = split(/\./, $version2); my $smallerCount = 0; # ============================================== # We want to use the smaller count of digits # when comparing the values. # if they are equal then just use the tmp2 count # if ($#tmp1 < $#tmp2) { $smallerCount = $#tmp1 } else { $smallerCount = $#tmp2; } # print "Got $smallerCount for @tmp1 and @tmp2\n"; my $i = 0; for ($i = 0; $i <= $smallerCount; $i++) { if ($tmp1[$i] > $tmp2[$i]) { $$rule = 3; return -1; } # Rule 03 if ($tmp1[$i] < $tmp2[$i]) { $$rule = 4; return +1; } # Rule 04 } # ================================================================= # if we got here then it could mean that the counts are different # if they are then the longer count wins # of course if the counts are the same, then check the release # if ($#tmp1 > $#tmp2) { $$rule = 5; return -1; # Downgrade Rule 05 } elsif ($#tmp1 < $#tmp2) { $$rule = 6; return +1; # Upgrade Rule 06 } # ======================================== # This means that the counts are the same # Check the $rest variables to see if they are equal if ($rest1 && !$rest2) { # Assume that rest1 is not as good $$rule = 7; return +1; # Upgrade Rule 07 } if (!$rest1 && $rest2) { # Assume that rest1 is better $$rule = 8; return -1; # Downgrade (+2?) Rule 08 } if ($rest1 && $rest2) { $$rule = 9; if ($rest1 gt $rest2) { return -1; } # Rule 09 if ($rest1 lt $rest2) { return +1; } # Rule 09 } # Wow, everything is equal now check releases # The battle of releases if (areDigitVersions($hv1{release}, $hv2{release})) { $$rule = 52; if ($hv1{release} > $hv2{release}) { return -1; } # Rule 52 } else { $$rule = 50; if ($hv1{'release'} gt $hv2{'release'}) { return -1; } # Rule 50 } } else { # Do the normal version compare using string compares &versionNormalize(\$hv1{'version'}, \$hv2{'version'}); $$rule = 1; if ($hv1{'version'} gt $hv2{'version'}) { return -1; } # Rule 01 $$rule = 2; if ($hv1{'version'} lt $hv2{'version'}) { return +1; } # Rule 02 # The battle of releases $$rule = 50; if ($hv1{'release'} gt $hv2{'release'}) { return -1; } # Rule 50 } $$rule = 51; return +1; # Rule 51 } # notify # Show a short message taking into account the --quiet option sub notify { my $msg = shift; print "$msg" if !$modQuiet; } # getType # Find out whether it hasn't changed, or whether it is an # upgrade/downgrade/extra. Update the record if necessary. sub getType { my $rCache = shift; my $id = shift; my $action = shift; my ($key, %h, $type, $msg); my ($ver_inst, $ver_db, $rel_inst, $rel_db); $key = &splitname($id, \%h); $ver_inst = $h{'version'}; $rel_inst = $h{'release'}; if (exists($$rCache{$key})) { my $whatisit; my %insversion; # The one we got from the RPM query %insversion = %h; # Before we lose it %h = &convert2hash($$rCache{$key}); if ($action == FLAG_ACTION_SET) { $h{'flgsrc'} = FLAG_SRC_DISTRO; $h{'flgsta'} = FLAG_STA_NOTAVAIL; } $ver_db = $h{'version'}; $rel_db = $h{'release'}; $msg = "$key $ver_db-$rel_db -> $ver_inst-$rel_inst"; my $rule; $whatisit = &$VersionComparePtr("$ver_db-$rel_db", "$ver_inst-$rel_inst", \$rule); if ($whatisit == 0) { # There was no change # If we are here the package _is_ installed, so take care # that we don't revert an upgrade/downgrade status and # that a notavail becomes original. At this point remember # %h has the cached version, while %insversion has the # one we got out of the real rpm query! if ($action == FLAG_ACTION_SET) { $h{'flgsta'} = FLAG_STA_ORIGINAL; } else { # FLAG_ACTION_UPDATE if ($h{'flgsta'} == FLAG_STA_NOTAVAIL) { $h{'flgsta'} = FLAG_STA_ORIGINAL; } } $h{'flgsta'} = FLAG_STA_ORIGINAL unless ($action == FLAG_ACTION_UPDATE); $type = PKG_UNCHANGED; } elsif ($whatisit > 0) { $h{'flgsta'} = FLAG_STA_UPGRADED; # Find out when it was done &getRpmTimeSize($key, \$h{'time'}, \$h{'size'}); &dbUpdate(DB_UPDATE_VER, \%h, $ver_inst, $rel_inst); print "\t$msg (upgrade)\n"; $type = PKG_UPGRADE; } else { $h{'flgsta'} = FLAG_STA_DOWNGRADED; # Find out when it was done &getRpmTimeSize($key, \$h{'time'}, \$h{'size'}); &dbUpdate(DB_UPDATE_VER, \%h, $ver_inst, $rel_inst); print "\t$msg (downgrade)\n"; $type = PKG_DOWNGRADE; } # Write the record into the cache $$rCache{$key} = &convert2raw(\%h); } else { &dbInsert($rCache, $key, FLAG_SRC_EXTRA, FLAG_STA_ORIGINAL); print "\t$key $ver_inst-$rel_inst (extra)\n"; $type = PKG_EXTRA; } return $type; } sub dbLoad { my $dbfile = shift; my $rDbCache = shift; my ($key, $rest); die "$dbfile: $!\n" if (ref($rDbCache) ne 'HASH'); open(DBREFERENCE, "< $dbfile") || die "$dbfile: $!\n"; while () { chomp; ($key, $rest) = split(/;/, $_, 2); $$rDbCache{$key} = $_; } close(DBREFERENCE); } sub dbUpdate { my $fld = shift; # DB_UPDATE* my $rData = shift; # \$cache{'name'} or \%cache_entry my @pars = @_; my %h; if (ref($rData) ne 'SCALAR' && ref($rData) ne 'HASH') { die "dbUpdate() par2 is wrong type\n"; } if (ref($rData) eq 'SCALAR') { %h = &convert2hash($$rData); } else { %h = %$rData; } # Which field(s) are we updating? if ($fld == DB_UPDATE_VER) { $h{'version'} = $pars[0]; $h{'release'} = $pars[1]; } else { die "dbUpdate() unsupported field\n"; } # Write the record into the cache if (ref($rData) eq 'SCALAR') { $$rData = &convert2raw(\%h); } else { %$rData = %h; } } sub dbInsert { my $rCache = shift; my $name = shift; my $src = shift; my $sta = shift; my (%h, $raw); $raw = &getRpmInfo($name); %h = &convert2hash($raw); $h{'flgsrc'} = $src; $h{'flgsta'} = $sta; $$rCache{$name} = &convert2raw(\%h); return "$name $h{'version'}-$h{'release'}"; } sub dbCommit { my $dbname = shift; my $rPkgs = shift; if ($modNoSave) { ¬ify("Not saving to the db file (per request)\n"); return; } ¬ify("Writing gathered data/changes to db file\n"); if ($optInit && $modAppend) { open(LEVELDB, ">> $dbname") || die "$dbname: $!\n"; } else { open(LEVELDB, "> $dbname") || die "$dbname: $!\n"; } for (sort keys %$rPkgs) { print LEVELDB "$$rPkgs{$_}\n"; } close(LEVELDB); } # Get all the packages from the original distribution sub getFromCD { my $rAttr = shift; my ($name, %attr, $dbrecord); my $d = new DirHandle $Cfg{'rpm-dist-dir'}; if (!defined($d)) { die "getFromCD: perhaps CDROM is not mounted - $!\n"; } while (defined($_ = $d->read)) { next if (/^\.{1,2}/); next if (!($_ =~ m/\.rpm$/i)); chomp; # Get RawInfo $name = &splitname($_, \%attr); $dbrecord = &getRpmInfo("$Cfg{'rpm-dist-dir'}/$_"); if (ref($rAttr) eq 'HASH') { %attr = &convert2hash($dbrecord); $attr{'flgsrc'} = FLAG_SRC_DISTRO; $attr{'flgsta'} = FLAG_STA_NOTAVAIL; $$rAttr{$name} = &convert2raw(\%attr); } } undef $d; } sub showReport { my $dbfile = shift; my $pkgstate = shift; my $r = shift; my (%h, $pkgversion, $footer); my $i; format STDOUT_TOP = Page @<<<< $% Distribution: @<<<<<<<<<<<<<< Category: @<<<<<<<<<<<<<<<<<<<<< $dbfile $pkgstate Package name Version Size Date -------------------- ------------- ----------- ------------------------ . format STDOUT = @<<<<<<<<<<<<<<<<<<<< @<<<<<<<<<<<<<<<@>>>>>>>>>>>>>> @<<<<<<<<<<<<<<<<<<<<<< $h{'name'} $pkgversion $h{'size'} $h{'time'} . $= = 20; $= = $modLines if defined($modLines); $- = 0; # $^L is causing problems when used as a footer... $footer = '_' x 75 . "\n\tRpmLevel v$version-$build\n\t" . "Copyright (c)1999 Didimo Grimaldo\n\t" . "http://www.coralys.com/\n\n"; $^L = $footer; $pkgstate = "Not Installed" if ($pkgstate eq 'n'); $pkgstate = "Upgraded" if ($pkgstate eq 'u'); $pkgstate = "Downgraded" if ($pkgstate eq 'd'); $pkgstate = "Third Party" if ($pkgstate eq 'e'); foreach $i (0 .. $#$r) { %h = &convert2hash($$r[$i]); $pkgversion = "$h{'version'}-$h{'release'}"; $h{'time'} = localtime($h{'time'}) if ($h{'time'} ne '(none)'); write(); } if ($-) { print $footer; } } # <<<<<<<<<<<<<<<< T O P L E V E L H A N D L E R S >>>>>>>>>>>> sub Help { my $onerror = shift; print "\n\tRpmLevel version $version-$build\n"; print "\tCopyright (c)1999 Didimo Emilio Grimaldo Tunon\n"; print "\t-----------------------------------------------\n"; print "Usage: rpmlevel { mode } [modifier] DB\n"; print "Modes:\n"; print "\t-h|--help This help\n"; print "\t-i|--init Create and initialize DB for new distr.\n"; print "\t--sync Resync DB with current installation\n"; print "\t--stats Show per-group statistics\n"; print "\t-r|--report [] Report options for spe\n"; print "\t\tn Packages that were Not Installed\n"; print "\t\tu Packages that were Upgraded\n"; print "\t\td Packages that were Downgraded\n"; print "\t\te Extra packages (Third Party)\n"; print "\t--identify Check which system this is\n"; print "\t--compare {AMRS} Compare two releases of a distribution and,\n"; print "\t\ta Additions\n"; print "\t\tm Mutations (version/release)\n"; print "\t\tr Removals (discontinued packages)\n"; print "\t\ts Sames (identical packages)\n"; print "Modifiers:\n"; print "\t--nosync Do not synchronize during --init\n"; print "\t--nosave Don't save during --init/--sync\n"; print "\t--append Append to existing DB during --init\n"; print "\t--quiet Don't show status/notification messages\n"; print "\t--cdpath PATH Alternate path where to find RPMs for --init\n"; print "Examples:\n"; print "\t\trpmlevel --init redhat60 \[--nosync\]\n"; print "\t\trpmlevel --sync redhat60\n"; print "\t\trpmlevel --report [nude] redhat60\n"; print "\t\trpmlevel --stats redhat60\n"; print "Exiting with status $onerror\n" if $onerror; exit($onerror); } # Create and initialize the RPMLEVEL database. It will report on # any package that has been up/downgraded with respect to the # original distribution. The same applies for those that were # not part of the original distro (3rd party). sub Init { my $dbname = shift; my %pkgs; my ($i, $type, @names); # Get original distribution contents ¬ify("Getting information from distribution CD\n"); &Identify(0); # Find out where CDROM RPMs are mounted &getFromCD(\%pkgs); # See what has been installed and immediately determine # whether it is the same, an upgrade, downgrade or an # extra package. if (!defined($modNoSync)) { ¬ify("Getting information from installed packages\n"); open(RPM, "rpm -qa |") || die "Cannot query rpm $!\n"; while () { chomp; push(@names, $_); } close(RPM); foreach $i (0 .. $#names) { $type = &getType(\%pkgs, $names[$i], FLAG_ACTION_SET); } } else { ¬ify("Auto synchronization disabled (by you)\n"); } # Get information of each of the installed packages. If it is # known to be part of the distribution then only fetch the # install-time, otherwise get all the info and mark it as extra. &dbCommit($dbname, \%pkgs); exit(0); } sub Synchronize { my $dbfile = shift; my %dbcache; my %dbrecord; my ($key, $v, $type, $changed, @names, $i); # Read the current db into memory ¬ify("Loading DB...\n"); open(LEVELDB, "< $dbfile") || die "$dbfile: $!\n"; while () { chomp; ($key, $v) = split(/;/, $_, 2); $dbcache{$key} = $_; } close(LEVELDB); # Check for all currently installed packages # - Anything extra? # - Any new upgrades or downgrades? ¬ify("Checking currently installed packages\n"); $changed = 0; open(RPM, "rpm -qa |") || die "Cannot query rpm $!\n"; while () { chomp; push(@names, $_); } close(RPM); foreach $i (0 .. $#names) { $type = &getType(\%dbcache, $names[$i], FLAG_ACTION_UPDATE); $changed = 1 if ($type != PKG_UNCHANGED); } # Save into db if ($changed) { &dbCommit($dbfile, \%dbcache); } else { ¬ify("No further action needed (no changes)\n"); } exit(0); } sub Status { my $dbfile = shift; my $distro = shift; my $report = shift; my $repNot = 0; my $repUpgrade = 0; my $repDowngrade = 0; my $repExtra = 0; my (%h, @n, @u, @d, @e); if ($report eq '') { $report = 'nude'; } $repNot = 1 if (index($report, 'n') > -1); $repUpgrade = 1 if (index($report, 'u') > -1); $repDowngrade = 1 if (index($report, 'd') > -1); $repExtra = 1 if (index($report, 'e') > -1); # Spread the various categories open(LEVELDB, "< $dbfile") || die "$dbfile: $!\n"; while() { chomp; %h = &convert2hash($_); if ($h{'flgsrc'} == FLAG_SRC_EXTRA) { # Has priority otherwise it passes as 'original' push(@e, $_); } elsif ($h{'flgsta'} == FLAG_STA_NOTAVAIL) { push(@n, $_); } elsif ($h{'flgsta'} == FLAG_STA_UPGRADED) { push(@u, $_); } elsif ($h{'flgsta'} == FLAG_STA_DOWNGRADED) { push(@d, $_); } } close(LEVELDB); # For each category print a report (if not empty) &showReport($distro, 'n', \@n) if ($#n > -1 && $repNot); &showReport($distro, 'u', \@u) if ($#u > -1 && $repUpgrade); &showReport($distro, 'd', \@d) if ($#d > -1 && $repDowngrade); &showReport($distro, 'e', \@e) if ($#e > -1 && $repExtra); exit(0); } sub Statistics { my $dbfile = shift; my (%h, %groups, %installed, %sitting); open(LEVELDB, "< $dbfile") || die "$dbfile: $!\n"; while() { chomp; %h = &convert2hash($_); if ($h{'flgsta'} != FLAG_STA_NOTAVAIL) { if (exists($groups{$h{'group'}})) { $groups{$h{'group'}} += $h{'size'}; $installed{$h{'group'}}++; } else { $groups{$h{'group'}} = $h{'size'}; $installed{$h{'group'}} = 1; } } else { $sitting{$h{'group'}} = 1; } } close(LEVELDB); printf("%38s %9s %3s %3s\n", "Group", "Size", "In", "Out"); for (sort keys %groups) { printf("%38s %9s %3s %3s\n", $_, $groups{$_}, $installed{$_}, $sitting{$_}); } exit(0); } sub ReGroup { my $dbfile = shift; my $candidate = shift; my (%reference, %unsorted, %myref, %mycand, %st, $name); open(DBREFERENCE, "< $dbfile") || die "$dbfile: $!\n"; $st{'refcnt'} = 0; $st{'unscnt'} = 0; $st{'matched'} = 0; # Read the reference db, this one has groups for every RPM ¬ify("Reading reference database (grouped)..."); while () { chomp; %myref = &convert2hash($_); $reference{$myref{'name'}} = $_; $st{'refcnt'}++; } close(DBREFERENCE); ¬ify("done\n"); # Read the candidate db, this one is missing the groups ¬ify("Reading candidate database (ungrouped)..."); open(DBCANDIDATE, "< $candidate") || die "$candidate: $!\n"; while () { chomp; %mycand = &convert2hash($_); $name = $mycand{'name'}; $unsorted{$name} = $_; $st{'unscnt'}++; if (exists($reference{$name})) { %myref = &convert2hash($reference{$name}); $mycand{'group'} = $myref{'group'}; $unsorted{$name} = &convert2raw(\%mycand); $st{'matched'}++; } } close(DBCANDIDATE); ¬ify("done\n"); ¬ify("Writing new database with fixed records..."); open(DBCANDIDATE, "> $candidate.new") || die "$candidate.new: $!\n"; for (sort keys %unsorted) { print DBCANDIDATE "$unsorted{$_}\n"; } close(DBCANDIDATE); ¬ify("done\n"); print "Regroup Results\n"; print "\tTotal records on reference db: $st{'refcnt'}\n"; print "\tTotal records on candidate db: $st{'unscnt'}\n"; print "\tTotal fixed candidate records: $st{'matched'}\n"; print "\tNew file saved in $candidate.new (please edit & store)\n"; } sub Identify { my $interactive = shift; my $id; my $ecode = 3; # Not a Red Hat system my $idfile= '/etc/mandrake-release'; if (! -e $idfile) { $idfile = '/etc/redhat-release'; if (! -e $idfile) { $idfile = '/etc/SuSE-release'; if (! -e $idfile) { print "unkown\n"; exit(3); } else { $Cfg{'rpm-dist-dir'} = $Cfg{'suse-cd'}; } } else { $Cfg{'rpm-dist-dir'} = $Cfg{'redhat-cd'}; } } else { $Cfg{'rpm-dist-dir'} = $Cfg{'mandrake-cd'}; } if (!open(RELID, "< $idfile")) { print "unknown\n"; exit(3); } while () { chomp; # I'm told SuSe has /etc/SuSE-release # SuSE Linux 6.1 (i386) # VERSION = 6.1 if (/Linux\s+Mandrake\s+release\s+([\d.]+)/i || /Mandrake\s+Linux\s+release\s+([\d.]+)/i) { # Check Mandrake first because they also keep an # /etc/redhat-release as a symbolic link to their # /etc/mandrake-release. my $major = $1; $major =~ s/\.//g; $id = "mandrake$major"; $ecode = 0; # Found last; } elsif (/Red\s+Hat\s+Linux\s+release\s+([\d.]+)/i) { my $major = $1; $major =~ s/\.//g; $id = "redhat$major"; $ecode = 0; # Found last; } elsif (/SuSE\s+Linux\s+([\d.]+)/i) { my $major = $1; $major =~ s/\.//g; $id = "suse$major"; $ecode = 0; # Found last; } } close(RELID); # This last bit allows one to define a different location for the # 'virgin' RPMs, could be the path on the CDROM or an NFS path # or wherever the 'virgin' distribution is found. if (defined($modCdPath)) { $Cfg{'rpm-dist-dir'} = $modCdPath; } if ($interactive == 1) { print "$id\n" if ($ecode == 0); exit($ecode); } return($ecode); } sub RegressionTest { my ($rtfile, $total_tests, $total_failed); # Other types of regression tests are added here, a # mnemonic is associated with a bare filename so, # --rt vc -> use versioncmp.rt # We search for the RT file first in ./misc/ and then in # the lib-dir configuration. if ($optRT eq 'vc') { $rtfile = 'versioncmp.rt'; } else { ¬ify("Unknown regression test $optRT\n"); exit(10); } # If we are doing development let's use the one in the # current work area as opposed to the one that is installed. if (-f "misc/$rtfile") { $rtfile = "misc/$rtfile"; } elsif (-f "$Cfg{'lib-dir'}/$rtfile") { $rtfile = "$Cfg{'lib-dir'}/$rtfile"; } else { print "Cannot find regression test file\n"; exit(11); } my $engine = &$VersionComparePtr('ident','ident'); print "Title: Version comparison engine $engine\n"; # Perform the regression test $total_tests = $total_failed = 0; open(RTFILE, "< $rtfile") || die "$rtfile: $!\n"; while () { chomp; print "Rule $'\n" if ($_ =~ m/^\s*#\s*%Rule/i); next if ($_ =~ m/^\s*#/); next if ($_ =~ m/^\s*$/); if ($optRT eq 'vc') { # Test version compare algorithm my (@data, $result, $rule); @data = split(/\s+/, $_, 3); $result = &$VersionComparePtr($data[0], $data[1], \$rule); printf "\t%15s -> %15s %3s : %+1d R%s ", $data[0], $data[1], $data[2], $result, $rule; $total_tests++; if ($result == $data[2]) { print "OK\n"; } else { $total_failed++; print "FAILED\n"; } } } close(RTFILE); my $per_ok = ($total_tests - $total_failed) / $total_tests * 100; $per_ok = int($per_ok * 100) / 100; print "Totals: $total_tests cases, $total_failed failed ($per_ok\%)\n"; exit(0); } sub compareCache { my $reverse = shift; my $key = shift; my $rRes = shift; my $rC1 = shift; my $rC2 = shift; my $rC3 = shift; my ($ver1, $ver2, %d1, %d2); %d2 = &convert2hash($$rC2{$key}); $ver2 = "$d2{'version'}-$d2{'release'}"; if (!exists($$rC1{$key})) { if ($reverse) { $$rRes{'discontinued'}++; $ver1 = $ver2; $ver2 = 'none'; } else { $$rRes{'added'}++; # Something new (most of the cases) $ver1 = 'none'; } $$rC3{$key} = join('|', $key, $ver1, $ver2); delete $$rC2{$key}; } else { %d1 = &convert2hash($$rC1{$key}); $ver1 = "$d1{'version'}-$d1{'release'}"; if ($ver1 ne $ver2) { # Different version/release so indicate that $$rRes{'mutations'}++; } else { $$rRes{'unchanged'}++; } if ($reverse) { my $tmp; $tmp = $ver1; $ver1 = $ver2; $ver2 = $tmp; } # if ($ver1 ne $ver2) { $$rC3{$key} = join('|', $key, $ver1, $ver2); # } delete $$rC1{$key}; delete $$rC2{$key}; } } sub CompareDistribution { my $dist1 = shift; my $dist2 = shift; my ($key, $ver1, $ver2, $comment, $rest, %dbdist1, %dbdist2); my (%db, %results, %rstat, %comments); format COMPARE_TOP = Distribution Comparison (Page @||||) $% Showing Additions (@|||) Discontinued (@|||) Mutations (@|||) Unchanged (@|||) $rstat{'a'} $rstat{'r'} $rstat{'m'} $rstat{'s'} Package name @>>>>>>>>>>>>> @>>>>>>>>>>>> Comments $dist1 $dist2 ------------------------- -------------- -------------- ---------------------- . format COMPARE = @<<<<<<<<<<<<<<<<<<<<<<<<< @>>>>>>>>>>>> @>>>>>>>>>>>> @<<<<<<<<<<<<<<<<<<<<<<< $key $ver1 $ver2 $comment . $^L = "\n" . '~' x 70 . "\n"; $~ = "COMPARE"; $optCompare =~ tr/[a-z]/[A-Z]/; &dbLoad($dist1, \%dbdist1); &dbLoad($dist2, \%dbdist2); open(DBREFERENCE, "< ../databases/rpmnotes") || die "/var/local/rpmlevel/rpmnotes: $!\n"; while () { chomp; ($key, $rest) = split(":",$_,2); $comments{$key} = $rest; } close(DBREFERENCE); # Test print the comments # for $key ( keys %comments ) { # print "$key : $comments{$key} \n"; } $dist1 = substr($dist1, rindex($dist1, '/') + 1); $dist2 = substr($dist2, rindex($dist2, '/') + 1); $results{'unchanged'} = 0; $results{'added'} = 0; $results{'discontinued'} = 0; $results{'mutations'} = 0; $rstat{'a'} = (index($optCompare,'A') == -1 ? 'no' : 'yes'); $rstat{'r'} = (index($optCompare,'R') == -1 ? 'no' : 'yes'); $rstat{'m'} = (index($optCompare,'M') == -1 ? 'no' : 'yes'); $rstat{'s'} = (index($optCompare,'S') == -1 ? 'no' : 'yes'); # First run through the latest distro, presumably it has # more packages than the previous one :) for (keys %dbdist2) { # Usually the newest has more stuff &compareCache(0, $_, \%results, \%dbdist1, \%dbdist2, \%db); } # Now do this in case the first is actually bigger for (keys %dbdist1) { &compareCache(1, $_, \%results, \%dbdist2, \%dbdist1, \%db); } # It's time to vomit! $= = 22; $= = $modLines if defined($modLines); for (sort keys %db) { # Only mutations are supposed to appear here ($key, $ver1, $ver2) = split(/\|/, $db{$_}); $comment = $comments{$key}; if ((index($optCompare,'A') > -1 && $ver1 eq 'none') || (index($optCompare,'R') > -1 && $ver2 eq 'none') || ((index($optCompare,'M') > -1 && ($ver2 ne 'none' && $ver1 ne 'none') && ($ver2 ne $ver1)) || (index($optCompare,'S') > -1 && $ver2 eq $ver1)) ) { if (index($optCompare,'S') > -1 && $ver2 eq $ver1) { $comment = ""; } write(); } } print "\nFrom $dist1 To $dist2" . "\n\tMutations: $results{'mutations'}" . "\n\tAdditions: $results{'added'}" . "\n\tRemovals : $results{'discontinued'}" . "\n\tUnchanged: $results{'unchanged'}\n"; exit(0); } # ***************************************************************** # M A I N # ***************************************************************** #my $vc = &versionCompare($ARGV[0], $ARGV[1]); #print "$ARGV[0] -> $ARGV[1] : $vc\n"; exit; my $dbfile; my $distro; $cvsId =~ m/Revision:\s+(\d+\.\d+\.*\d*\.*\d*)/; $version = $1; # For now let's have the possibility of multiple # version compare algorithms. There is no sure way of doing # it because nobody seems to respect standards and sadly # same goes for enforcing it. #$VersionComparePtr = \&versionCompare; # Used in 1.2-1 $VersionComparePtr = \&newVersionCompare; # Used in 1.3-1 &GetOptions('h|help' => \$optHelp, 'i|init' => \$optInit, 'r|report:s'=> \$optReport, 'sync' => \$optSync, 'identify' => \$optIdentify, 'stats' => \$optStats, 'compare=s' => \$optCompare, 'regroup=s' => \$optRegroup, 'rt=s' => \$optRT, # Regression test 'append' => \$modAppend, # Only with --init 'nosave' => \$modNoSave, # Only with --init or --sync 'lines=i' => \$modLines, # Only with --report --compare 'cdpath=s' => \$modCdPath, # Only with --init 'nosync' => \$modNoSync); # Only with --init &Help(0) if $optHelp; &RegressionTest if defined($optRT); if ($optInit || $optReport || $optSync || $optStats || $optRegroup) { &Help(1) if $#ARGV > 0; $dbfile = 'default'; $dbfile = $ARGV[0] unless $#ARGV == -1; $distro = $dbfile; $dbfile = "$Cfg{'level-dir'}/$dbfile.db"; } elsif (defined($optCompare)) { &Help(1) if $#ARGV != 1; &CompareDistribution($ARGV[0], $ARGV[1]); } &Statistics($dbfile) if $optStats; &Identify(1) if $optIdentify; &Init($dbfile) if $optInit; &Status($dbfile, $distro, $optReport) if $optReport; &Synchronize($dbfile) if $optSync; &ReGroup($dbfile, $optRegroup) if $optRegroup; &Help(2); # ***************************************************************** # H I S T O R Y # ***************************************************************** # 08.nov.1999 DEGT Initial version (1.1) # 15.nov.1999 DEGT Added --regroup # 19.nov.1999 DEGT Added headings to Statistics # 20.nov.1999 DEGT Theoretically speaking... Mandrake & SuSE # 21.nov.1999 DEGT Show distribution file in the header # 21.nov.1999 DEGT Added --nosave for --init and --sync, messages # 21.nov.1999 DEGT Added --append for --init (SuSE) # 21.nov.1999 DEGT Fixed sync problem, was losing previous upgrade tags # 01.dec.1999 DEGT Added --compare # 03.dec.1999 DEGT v1.2-1 Second Official Release # xx.xxx.1999 AG agonzalez@yahoo.com path for version compare # 28.dec.1999 DEGT Built-in regression test (--rt vc)