File sam of Package suse-sam
#!/usr/bin/perl -w
# vim: set et ts=8 sts=4 sw=4 ai si:
#
# sam - Supportability Analysis Module
#
# Copyright (c) 2008 SuSE Linux Products GmbH, Nuernberg, Germany
#
# Author: Olaf Dabrunz <od@suse.de>
# (based on 'sammi' by Raymund Will <rw@suse.de>)
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Notes:
#
# RPM epoch is not used (policy at SUSE/Novell and elsewhere), as it is sticky
# (every version upgrade must contain the right epoch value) and also not
# visible to the user.
#
# Extensions for SELinux, ACLs, capabilities and others need to be added when
# they are supported by both the SUSE Linux kernel and the SUSE version of RPM.
#
use strict;
use POSIX qw(strftime WIFEXITED WEXITSTATUS WIFSIGNALED WTERMSIG);
# handle HUP INT PIPE TERM ABRT QUIT with die, so the END block is executed
# which unlinks temporary files
use sigtrap qw(die untrapped normal-signals ABRT QUIT);
use File::Find;
my $progname = $0; $progname =~ s{^.*/}{};
my $progspcs = $progname; $progspcs =~ s{.}{ }g;
my $invocation_cmd_line = $progname ." ". join (" ", @ARGV);
my $prog_version = 0.6;
my $tmpdir = "/tmp";
my $pubring = "$progname.pubring.$$";
my $sigfile = "$progname.sigfile.$$";
my $signedfile = "$progname.signedfile.$$";
# unlink temp files in the end (but not if the program ends before temp file
# names are known)
my $unlink_ok = 0;
END {
if (defined $unlink_ok and $unlink_ok) {
unlink($pubring, $pubring . "~", $sigfile, $signedfile);
}
}
$ENV{"LC_ALL"} = "C";
delete $ENV{"LANG"};
my $Unsupportable = 0;
my $root_dir = "/";
my $rpm_command = "/bin/rpm";
my $gpg_exe = "/usr/bin/gpg";
my $gpg_command = "$gpg_exe --no-default-keyring --keyring $pubring " .
"--trust-model always";
my $zypp_conf = defined $ENV{'ZYPP_CONF'} ?
$ENV{'ZYPP_CONF'} : "/etc/zypp/zypp.conf"; # libzypp
# caches -- for development and debugging
my $cache_dir = "sam.d";
my $rpm_qa_cache = "$cache_dir/rpm-qa";
my $rpm_Vv_cache = "$cache_dir/rpm-Vv";
my $rpm_e_cache = "$cache_dir/rpm-e";
my $cache_file_version = "0.4";
# repositories -- for gpg keys and inst source information
my $default_zypp_cache = "/var/cache/zypp";
my $repossubdir = "repos.d";
my $metadatasubdir = "raw";
my $solvfilessubdir = "solv";
my $solvfilename = "solv";
my $reposdir;
my $metadatadir;
my $solvfilesdir;
my %repoinfo = ();
# installed package information
my %package2inst_time = ();
my %package2name = ();
my %package2edition = ();
my %skipped_packages = ();
my %alien_packages = ();
my %alien2name = ();
my %alien2edition = ();
my $num_sig_ok_packs = 0;
# package signing keys
my %good_key_ids = ();
# --------------------------------------------------------------------------
# SAM configuration
my $ALLOW_MULTIHOMED_OBJECTS = 0;
my $CHECK_SIGNATURE = 1;
my $VERIFY_PACKAGES = 1;
my $FIND_DETACHED = 1;
# hex encoding of RPM header prefix used when creating/verifying the header
# signature
my $headerprefix = "8eade80100000000";
# --------------------------------------------------------------------------
# statistics
my $Tstart = time();
my ($cacheMiss, $cacheHit) = (0, 0);
my ($aInodes, $aSize, $pInodes, $pSize) = (0, 0, 0, 0);
# --------------------------------------------------------------------------
# logging
#
my $debug = 0;
my $verbose = 1;
my $writeReport = 0;
my $logCont = 0;
my $logBuffer = "";
#
# Log to LOG file and maybe also STDERR, if we are $verbose enough for the
# $level of the message. "%T" in the message are replaced with a timestamp.
#
# 0 - really useful to know in most cases
# 1 - more verbose
# - show packages
# - for which updates exist but are not installed
# - which have the same evr as a package in a repo, but are not
# identical
# - output of some external programs
# - more steps reported
# 2 - show installed packages which are newer than available packages in repos
# 3 - show installed packages which are identical with some package in repos
# and which have no newer updates
# 4 - show informative output of programs
# 5 - show when heuristics drop candidates
# - dropping pseudo-packages
# - dropping files that where considered as gpg-keys but turned out not
# to be
#
# 8 - show execution and all output of several external programs
# 9 - show fatal error message
# - FIXME: describe purpose of other Log(9,...)
#
sub Log($$@) {
my ($level, $format, @args) = @_;
my $msg = sprintf($format, @args);
my $timestamp = strftime( "%Y-%m-%d_%H:%M:%S_Z", gmtime(time()));
$msg =~ s{(\%T)}{$timestamp}g;
$logCont = ( substr( $msg, -1, 1) eq "\n" ) ? 0 : 1;
print(LOG $msg);
if ($level < $verbose) {
print(STDERR $msg);
print(STDERR (($logCont) ? "C" : "")) if $debug;
}
return($msg);
}
#
# Report at log level 0
#
sub Report($@) {
my ($format, @args) = @_;
my $msg = Log(0, $format, @args);
print(REPORT $msg);
}
#
# Report at log level 2
#
sub ReportQ($@) {
my ($format, @args) = @_;
my $msg = Log(2, $format, @args);
print(REPORT $msg);
}
# ---------------------------------------------------------------------------
# Die. Exits the program with an error return value and an optional message.
#
# Die();
# Die(1);
# Die(2, "%T %s: could not open file %s\n", $progname, $file);
# Die("%T %s: could not open file %s\n", $progname, $file);
#
# Accepts zero or more parameters.
#
# If the first parameter is a number between -999 and 999, it is masked with
# 0x7f and used as an error code. It this case, the next parameter is used as
# the format string for the error message. Otherwise, the error code is -1 and
# the first parameter is used as the format string. Any following parameters
# are used as arguments to the format string.
#
# The message is logged at level 9, and an extra message is written to STDERR
# (using warn()).
#
sub Die(@) {
my (@args) = @_;
my ($error_code, $format) = (-1, undef);
my $t = shift(@args);
if (defined($t) && $t =~ m{^(0|-?[1-9][0-9]{0,2})$}) {
$error_code = $t;
$format = shift(@args);
} else {
$format = $t;
}
if (defined($format)) {
Log(9, "FATAL: $format", @args);
warn(sprintf("$format", @args));
}
exit($error_code & 127);
}
# ---------------------------------------------------------------------------
# Print package information as HTML
#
sub initHTML() {
print(HTML
" <table id=\"SAM-list\">\n",
" <thead>\n",
" <th>Name</th><th>Version</th><th>Supported</th>",
" <th>Notes</th>\n",
" </thead>\n",
" <tbody>\n");
}
sub finishHTML() {
print(HTML
" </tbody>\n",
" </table>\n");
}
sub HTMLize($) {
return @_;
}
my $hOE = "odd ";
sub printHTML($$$) {
my ($p, $r, $n) = @_;
my ($N, $E, $c) = (HTMLize($package2name{$p}), HTMLize($package2edition{$p}), "class");
my ($s, $b) = (($r == 0) ? (" supported", "Yes") : ("unsupported", "No"));
print(HTML " "x8 . "<tr $c=\"$hOE $s\"><th>$N</th><td $c=\"edition\">",
"$E</td><td $c=\"support\">$b</td><td>$n</td></tr>\n");
$hOE = (($hOE eq "odd ") ? "even" : "odd ");
}
# ---------------------------------------------------------------------------
# Print package information as JSON
#
BEGIN() {
eval { require JSON::XS; };
if ( $@ ) {
die unless ($@ =~ m{Can't locate});
} else {
require JSON::XS;
}
}
my %J = ();
my $js_true = 1;
my $js_false = 0;
my $json;
eval { require JSON::XS; };
if ( $@ ) {
die unless ($@ =~ m{Can't locate});
$json = undef;
} else {
$js_true = JSON::XS->true;
$js_false = JSON::XS->false;
$json = JSON::XS->new->ascii->pretty->allow_nonref->canonical;
$json = $json->space_before(0)->space_after(1)->indent(1);
}
sub JSONize($) {
my ($s) = @_;
$s =~ s{([\"\\\/])}{\\$1}g;
return ( $s );
}
sub fillJSON($$$) {
my ($p, $r, $n) = @_;
#print( JSON $json( [$p2name{$p}, $p2edition{$p}, $r, $n ]), "\n");
push @{ $J{"packages"} },
{"0::name" => $package2name{$p},
"1::edition" => $package2edition{$p},
"2::support" => (($r == 0) ? $js_true : $js_false),
"3::note" => $n
};
}
sub finishJSON() {
my $j;
if ( defined( $json) ) {
$j = $json->encode( \%J);
} else {
$j = "Please install JSON::XS and cie.\n";
}
$j =~ s{ \{ \s* \" }{\{ \"}gsx;
$j =~ s{ \s+ \}, }{ \},}gsx;
$j =~ s{ ([^\}],) \s* }{$1 }gsx;
$j =~ s{ \s+ (\}) }{ $1}gsx;
$j =~ s{ (\") [0-9] \:\: (\S+\") }{$1$2}gmx;
$j =~ s{ ^ \s+ (\{) }{ $1}gmx;
$j =~ s{ ^ \s+ (\]) }{$1}gmx;
print( JSON $j, "\n");
}
# ---------------------------------------------------------------------------
# finding package source repository with libsatsolver
#
BEGIN() {
eval { require satsolver; };
if ( $@ ) {
die unless ($@ =~ m{Can't locate});
} else {
require satsolver;
}
}
my $satsolver = 0;
my %needed_methods = (
'Pool' => ['providers'],
'Repo' => ['solvables'],
'Solvable' => ['compare', 'identical'],
);
eval { require satsolver; };
if ( $@ ) {
die unless ($@ =~ m{Can't locate});
} else {
$satsolver = 1;
foreach my $subpack (keys %needed_methods) {
foreach my $sym (@{$needed_methods{$subpack}}) {
if (not (defined $satsolver::{"${subpack}::"}->{$sym} or
defined $satsolver::{$sym})) {
$satsolver = -1;
}
}
}
}
# ---------------------------------------------------------------------------
# Reading config files
#
sub simplify_path ($) {
my ($path) = @_;
# remove additional "/"
$path =~ s{//+}{/}og;
return $path;
}
sub get_repo_conf ($) {
my ($root_dir) = @_;
my $cachedir;
$zypp_conf =~ m{^(.*)/};
my $zypp_confdir = $1;
if (not open(CONF, "<", "$root_dir/$zypp_conf")) {
Log(0, " open(\"$root_dir/$zypp_conf\"): $!\n");
} else {
# get metadatadir, reposdir (fallback) and solvfilesdir from zypp.conf
while (<CONF>) {
$cachedir = $1, next if m{^\s*cachedir\s*=\s*(\S+)\s*$};
$reposdir = $1, next if m{^\s*reposdir\s*=\s*(\S+)\s*$};
$metadatadir = $1, next if m{^\s*metadatadir\s*=\s*(\S+)\s*$};
$solvfilesdir = $1, next if m{^\s*solvfilesdir\s*=\s*(\S+)\s*$};
}
close(CONF);
}
$cachedir = $default_zypp_cache if not defined $cachedir;
$reposdir = "$zypp_confdir/$repossubdir" if not defined $reposdir;
$metadatadir = "$cachedir/$metadatasubdir" if not defined $metadatadir;
$solvfilesdir = "$cachedir/$solvfilessubdir" if not defined $solvfilesdir;
$cachedir = simplify_path("$root_dir/$cachedir");
$reposdir = simplify_path("$root_dir/$reposdir");
$metadatadir = simplify_path("$root_dir/$metadatadir");
$solvfilesdir = simplify_path("$root_dir/$solvfilesdir");
}
sub get_repo_infos () {
foreach my $repofile (glob("$reposdir/*.repo")) {
if (not open(REPO, "<", $repofile)) {
Log(0, " open(\"$repofile\"): $!\n");
} else {
# get reposubdir, name, and baseurl from this *.repo file
my $subdir;
while (<REPO>) {
$subdir = $1, last if m{^\s*\[(.*)\]\s*$};
}
if (defined $subdir) {
while (<REPO>) {
$repoinfo{$subdir}->{'name'} = $1, next if m{^\s*name\s*=\s*(.+)\s*$};
$repoinfo{$subdir}->{'baseurl'} = $1, next if m{^\s*baseurl\s*=\s*(\S+)\s*$};
}
}
close(REPO);
}
}
foreach my $subdir (keys %repoinfo) {
my $info_ref = $repoinfo{$subdir};
my $solvfile = "$solvfilesdir/$subdir/$solvfilename";
if ( -r $solvfile ) {
$repoinfo{$subdir}->{'solvfile'} = $solvfile;
} else {
# delete repositories without solv file
Log(0, " skipping repository without solv file %s\n" .
" name: %s\n baseurl: %s\n",
"$subdir:",
$info_ref->{'name'} . ",",
$info_ref->{'baseurl'});
delete $repoinfo{$subdir};
next;
}
my $contentfile = "$metadatadir/$subdir/content";
if (not open(CF, "<", $contentfile)) {
Log(0, " open(\"$contentfile\"): $!\n");
} else {
# get label from this content file
while (<CF>) {
$info_ref->{'label'} = $1, last if m{^\s*LABEL\s*(.+?)\s*$};
}
close(CF);
}
}
# make duplicate names and labels unique
foreach my $subdir (keys %repoinfo) {
my $info_ref = $repoinfo{$subdir};
my $name = $info_ref->{'name'};
my $label = $info_ref->{'label'};
my $next_name_cnt = 2;
my $next_label_cnt = 2;
foreach my $subdir2 (keys %repoinfo) {
next if ($subdir eq $subdir2);
if ($name eq $repoinfo{$subdir2}->{'name'}) {
$repoinfo{$subdir2}->{'name'} = "$name (" . ($next_name_cnt++) . ")";
}
if ($label eq $repoinfo{$subdir2}->{'label'}) {
$repoinfo{$subdir2}->{'label'} = "$label (" . ($next_label_cnt++) . ")";
}
}
$info_ref->{'name'} = "$name (1)" if $next_name_cnt > 2;
$info_ref->{'label'} = "$label (1)" if $next_label_cnt > 2;
}
# assign number and print found repos
my $cnt = 0;
foreach my $subdir (sort (keys %repoinfo)) {
my $info_ref = $repoinfo{$subdir};
$info_ref->{'number'} = ++$cnt;
Log(0, " found repository #%d: %s\n" .
" name: %s\n label: %s\n baseurl: %s\n",
$info_ref->{'number'},
"$subdir:",
$info_ref->{'name'} . ",",
$info_ref->{'label'} . ",",
$info_ref->{'baseurl'});
}
}
# ---------------------------------------------------------------------------
# Setting up a keyring with SUSE/Novell build keys
#
# SUSE/Novell vendors of supported packages
my $Vendors = qr((?:
SuSE\ GmbH |
SuSE\ AG |
SuSE\ Linux\ AG |
SUSE\ LINUX\ Products\ GmbH |
UnitedLinux\ LLC |
Novell
))ixo;
# SUSE/Novell build key of supported packages
my $Buildkeys = qr((?:
(?:SuSE|$Vendors)\ Package\ Signing\ Key |
(?:SuSE|$Vendors)\ Security\ Team |
Novell Provo Build |
Open Enterprise Server
))ixo;
# SUSE/Novell repository content file labels for supported installation sources
my $Labels = qr((?:
SUSE |
Novell
))ixo;
sub setup_key () {
my $cmd;
my ($key_id_string, $date, $pub_comment, $pub_line);
# only consider *.key and *.asc files
return if not m{\.(key|asc)$}io;
# find public keys and check comment string against SUSE/Novell vendor
# strings
$cmd = "$gpg_command $File::Find::name";
open(FH, "$cmd 2>&1 |") || Die("$cmd: failed to execute: $!\n");
while (<FH>) {
chomp;
if (m{^pub\s+(\S+)\s+(\S+)\s+(.*)$}) {
($key_id_string, $date, $pub_comment) = ($1, $2, $3);
$pub_line = $_;
}
Log(8, " $_\n");
}
close(FH);
if (not defined $pub_comment) {
Log(5, " gpg: not a public key file: %s\n",
$File::Find::name);
} elsif ($pub_comment =~ m{^$Buildkeys}) {
if (not defined $good_key_ids{$key_id_string}) {
Log(1, " gpg: using SUSE/Novell public key file: %s: %s\n",
$File::Find::name, $pub_line);
$good_key_ids{$key_id_string} ++;
$cmd = "$gpg_command --import $File::Find::name";
System($cmd);
} else {
Log(4, " gpg: already imported SUSE/Novell public key file: %s: %s\n",
$File::Find::name, $pub_line);
}
} else {
Log(4, " gpg: not using foreign public key file: %s: %s\n",
$File::Find::name, $pub_line);
}
}
# ---------------------------------------------------------------------------
# Is the package from us?
#
sub is_our_package ($$$$$) {
my ($rsaheadersig, $dsaheadersig, $rpmheader, $package_name, $vendor) = @_;
my $headersig;
my $cmd;
my $is_ours = 0;
# user asked to skip sig check or
# no SUSE/Novell keys we could use for checking?
if (not $CHECK_SIGNATURE or scalar (keys %good_key_ids) == 0) {
# cannot check, so fall back on the vendor string in the RPM DB and
# continue anyway, hoping for the best
return $vendor =~ m(^$Vendors)o;
}
# does the RPM DB have a signature for the header?
# use RSAHEADER (more hash bits) if available, or fall back to DSAHEADER
$headersig = ($rsaheadersig =~ m{(none)}) ? $dsaheadersig : $rsaheadersig;
if ($headersig =~ m{(none)}) {
# cannot check, use fallback
if ($vendor =~ m(^$Vendors)o) {
$is_ours = 1;
}
# log at level 0 if one of "our" packages had no signature (should not happen),
# log at level 4 if it is a "foreign" package without signature
Log($is_ours ? 0 : 4,
" rpm: no header signature, using vendor: %-45s %s\n",
"$package_name:", $vendor);
return $is_ours;
}
# convert hex strings to binary
my $rpmheader_bin = pack("H*H*", $headerprefix, $rpmheader);
my $headersig_bin = pack("H*", $headersig);
# save RPM header and signature
open(FH, ">", "$signedfile") || Die( "open($signedfile): $!\n");
print(FH $rpmheader_bin);
close(FH);
open(FH, ">", "$sigfile") || Die( "open($sigfile): $!\n");
print(FH $headersig_bin);
close(FH);
# check the signature of the RPM header with our selected keys
$cmd = "$gpg_command --verify $sigfile $signedfile";
open(FH, "$cmd 2>&1 |") || Die( "command failed: $cmd: $!\n");
while (<FH>) {
chomp;
if (m{^gpg:\s*Good\s+signature}io) {
Log(4, " $_: $package_name: $vendor\n");
$is_ours = 1;
} elsif (m{^gpg:\s*(?:Can't|Cannot)\s+check\s+signature}io) {
Log(5, " $_: $package_name: $vendor\n");
}
}
close(FH);
$num_sig_ok_packs += $is_ours;
return $is_ours;
}
my %prettyR =
(
"S" => "size",
"M" => "mode",
"5" => "checksum",
"D" => "device-node",
"L" => "sym-link",
"U" => "owner",
"G" => "group",
"T" => "mod-time",
"?" => "cannot-read",
);
#
# return pretty-printed result
#
sub prettyR($) {
my ($result) = @_;
my ($pretty) = ("'$result'");
$_ = $result;
if ( m{^U\:miss\s*(.)(\s+(.*))?$} ) {
$pretty = "missing" . (defined $3 ? " $3" : "");
} elsif ( m{^U\:mod (.):([SM5?DLUGT]+)(?: (.*)|)$} ) {
my ($kind, $summary_result, $R) = ($1, $2, $3);
my @L = map( $prettyR{$_}, split(//, $summary_result));
$pretty = "modified: " . join(", ", @L);
}
return $pretty;
}
#
# Create pretty-printed string for a size in bytes
#
sub prettyK($) {
my ($n) = @_;
my ($f, $p);
my @P = ("M", "G", "T");
if ( $n < 0 ) {
return sprintf( "%3dk??", $n);
} elsif ( $n < 1000 ) {
return sprintf( "%3d kB", $n);
}
while ( $n > 999 ) {
$p = shift(@P);
$f = $n % 1024;
$n = $n >> 10;
}
if ( $n > 9 ) {
return sprintf("%3d %sB", $n, $p);
}
$f = int(($f * 10 ) / 1024);
return sprintf("%d.%d %sB", $n, $f, $p);
}
#
# Execute program and die on errors with appropriate message
# Program output is logged at log level 2
# Also logs the command at log level 8
#
sub System ($) {
my ($cmd) = @_;
my @C = split(/ /, $cmd);
Log(8, "+$cmd\n");
# open a pipe to catch output as well
open(FH, "$cmd 2>&1 |") || Die("$C[0]: failed to execute: $!\n");
while (<FH>) {
Log(4, " " . $_);
}
close(FH);
if (WIFSIGNALED($?)) {
Die(sprintf( "$C[0]: died with signal %d, %s coredump\n",
(WTERMSIG($?)), ($? & 128) ? 'with' : 'without'));
} elsif ( WEXITSTATUS($?) != 0 ) {
Die("$C[0]: failed with error code %d\n", WEXITSTATUS($?));
}
}
# ---------------------------------------------------------------------------
# Find files that do not belong to any RPM package
#
# TODO: handle exclusion of directories
#
my @orphans = ("undef");
my %dirpath2devinode;
my %dircontents;
sub findOrphans($) {
our ($rootdir) = @_;
our ($rootlen, $rootdev, $ignoredir);
# based on 'airbag,v 1.2 2001/10/02 15:04:30'
# created by Torsten Duwe
# modified by Raymund Will
# "find" of additional files; more precisely files and directories that
# do not come from installed RPMs.
# We take a fsck-like approach: %dirpath2devinode holds a [dev:inode]
# pair for given directory path(s) and %dircontents stores the
# directory content's names, as if they had been received via
# opendir() and readdir().
# First we fill the %dirpath2devinode / %dircontents cache with
# list info from "rpm -qal", then we do a "find /" and report all new
# files and dirs encountered, pruning dirs, of course. A few
# well-known candidates are suppressed, for convenience.
# subroutine pathhash: make sure [dev:inode] pair for this path is
# known as well as those of all of its parents. Argument is a path
# string.
sub pathhash($);
sub pathhash($){
my($path) = @_;
print STDERR "pathhash($path): " if ($debug & 0x1);
my($dev,$ino,$mode,@rest,$parent,$myname);
# defensive programming: make sure our path string has exactly
# one slash at the beginning and for subdir separation, and no
# slash at the end.
$path =~ s,/+,/,g;
#$path =~ s,/$,,g;
$path =~ s,^/,,g;
$path = "/$path";
print STDERR "-> '$path'" if ($debug & 0x1);
if (defined $dirpath2devinode{$path}) {
print STDERR "=> known\n" if ($debug & 0x1);
return;
} # already known
print STDERR ": stat\n" if ($debug & 0x1);
($dev,$ino,$mode,@rest) = stat($rootdir . $path);
if (@rest < 10) {
print STDERR "cannot stat($rootdir,$path): $!\n"; return;
}
# if we stat()ed a directory, let's remember it.
if (($mode & 0xf000) == 0x4000) {
$dirpath2devinode{$path} = "$dev:$ino";
$dircontents{"$dev:$ino"} = "" unless defined($dircontents{"$dev:$ino"});
}
# so this one was new. how about the parent dir ? recursion will
# stop at "/" (provided it's the real root!), which is its own
# parent and will be "already known" above.
return if ( $path eq "/" );
$parent = $path;
$parent =~ s,/([^/]*)/?$,,;
$myname = "$1";
$parent =~ s,^/,,;
$parent = "/$parent";
# print(STDERR "parent='$parent' myname='$myname' %> ");
pathhash($parent);
# back from recursion -- ensure this path's name is listed in
# parent's contents.
if ($dircontents{$dirpath2devinode{$parent}} =~ m,/\Q$myname/, ){
# print " already have $parent##/##$myname\n";
} else {
$dircontents{$dirpath2devinode{$parent}} .= "/$myname/";
# print " $parent##/##$myname\n";
}
# print "$dirpath2devinode{$dir} <= $dir\n";
# $dirpath2devinode{$dir};
}
$rootlen = length($rootdir);
$rootdev = (lstat($rootdir))[0];
$ignoredir = 1;
$debug = 0;
$| = 1;
if ( $> == 0 && ( -x "./bin/rpm" ) ) {
open(FLIST, "chroot '$rootdir' ./bin/rpm -qal |") ||
Die( "open( chroot rpm -qa): $!\n");
} else {
open(FLIST, "/bin/rpm -qal --root '$rootdir'|") ||
Die( "open( rpm -qa): $!\n");
}
while(<FLIST>){
my($dir, $fname, $inode);
chomp;
s,/$,,; # doesn't ever happen, anyway.
s,/+,/,g;
m,^(.*/)([^/]+)$, || next;
$dir = $1; $fname = $2;
$dir =~ s,^/,,;
$dir = "/$dir";
next unless ( -d "$rootdir$dir" );
pathhash($dir);
$inode = $dirpath2devinode{$dir};
$dircontents{$inode} .= "/$fname/";
}
close( FLIST);
# subroutine wanted: called by the file tree walk for every node, with
# the basename() of the current node as string argument.
sub wanted() {
my($dir) = "/" . substr($File::Find::dir, $rootlen) . "/";
$dir =~ s,/+,/,g;
print STDERR "wanted: $_, dir='$dir'\n" if ($debug & 0x2);
# omit dot and dotdot, backup files, and well-known boring paths.
m/^\.\.?$/ && return;
m/~$/ && return;
# m/\.rpmorig$/ && return;
# m/\.rpmnew$/ && return;
# m,^/vmlinu, && return;
# m,^/initrd, && return;
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,@rest) = lstat($_);
$aInodes++;
$aSize += ($size + 512) / 1024;
if ($dev != $rootdev ) { $File::Find::prune = 1; return; }
if ($File::Find::name =~ m,^/vmlinu, ) { return; }
if ($File::Find::name =~ m,^/initrd, ) { return; }
if ($File::Find::name =~ m,/man/whatis$, ) { return; }
if ($File::Find::name =~ m,^/proc, ) { $File::Find::prune = 1; return; }
if ($File::Find::name =~ m,^/etc/rc\.d/rc[0-6]\.d, )
{ $File::Find::prune = 1; return; }
# if (!defined $dirpath2devinode{$dir}) {
# print " XXX $dir XXX no match in hash table !\n";
# return;
# }
# see if we know the dir we're in
pathhash($dir);
my $inode = $dirpath2devinode{$dir};
# does it know about the file/dir we're examining at this invocation ?
if ( $dircontents{$inode} =~ m,/\Q$_/, ) { # yes, ok.
print STDERR "known: ($dir) $_\n" if ($debug&0x4);
return;
} elsif ( $dir eq "/home/httpd/icons/" ) {
print STDERR "UNknown: ($dir) $_\n" if ($debug&0x4);
print STDERR "$inode=>'$dircontents{$inode}'\n" if ($debug&0x4);
}
# if not, let's have a closer look.
# we're not interested in symlinks at all.
if (($mode & 0xf000) == 0xa000) { return; }
my $isdir = "";
if (($mode & 0xf000) == 0x4000) {
return if ( $ignoredir );
$isdir = "/";
# maybe we know this directory, but by another name, if
# the installation has followed symlinks like /opt -> /usr/opt
if (defined $dircontents{"$dev:$ino"}) { return; }
}
$File::Find::prune = 1;
# the rare case of a l&f directory under a mount point. Checked here
# because of its low probability and because we want prune=1 for it.
if ($isdir eq "/" && $_ eq "lost+found" && $inode =~ /:2$/) { return; }
#print" $File::Find::name$isdir\n";
push @orphans, "$dir$_$isdir";
}
#$debug = 0x4;
find(\&wanted, $rootdir); # Launch !
}
# ---------------------------------------------------------------------------
# Return filehandle for the list of RPMs with ancillary data.
# May use cached data or pipe directly from the rpm command.
#
sub rpm_qa($) {
my ($root_dir) = @_;
my $FH;
my $rpmQ = "$rpm_command -qa --qf " .
"'%{NAME} %{VERSION}-%{RELEASE} %{ARCH} %{INSTALLTIME} " .
"%{VENDOR:shescape} %{RSAHEADER} %{DSAHEADER} %{HEADERIMMUTABLE}\n'";
# Either use cache if available...
if ( -d $cache_dir && -r $rpm_qa_cache ) {
open( $FH, "< $rpm_qa_cache") || Die( "open(rpm -qa): $!\n");
$_ = <$FH>;
if ( ! m{^# (\S+) -- (.*)$} ) {
Die("$progname: unknown cache format! Please remove.\n" .
"(e.g. with 'rm -rf $cache_dir/rpm-{qa,Vv})'\n");
} else {
my ($wrong_vers, $wrong_root) =
($cache_file_version ne $1, $root_dir ne $2);
if ( $wrong_vers or $wrong_root ) {
Die("$progname: invalid cache: %s%s%s. Please remove.\n" .
"(e.g. with 'rm -rf $cache_dir/rpm-{qa,Vv})'\n",
( $wrong_vers ? "wrong version" : "" ),
( $wrong_vers and $wrong_root ? " and" : "" ),
( $wrong_root ? "different root dir checked" : "" ));
# or should we only warn and "refresh" it automagically?
} else {
##$cacheHit ++;
#return $FH;
close($FH);
}
}
}
# rpm -qa is usually so fast, that we only cache to "record" the list
# of installed packages => no "cache accounting"
##$cacheMiss ++;
Log(8, "+%s\n", $rpmQ);
# ... or read directly from rpm command (and recreate cache if possible)
open($FH, "$rpmQ |") || Die( "rpm: $!\n");
if (-d $cache_dir) {
if (open(OUT, "> $rpm_qa_cache") ) {
print OUT "# $cache_file_version -- $root_dir\n";
while (<$FH>) {
print OUT;
}
close(OUT);
close($FH);
open($FH, "< $rpm_qa_cache") || Die( "reopen: $rpm_qa_cache: $!\n");
# skip version / root_dir string
$_ = <$FH>;
# pre-create directory for rpm_V()
if (! -d $rpm_Vv_cache) {
mkdir($rpm_Vv_cache) || warn "mkdir $rpm_Vv_cache: $!\n";
}
# pre-create directory for rpm_e()
if (! -d $rpm_e_cache ) {
mkdir($rpm_e_cache) || warn "mkdir $rpm_e_cache: $!\n";
}
} else {
warn("create: $rpm_qa_cache: $!\n");
}
}
return $FH;
}
#
# return filehandle for the output of "rpm -V..." on a package
# may use cached data or pipe directly from the rpm command
#
sub rpm_V($$) {
my ($root_dir, $package) = @_;
my $FH;
my $cache_file = "$rpm_Vv_cache/$package";
my $rpmV = "$rpm_command -Vv '$package' 2> /dev/null";
if (-r $cache_file) {
$cacheHit ++;
open($FH, "< $cache_file") || Die( "open($cache_file): $!\n");
return $FH;
}
$cacheMiss ++;
open($FH, "$rpmV |") || Die( "rpm -V: $!\n");
if (-d $rpm_Vv_cache) {
if (open(OUT, "> $cache_file")) {
while (<$FH>) {
print OUT;
}
close(OUT);
close($FH);
open($FH, "< $cache_file") || Die( "reopen($cache_file): $!\n");
} else {
warn("create: $cache_file: $!\n");
}
}
return $FH;
}
#
# return filehandle for the output of "rpm -e --test ..." on a package
# may use cached data or pipe directly from the rpm command
#
sub rpm_e($$) {
my ($root_dir, $package) = @_;
my $FH;
my $cache_file = "$rpm_e_cache/$package";
my $rpme = "$rpm_command -e --test '$package' 2> /dev/null";
if ( -r $cache_file ) {
$cacheHit ++;
open($FH, "< $cache_file") || Die( "open($cache_file): $!\n");
return $FH;
}
$cacheMiss ++;
open($FH, "$rpme |") || Die( "rpm -e: $!\n");
if (-d $rpm_e_cache) {
if (open(OUT, "> $cache_file") ) {
while (<$FH>) {
print OUT;
}
close(OUT);
close($FH);
open($FH, "< $cache_file") || Die( "reopen($cache_file): $!\n");
} else {
warn("create: $cache_file: $!\n");
}
}
return $FH;
}
# ---------------------------------------------------------------------------
# Assess if this file was changed in an unsupportable way. Return descriptive
# string for the supportability information based on the evaluation of the
# "rpm -V..." output for a single file from some package.
#
# assessment results:
# O: OK (miss/mod: no, supportability problem: no, report: lvl 4)
# H: Harmless (miss/mod: yes, supportability problem: no, report: lvl 3)
# T: Tolerable (miss/mod: yes, supportability problem: no, report: lvl 2)
# U: Unsupportable (miss/mod: yes, supportability problem: yes, report: lvl 1)
#
# change state of files:
# OK OK, no changes
# miss missing
# mod modified
#
# TODO: check report levels are well chosen and documented corrrectly
#
sub assess($$$$$) {
my ($rpm, $file, $kind, $result, $error) = @_;
my $summary_result = $result;
$summary_result =~ s{\.}{}g;
$error = (defined $error ? " ($error)" : "");
if ( $result =~ m{^\.{8}$} ) {
# file is not modified at all: OK
return "O:OK";
} elsif ( $result eq "missing " && $kind eq "d" ) {
# missing documentation: Harmless
return "H:miss doc" . $error;
} elsif ( $result eq "missing " ) {
# missing non-documentation file: Unsupportable
return "U:miss $kind" . $error;
} elsif ( $kind eq "c" ) {
# existing config file with any kind of changes:
# Harmless
return "H:mod c:$summary_result";
} elsif ( $result =~ m{^[UG.]{8}$} ) {
# existing (non-config) file with ownership change only:
# Tolerable
return "T:mod $kind:$summary_result";
} elsif ( $result =~ m{^[MUG.]{8}$} ) {
# existing (non-config) file with exactly some kind of
# ownership change and file mode change: Unsupportable
return "U:mod $kind:$summary_result";
} elsif ( $result =~ m{^[T.]{8}$} ) {
# existing (non-config) file with some kind of metadata
# change that does not affect ownership or file mode (and
# no other changes): Tolerable
return "T:mod $kind:$summary_result";
} else {
# existing (non-config) file
# - has a change in file size
# - has a content change
# - is a device node and major/minor has changed
# - is a softlink that has changed
# -> Unsupportable
return "U:mod $kind:$summary_result";
}
}
sub max($$) {
my ($a, $b) = @_;
return ($a > $b) ? $a : $b;
}
# Usage message
sub Usage($$) {
my( $rv, $msg) = @_;
print( STDERR $msg . "\n") if ( $msg );
print STDERR <<"EOF";
$progname $prog_version
Supportability Analysis Module
$progname [-v|--verbose] [-q|--quiet] [-d|--debug] [-t|--tmpdir <tmpdir>]
$progspcs [--no-header-sig-check] [--no-rpm-verify] [--no-orphan-search]
$progspcs [-w|--write] [root_path]
$progname [-h|--help]
If <root_path> is specified, it will be used as the path to the root of the
installation to verify.
Options:
-w|--write write reports to sam.* files
-t|--tmpdir write temporary files to <tmpdir> (default: $tmpdir)
$progname needs about 100KB for temporary files
--no-header-sig-check skip checking RPM header signature of installed packs
--no-rpm-verify skip verifying installed files against the RPM db
--no-orphan-search skip searching for orphaned files
-h|--help print this help message
-v|--verbose increase verbosity level
-q|--quiet decrease verbosity level
-d|--debug increase debug level
EOF
Die( $rv);
}
# ---------------------------------------------------------------------------
# Main program
#
{
use Getopt::Long;
$Getopt::Long::debug = 0;
$Getopt::Long::ignorecase = 0;
$Getopt::Long::bundling = 1;
$Getopt::Long::passthrough = 0;
my %Opt = ();
Usage( -1, "") unless ( GetOptions( \%Opt,
'help|h', , 'verbose|v+', 'quiet|q+', 'debug|d+',
'write|w', 'tmpdir|t:s', 'no-header-sig-check',
'no-rpm-verify', 'no-orphan-search'));# && ! $Opt{'help'} );
Usage(0, "") if ( $Opt{'help'} );
$debug += $Opt{'debug'} if ( $Opt{'debug'} );
$verbose += $Opt{'verbose'} if ( $Opt{'verbose'} );
$verbose -= $Opt{'quiet'} if ( $Opt{'quiet'} );
$writeReport = 1 if ( $Opt{'write'} );
$tmpdir = $Opt{'tmpdir'} if ( $Opt{'tmpdir'} );
$CHECK_SIGNATURE = 0 if ( $Opt{'no-header-sig-check'} );
$VERIFY_PACKAGES = 0 if ( $Opt{'no-rpm-verify'} );
$FIND_DETACHED = 0 if ( $Opt{'no-orphan-search'} );
}
$verbose = ($verbose < 0) ? 0 : $verbose;
$debug = ($debug <= 0) ? 0 : (1 << $debug) - 1;
if ( exists( $ARGV[0]) && -d $ARGV[0] ) {
$root_dir = $ARGV[0];
$rpm_command .= " --root '$root_dir'";
}
#
# set up temp file names and delete stale temp files
#
Die("$tmpdir is not a writeable directory\n") if ( not (-d $tmpdir and -w $tmpdir) );
$pubring = "$tmpdir/$pubring";
$sigfile = "$tmpdir/$sigfile";
$signedfile = "$tmpdir/$signedfile";
$unlink_ok = 1;
unlink($pubring, $sigfile, $signedfile);
#
# write reports if we have the $cache_dir directory
#
$writeReport++ if ( -d $cache_dir );
if ( $writeReport ) {
open( LOG, "> sam.log") || Die("open(LOG): $!\n");
open( REPORT, "> sam.report") || Die("open(REPORT): $!\n");
open( HTML, "> sam.html") || Die("open(HTML!\n");
open( JSON, "> sam.json") || Die("open(JSON!\n");
} else {
open( LOG, ">> /dev/null") || Die("open(/dev/null): $!\n");
open( REPORT, ">& LOG") || Die("dup(LOG, REPORT): $!\n");
Log( 1, "$progname: $cache_dir: no such directory.\n" .
" sam.{log,report,html.json} will not be written.\n\n");
}
# ---------------------------------------------------------------------------
Log(0, "%%T: MS00: started %s\n", $invocation_cmd_line);
Log(0, "%%T: MS01: Find metadata and set up GPG\n");
#
# read available configuration and repository infos
#
# get (configured) locations of repository files
get_repo_conf($root_dir);
# get infos about all repositories
get_repo_infos();
if (not $CHECK_SIGNATURE) {
Log(0, " rpm header signature check disabled: will not check package authenticity\n");
} elsif (not -x $gpg_exe) {
Log(0, " GPG executable \"$gpg_exe\" not found: will not check package authenticity\n");
} else {
# setup keyring
find(\&setup_key, $metadatadir);
if (scalar (keys %good_key_ids) > 0) {
Log(0, " found %d SUSE/Novell build keys: will check package authenticity\n",
scalar (keys %good_key_ids));
} else {
Log(0, " no SUSE/Novell build keys found: cannot check package authenticity\n");
}
}
# ---------------------------------------------------------------------------
# Enumerate packages
#
my $max_pack_name_length = 0;
my $max_filename_length = 0;
Log(0, "%%T: MS02: enumerate packages (1a)\n");
my $IN = rpm_qa($root_dir);
while ( <$IN> ) {
chomp();
if ( ! m{^(\S+) (\S+) (\S+) ([0-9]+) '(.*?)' (\S+) (\S+) (\S+)$} ) {
Log(0, " rpm: unexpected query response: '$_'\n");
next;
}
my ($name, $vers_rel, $arch, $inst_time, $vendor,
$rsaheadersig, $dsaheadersig, $rpmheader) =
($1, $2, $3, $4, $5, $6, $7, $8);
my $package_name = "$name-$vers_rel.$arch";
$vendor ||= "undef";
if ( $arch eq '(none)' and $package_name =~ m{^gpg-pubkey-} ) {
# "silently" drop verification keys
Log(5, " rpm: dropping: $package_name\n");
next;
}
# check for SUSE/Novell package
my $is_ours = is_our_package($rsaheadersig, $dsaheadersig, $rpmheader,
$package_name, $vendor);
if ( $arch eq '(none)' and $is_ours ) {
#if ( $vendor =~ m(^$Vendors)o ) {
# ignore SUSE/Novell packages without architecture info
Log(0, " rpm: ignoring: $package_name\n");
$skipped_packages{$package_name} = "$inst_time $vendor";
next;
}
#if ( $vendor !~ m(^$Vendors)o ) {
if ( not $is_ours ) {
# remember packages from other vendors
Log(0, " rpm: foreign vendor package: $package_name: $vendor\n");
$alien_packages{$package_name} = "$inst_time $vendor";
$alien2name{$package_name} = $name;
$alien2edition{$package_name} = "$vers_rel";
next;
}
$package2inst_time{$package_name} = $inst_time;
$package2name{$package_name} = $name;
$package2edition{$package_name} = "$vers_rel";
$max_pack_name_length = max($max_pack_name_length, length($package_name));
}
close($IN);
# ---------------------------------------------------------------------------
# Verify installed files against package headers:
# first "our" packages, then "foreign" packages
#
my $num_packages = scalar(keys(%package2inst_time));
my $num_foreign = scalar(keys(%alien_packages));
my %inodes;
my %file2rpm;
my %file2kind;
my %file2type;
my %file2res;
my %unsatisfied;
my %foreign_unsatisfied;
my %depends_on_alien;
my @file_modified;
my @file_tolerated;
my @file_missing;
my @file_dispensable;
my @foreign_file_modified;
my @foreign_file_tolerated;
my @foreign_file_missing;
my @foreign_file_dispensable;
my %unsupportable;
my %tolerable;
my %harmless;
my %ok;
my $is_ours = 0;
# have we seen this filename already?
# -> handle duplicates
#
sub check_and_log_duplicate_file ($$$$$$) {
my ($logBuffer_ref, $rpm, $file, $kind, $vrfy_result, $error) = @_;
if ($ALLOW_MULTIHOMED_OBJECTS) {
# if multihomed files are allowed, all the packages for such a file are
# remembered
push @{ $file2rpm{$file} }, $rpm;
if ( exists( $file2kind{$file}) && $file2kind{$file} ne $kind ) {
Die( "$file: conflicting attributes: $file2kind{$file} <> $kind\n");
} else {
$file2kind{$file} = $kind;
#Log( 0, "$file\n") if $kind eq "d" && $vrfy_result eq "missing ";
}
} else {
# if multihomed files are not allowed, log entries will be generated
if ( exists($file2rpm{$file}) ) {
# file is multihomed
$_ = $file2res{$file};
$$logBuffer_ref = " "x4 . "$file: duplicate $file2rpm{$file}\n";
if ( m{^[UTH]:miss} ) {
# a duplicate of a missing object hardly makes it worse...
$$logBuffer_ref .= " "x6 ."dupe: miss\n";
$file2res{$file} .= " && U:dup :$rpm";
push @{ $unsupportable{$rpm} }, $file;
} elsif ( m{^U:} ) {
if ( $file2type{$file} eq "dir" ) {
# packaging directories multiple times is OK...
$$logBuffer_ref .= " "x6 ."dup: U: dir => H\n";
$file2res{$file} .= " && H:dupe:$rpm";
} else {
# ...but not other objects
$$logBuffer_ref .= " "x6 ."dup: U: !dir => U\n";
$file2res{$file} .= " && U:dupe:$rpm";
push @{ $unsupportable{$rpm} }, $file;
}
} elsif ( $file2type{$file} eq "dir" ) {
# again, packaging directories multiple times is OK...
$$logBuffer_ref .= " "x6 ."dup: H: dir => H\n";
$file2res{$file} .= "H:dupe: $rpm";
} else {
$_ = assess($rpm, $file, $kind, $vrfy_result, $error);
if ( ! m{^U:} ) {
# ...if it verifies OK, only note
$$logBuffer_ref .= " "x6 ."dup: H: same => H\n";
$file2res{$file} .= "H:dupe: $rpm";
} else {
# ...otherwise promote to "Unsupportable"
$$logBuffer_ref .= " "x6 ."dup: H: !dir => U\n";
$file2res{$file} = "$_ && U:dupe: $rpm && $file2res{$file}";
if ( ! exists $unsupportable{$file2rpm{$file}} ) {
push @{ $unsupportable{$file2rpm{$file}} }, $file;
}
# FIXME: why push $file twice on @{$unsupportable{$file2rpm{$file}}}
# if it does not exist? (see above and below): the above
# should be unnecessary
push @{ $unsupportable{$file2rpm{$file}} }, $file;
push @{ $unsupportable{$rpm} }, $file;
}
}
return 1;
}
# not multihomed: record package for this file
$file2rpm{$file} = $rpm;
$file2kind{$file} = $kind;
}
return 0;
}
sub evaluate_supportability_and_record_results ($$$$$) {
my ($rpm, $file, $kind, $vrfy_result, $error) = @_;
# is the result for the file a supportability problem? classify...
$_ = assess($rpm, $file, $kind, $vrfy_result, $error);
#$packinfo{$rpm}->{file}->{$file}->{res} = $_;
$file2res{$file} = $_;
if ( m{^U:miss} ) {
if ($is_ours) {
push @file_missing, $file;
push @{ $unsupportable{$rpm} }, $file;
} else {
push @foreign_file_missing, $file;
}
return;
} elsif ( m{^T:miss} ) {
if ($is_ours) {
push @file_dispensable, $file;
push @{ $tolerable{$rpm} }, $file;
} else {
push @foreign_file_dispensable, $file;
}
return;
} elsif ( m{^H:miss} ) {
if ($is_ours) {
push @file_dispensable, $file;
push @{ $harmless{$rpm} }, $file;
} else {
push @foreign_file_dispensable, $file;
}
return;
} elsif ( m{^U} ) {
if ($is_ours) {
push @file_modified, $file;
push @{ $unsupportable{$rpm} }, $file;
} else {
push @foreign_file_modified, $file;
}
} elsif ( m{^T} ) {
if ($is_ours) {
push @file_tolerated, $file;
push @{ $tolerable{$rpm} }, $file;
} else {
push @foreign_file_tolerated, $file;
}
} elsif ( m{^H} ) {
if ($is_ours) {
push @{ $harmless{$rpm} }, $file;
}
} elsif ( m{^O} ) {
if ($is_ours) {
push @{ $ok{$rpm} }, $file;
}
} else {
Die("$progname: internal error. $_\n");
}
# when we see the file for the first time, add to total size of all files,
# increase number of different files and remember type of file (file, dir,
# link, special)
my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
$atime,$mtime,$ctime,$blksize,$blocks) =
lstat( $root_dir . $file);
if ( -f _ ) {
$file2type{$file} = "file";
if (! exists $inodes{"$dev:$ino"}) {
$pSize += ($size + 512) / 1024;
}
} elsif ( -d _ ) {
$file2type{$file} = "dir";
} elsif ( -l _ ) {
$file2type{$file} = "link";
} else {
$file2type{$file} = "special";
}
if (! exists $inodes{"$dev:$ino"}) {
$inodes{"$dev:$ino"} = 1;
$pInodes++;
}
}
sub verify_packages ($$$) {
my ($packhash_ref, $unsatisfied_ref, $msg) = (@_);
foreach my $rpm ( sort(keys(%$packhash_ref)) ) {
$logBuffer = "";
Log(1, " %%T: $msg: $rpm\n");
$IN = rpm_V($root_dir, $rpm);
while ( <$IN> ) {
chomp();
if (m{^([S.][M.][5?.][D.][L.][U.][G.][T.]|missing ) ([cdglr ]) (\S.+)$}) {
# note: rpm(8) calls the file kind (%config, %doc, ...) an
# "attribute" of the file
my ($vrfy_result, $kind, $file, $error) = ($1, $2, $3, undef);
if ($vrfy_result =~ /missing/ and $file =~ /^(\S.+) \(([^\(\)]*)\)$/) {
$file = $1;
$error = $2;
}
$max_filename_length = max($max_filename_length, length($file));
next if check_and_log_duplicate_file(\$logBuffer, $rpm, $file,
$kind, $vrfy_result, $error);
evaluate_supportability_and_record_results($rpm, $file, $kind,
$vrfy_result, $error);
} elsif ( m{^Unsatisfied dependencies for ([^:]+)\: (\S.+)$} ) {
my ($pkg, $deps) = ($1, $2);
$$unsatisfied_ref{$pkg} = $deps;
} else {
Die( "$progname: rpm: unexpected query response:\n $_\n");
}
}
close( $IN);
Log(3, $logBuffer) if $logBuffer;
}
}
Log(0, "%%T: MS03: verify");
if (not $VERIFY_PACKAGES) {
Log(0, "%d packages: skipped\n", $num_packages);
} elsif ( $num_packages == 0 ) {
Log(0, "%%T: Panic: no packages to verify!?\n");
Die(-1);
} else {
Log(0, " %d packages\n", $num_packages);
# first, verify "our" packages
$is_ours = 1;
verify_packages(\%package2inst_time, \%unsatisfied, "verify");
# then verify "foreign" packages
$is_ours = 0;
verify_packages(\%alien_packages, \%foreign_unsatisfied, "verify foreign");
}
# ---------------------------------------------------------------------------
Log(0, "%%T: MS04: find packages that depend on foreign packages\n");
foreach my $rpm ( sort(keys(%alien_packages)) ) {
Log(1, " find dependents on: $rpm\n");
$IN = rpm_e($root_dir, $rpm);
while ( <$IN> ) {
chomp();
if (m{^error: Failed dependencies:$}) {
Log(4, " rpm: found dependents on: $rpm\n");
} elsif (m{^\s+(.*)\s+is needed by(?: \(installed\))? (\S+)$}) {
my ($provided, $dependant_pack) = ($1, $2);
# remember if the dependant package is one of ours
if (exists $package2inst_time{$dependant_pack}) {
push @{$depends_on_alien{$dependant_pack}->{$rpm}}, $provided;
Log(4, " rpm: our package $dependant_pack needs $provided from $rpm\n");
}
} else {
Die("$progname: rpm: unexpected query response:\n $_\n");
}
}
close( $IN);
}
# ---------------------------------------------------------------------------
# Find SUSE/Novell source repos for installed packages, compare versions
#
my %newer_exists = ();
Log(0, "%%T: MS05: Identify package sources (2i)");
if ($satsolver == 0) {
Log(0, ": skipped: no satsolver\n");
} elsif ($satsolver == -1) {
Log(0, ": skipped: no usable satsolver (missing functions)\n");
} elsif ((scalar keys %repoinfo) == 0) {
Log(0, ": skipped: no repositories\n");
} else {
Log(0, "\n");
# create pool
my $pool = new satsolver::Pool;
# set architecture: only compatible packages are considered
my $sysarch = `uname -m` || Die("uname -m\n");
chomp $sysarch;
$pool->set_arch($sysarch);
# create repo with RPM database
my $installed = $pool->create_repo('installed') || Die("create_repo('installed')\n");
$installed->add_rpmdb("/");
# create a repo each for SUSE/Novell installation sources
my @repos = ();
foreach my $subdir (keys %repoinfo) {
my $name = $repoinfo{$subdir}->{'name'};
my $label = $repoinfo{$subdir}->{'label'};
next if $label !~ m($Labels)o;
Log(0, " using repository %s\n", $name);
push @repos, $pool->create_repo($subdir) || Die("create_repo($subdir)\n");
$repos[$#repos]->add_solv($repoinfo{$subdir}->{'solvfile'});
}
# create dependencies to provides table
$pool->prepare();
# find providers for each installed package
foreach my $inst_solvable ($installed->solvables()) {
Die "inst_solvable not defined\n" if not defined $inst_solvable;
my $inst_solvname = $inst_solvable->name();
my $inst_solvevr = $inst_solvable->evr();
my $inst_solvstring = $inst_solvable->string();
my %found = ();
foreach my $solvable ($pool->providers($inst_solvable->name())) {
next if (not defined $solvable);
my $subdir = $solvable->repo()->name();
# do not use matches on the 'installed' repo
next if $subdir eq "installed";
my $reponame = $repoinfo{$subdir}->{name};
my $reponumber = $repoinfo{$subdir}->{number};
# identical package? (name, arch, evr, vendor, build time,
# requires, ...)
if ($solvable->identical($inst_solvable)) {
Log(3, " = %-69s (repo has same package: %s)\n",
$inst_solvstring, $reponame);
$found{identical}++;
} else {
# find out if the repository provides an older or newer package
my $result = $solvable->compare($inst_solvable);
my $solv_evr = $solvable->evr();
if ($result < 0) {
Log(2, " + %-35s %-15s > %-15s (repo has older evr: %s)\n",
$inst_solvname, $inst_solvevr, $solv_evr, $reponame);
$found{older}++;
} elsif ($result > 0) {
Log(1, " - %-35s %-15s < %-15s (repo has newer evr: %s)\n",
$inst_solvname, $inst_solvevr, $solv_evr, $reponame);
$found{newer}++;
push @{$newer_exists{$inst_solvstring}}, "repo #$reponumber: $solv_evr";
} else {
# identical evr, different package: strange
# (manual rebuild installed or in repo?)
Log(1, " ! %-35s %-15s ~ %-15s (repo has same evr: %s)\n",
$inst_solvname, $inst_solvevr, $solv_evr, $reponame);
$found{similar}++;
}
}
}
if ((scalar(keys %found)) == 0) {
Log(0, " m %-69s (no package with this name exists in SUSE/Novell repos)\n",
$inst_solvname);
} elsif (not exists $found{identical}) {
Log(0, " ? %-69s (not found in SUSE/Novell repos)\n",
$inst_solvstring);
}
}
}
# ---------------------------------------------------------------------------
Log( 0, "%%T: MS06: find 'detached' files (1e)");
if ( not $FIND_DETACHED ) {
Log( 0, ": skipped\n");
shift @orphans;
$aSize = $aInodes = -1;
} else {
Log( 0, "\n");
findOrphans($root_dir);
}
# ---------------------------------------------------------------------------
Log( 0, "%%T: MS90: Reporting Results...\n");
# ===========================================================================
Report("*** Report by %s\n", $invocation_cmd_line);
Report("*** Considered Packages (1a): %d (sig ok: %s), foreign: %d (excluded: %d)\n",
$num_packages, $CHECK_SIGNATURE ? $num_sig_ok_packs : "OFF",
$num_foreign, scalar( keys( %skipped_packages)));
my $unsupportable = scalar( keys( %unsupportable));
my $tolerable = scalar( keys( %tolerable));
Report("*** Packages with Verification Anomalies (1b): ");
if (not $VERIFY_PACKAGES) {
Report( "NOT CHECKED.\n");
} elsif ( $unsupportable + $tolerable > 0 ) {
Report( "%d (+%d minor)\n", $unsupportable, $tolerable);
} else {
Report( "NONE.\n");
}
my $unsat = scalar( keys( %unsatisfied));
Report("*** Violated Package Dependencies (1c): ");
if (not $VERIFY_PACKAGES) {
Report( "NOT CHECKED.\n");
} elsif ( $unsat > 0 ) {
Report("%d\n", $unsat);
foreach my $package ( sort( keys( %unsatisfied)) ) {
Report(" %s\n %s\n", $package, $unsatisfied{$package});
}
} else {
Report("NONE.\n");
}
my $aliendeps = scalar( keys( %depends_on_alien));
Report("*** Packages Depending on Foreign Packages (2l): ");
if ( $aliendeps > 0 ) {
Report("%d\n", $aliendeps);
foreach my $package ( sort( keys( %depends_on_alien)) ) {
Report( " %s\n %s\n", $package, join (" ", sort( keys( %{$depends_on_alien{$package}}))) );
}
} else {
Report("NONE.\n");
}
if ( $unsupportable + $unsat + $aliendeps > 0 ) {
$Unsupportable = 1;
}
# ---------------------------------------------------------------------------
Report("*** Found Package Repositories (2h): ");
my $num_repos = (keys %repoinfo);
if ( $num_repos == 0 ) {
Report("NONE.\n");
} else {
Report("%d\n", $num_repos);
foreach my $subdir (sort (keys %repoinfo)) {
my $info_ref = $repoinfo{$subdir};
Report(" Repository #%d:\n", $info_ref->{number});
Report(" %s\n",
join("\n ", map(sprintf("%-12s %s", "$_:", $info_ref->{$_}),
(sort (keys %$info_ref)))));
}
}
# ---------------------------------------------------------------------------
Report("*** Package Version Checks (2i): ");
my $num_old_packages = (keys %newer_exists);
if ($satsolver <= 0) {
Report("NOT CHECKED.\n");
} elsif ( $num_old_packages > 0 ) {
Report("updates available for %d packages\n", $num_old_packages);
foreach my $pack (sort(keys(%newer_exists))) {
Report(" %-55s %s\n", "$pack:", join(", ", @{$newer_exists{$pack}}));
}
} else {
Report("NONE.\n");
}
# ---------------------------------------------------------------------------
Report("*** Modified filesystem objects (1d): ");
if (not $VERIFY_PACKAGES) {
Report("NOT CHECKED.\n");
} elsif ( ($#file_modified + $#file_tolerated) > 0 ) {
Report("%d (+%d ignored)\n", $#file_modified + 1, $#file_tolerated + 1);
foreach my $pack ( sort( keys(%unsupportable)) ) {
Report(" %s\n", $pack);
my $num_files = 0;
foreach my $pfile ( @{ $unsupportable{$pack} } ) {
my $res = prettyR($file2res{$pfile});
# if ( length($pfile) + length($res) > 72 ) {
# ReportQ(" "x4 ."%s\n" ." "x6 ."%s\n", $pfile, $res);
# } else {
## ReportQ(" "x4 ."%s %s\n", $pfile, $res);
# ReportQ(" "x4 ."%-*s %s\n", 55 - max((72 - 55), length($res)), $pfile, $res);
# }
ReportQ(" "x4 ."%-55s %s\n", $pfile, $res);
Log(9, " "x6 ."%s %s\n", $file2kind{$pfile}, $file2res{$pfile});
if ( $num_files++ >= 10 ) {
ReportQ(" "x4 ."and %d more\n", $#{ $unsupportable{$pack} } - 10);
last;
}
}
}
} else {
Report("NONE.\n");
}
# ---------------------------------------------------------------------------
Report("*** Accrued filesystem objects (1e): ");
if (not $FIND_DETACHED) {
Report( "NOT CHECKED.\n");
} elsif ( $#orphans == 0 ) {
Report("* s k i p p e d *.\n");
} elsif ( $#orphans >= 0 ) {
Report("%d (+%d ignored)\n", $#orphans, 0);
shift @orphans;
Log(1, " %s\n", join( "\n ", @orphans));
} else {
Report("NONE.\n");
}
Report("*** Preliminary Conclusion: %ssupportable.\n",
(($Unsupportable) ? "NOT " : ""));
Report(" %d (%.2f%%) OK, %d (%.2f%%) dubious, %d (%.2f%%) fail\n",
($num_packages - $unsupportable - $unsat - $aliendeps),
($num_packages - $unsupportable - $unsat - $aliendeps) * 100.0 / $num_packages,
$unsupportable,
$unsupportable * 100.0 / $num_packages,
($unsat + $aliendeps),
($unsat + $aliendeps) * 100.0 / $num_packages);
if ($Unsupportable) {
Report(" This is a preliminary interpretation.\n");
Report(" A detailed analysis of the report or the log file\n");
Report(" may show that this system is supportable.\n");
}
# ===========================================================================
if ( $writeReport ) {
my ($user, $system, $cuser, $csystem) = times();
my $Telapsed = time() - $Tstart;
Log( 1, "%%T: MS92: Generate reports...\n");
initHTML();
foreach my $p ( sort( keys( %package2inst_time)) ) {
my ($r, $n, $t) = ( 0, "", "");
$t = $unsatisfied{$p};
if (defined( $t) ) {
$r = 1;
$n .= "unsatisfied dependencies: $t";
}
if (defined( @{ $unsupportable{$p} }) ) {
$r = 1;
$n .= "; " if ( $n );
$t = $#{ $unsupportable{$p} } + 1;
$n .= sprintf( "%d object%s modified", $t,
($t != 1) ? "s were" : " was");
}
printHTML($p, $r, $n);
fillJSON($p, $r, $n);
}
finishHTML();
finishJSON();
Log(1, "%%T: MS95: Log statistics...\n");
Log(1, " Filesystem: %s, %d inodes over all\n",
prettyK($aSize), $aInodes);
Log(1, " rpmDB: %s, %d inodes from %d packages (hit %d, miss %d)\n",
prettyK($pSize), $pInodes, scalar( keys( %package2inst_time)),
$cacheHit, $cacheMiss);
Log(1, " Runtime (in secs): %.2f user, %.2f system, %.2f elapsed\n",
$user, $system, $Telapsed);
open( IN, "< /proc/self/status")|| Die( "open(status): $!\n");
Log(1, " Memory: ");
while ( <IN> ) {
next unless ( m{^(Vm(Size|RSS|Data))} );
chomp; s{\s+}{}g; s{\:}{=}g;
Log(1, " %s", $_);
}
Log(1, "\n");
close( IN);
}
exit($Unsupportable);