File purge-kernels of Package dracut.12460
#!/usr/bin/perl
use strict;
use warnings;
use Getopt::Long;
sub usage {
print "Usage: $0 [--test]\n";
print "Reads list of kernels to keep from /etc/zypp/zypp.conf:multiversion.kernels\n";
print "kernels can be given as <version>, latest(-N), running, oldest(+N).\n";
}
# arch/flavor => version-release => [ subpackages ]
my %kernels;
my @keep_spec;
my ($want_running, $running_version, $running_flavor);
# do not actually delete anything
my $test_only;
# undocumented debugging options
my ($fake_config, $fake_rpm_qa, $fake_uname_r, $fake_uname_m);
sub get_config_line {
my $file = "/etc/zypp/zypp.conf";
if ($fake_config) {
return $fake_config;
}
if (!-e $file) {
print STDERR "$0: /etc/zypp/zypp.conf does not exist, exiting.\n";
exit 0;
}
open(my $fh, '<', $file) or die "$0: $file: $!\n";
while (<$fh>) {
chomp;
next unless /^\s*multiversion\.kernels\b/;
s/^[^=]*=\s*//;
close($fh);
return $_;
}
close($fh);
return "";
}
sub load_config {
my @kernels;
@kernels = split(/,\s*/, get_config_line());
for my $kernel (@kernels) {
if ($kernel =~ /^\s*(latest|oldest|running)(\s*[-+]\s*\d+)?\s*$/) {
my $new = { whence => $1, offset => $2 || 0 };
$new->{offset} =~ s/\s*//g;
if ($new->{whence} eq "running") {
$want_running = 1;
}
push (@keep_spec, $new);
} elsif ($kernel =~ /^\d+\.\d+/) {
my $new = { version => $kernel };
push (@keep_spec, $new);
} elsif ($kernel =~ /^\s*$/) {
next;
} else {
print STDERR "$0: Ignoring unknow kernel specification in\n";
print STDERR "/etc/zypp/zypp.conf:multiversion.kernels: $kernel\n";
}
}
}
sub add_package {
my ($name, $vr, $arch) = @_;
(my $flavor = $name) =~ s/^kernel-//;
#print STDERR "add_package: $name $vr $arch\n";
if ($name eq "kernel-firmware" || $name eq "kernel-coverage") {
return;
}
# Put all subpackages into the same group, except for
# kernel-source-{vanilla,rt}, which are packages on their own
if ($flavor !~ /^source/) {
$flavor =~ s/-.*//; # XXX: No dashes in flavor names
}
# kernel-devel is a subpackage of kernel-source
$flavor =~ s/^devel/source/;
$kernels{"$arch/$flavor"} ||= {};
$kernels{"$arch/$flavor"}{$vr} ||= [];
push(@{$kernels{"$arch/$flavor"}{$vr}}, "$name-$vr.$arch");
}
sub load_packages {
my $pipe;
if ($fake_rpm_qa) {
open($pipe, '<', $fake_rpm_qa) or die "$fake_rpm_qa: $!\n";
} else {
open($pipe, '-|', 'rpm', '-qa', '--qf',
'%{n} %{v}-%{r} %{arch}\n', 'kernel-*') or die "rpm: $!\n";
}
while (<$pipe>) {
chomp;
my ($name, $vr, $arch) = split;
add_package($name, $vr, $arch);
}
close($pipe)
}
sub sort_versions {
my @versions = @_;
pipe (my $read, my $write);
my $pid = fork();
if (!defined($pid)) {
die "Cannot fork: $!\n";
} elsif ($pid == 0) {
# child
close($read);
open STDOUT, '>&', $write;
open(my $fh, '|-', "/usr/lib/rpm/rpmsort") or die "/usr/lib/rpm/rpmsort: $!\n";
print $fh join("\n", @versions), "\n";
close($fh);
die "rpmsort failed ($?)\n" if $? != 0;
exit 0;
}
# parent
close($write);
@versions = <$read>;
chomp @versions;
close($read);
waitpid($pid, 0);
die "rpmsort failed ($?)\n" if $? != 0;
return @versions;
}
# return true if VER1 == VER2 or VER1 == (VER2 minus rebuild counter)
sub version_match {
my ($ver1, $ver2) = @_;
return 1 if $ver1 eq $ver2;
# copied from kernel-source/rpm/kernel-spec-macros
$ver2 =~ s/\.[0-9]+($|\.[^.]*[^.0-9][^.]*$)/$1/;
return $ver1 eq $ver2;
}
sub list_old_versions {
my ($flavor) = @_;
my $is_source = $flavor =~ /\/(source|syms)/;
my $kernels = $kernels{$flavor};
my @versions = sort_versions(keys(%$kernels));
my %idx = (
oldest => 0,
latest => scalar(@versions) - 1,
);
if ($want_running && ($running_flavor eq $flavor || $is_source)) {
for (my $i = scalar(@versions) - 1; $i >= 0; $i--) {
if (version_match($running_version, $versions[$i])) {
$idx{running} = $i;
last;
}
}
if (!exists($idx{running}) && !$is_source) {
print STDERR "$0: Running kernel $running_version-$running_flavor not installed.\n";
print "NOT removing any packages for flavor $flavor.\n";
return;
}
}
my %delete = map { $_ => 1 } @versions;
for my $keep (@keep_spec) {
if ($keep->{version}) {
for my $ver (@versions) {
if (version_match($keep->{version}, $ver)) {
$delete{$ver} = 0;
}
}
} elsif ($keep->{whence}) {
next unless exists($idx{$keep->{whence}});
my $idx = $idx{$keep->{whence}};
$idx += $keep->{offset};
next unless $idx >= 0 && $idx < scalar(@versions);
$delete{$versions[$idx]} = 0;
} else {
die "??";
}
}
return grep { $delete{$_} } @versions;
}
sub package_exists {
my ($version, $archs, $flavors) = @_;
for my $arch (@$archs) {
for my $flavor (@$flavors) {
my $config = "$arch/$flavor";
if (exists($kernels{$config})
&& exists($kernels{$config}->{$version})) {
return 1;
}
}
}
return 0;
}
sub list_old_packages {
my (@packages, @archs, @flavors);
my (@syms_flavors, @binary_flavors, @source_configs);
# there are some inter-dependencies among the kernel packages,
# so we have to be careful
my %t = map { s:/.*::; $_ => 1 } keys(%kernels);
@archs = sort(keys(%t));
%t = map { s:.*/::; $_ => 1 } keys(%kernels);
@flavors = sort(keys(%t));
@syms_flavors = grep { /^syms/ } @flavors;
@binary_flavors = grep { !/^(source|syms)/ } @flavors;
@source_configs = grep { /\/source/ } sort(keys(%kernels));
for my $arch (@archs) {
for my $flavor (@syms_flavors) {
my $config = "$arch/$flavor";
next unless exists($kernels{$config});
my @versions = list_old_versions($config);
for my $ver (@versions) {
push(@packages, @{$kernels{$config}->{$ver}});
delete($kernels{$config}->{$ver});
}
}
for my $flavor (@binary_flavors) {
my $config = "$arch/$flavor";
next unless exists($kernels{$config});
my @versions = list_old_versions($config);
for my $ver (@versions) {
my @pacs = @{$kernels{$config}->{$ver}};
my $remove_all = 1;
# do not remove kernel-$flavor-devel-$ver
# if kernel-syms-$ver still exists
if (grep { /-devel$/ } @pacs) {
my $syms = "syms";
if ($flavor =~ /^rt/) {
$syms = "syms-rt";
}
if (exists($kernels{$syms}->{$ver})) {
$remove_all = 0;
@pacs = grep { !/-devel$/ }
@pacs;
}
}
push(@packages, @pacs);
if ($remove_all) {
delete($kernels{$config}->{$ver});
}
}
}
}
for my $config (@source_configs) {
my @versions = list_old_versions($config);
for my $ver (@versions) {
# Remove kernel-{devel,source} only if no other package
# of the same version exists
next if package_exists($ver, \@archs, \@binary_flavors);
push(@packages, @{$kernels{$config}->{$ver}});
}
}
return @packages;
}
sub find_package {
my $name = shift @_;
my $version = shift @_;
my @packages = @_;
my $expr = "^" . quotemeta($version ? "$name-$version" : $name);
my @found = grep { $_ =~ $expr } @packages;
return @found if @found;
$expr = "^" . quotemeta($name) . " = " . quotemeta($version) . "\$";
@found = grep {
my @provides = qx/rpm -q --provides $_/;
chomp (@provides);
grep { $_ =~ $expr} @provides;
} @packages;
return @found;
}
# Try to remove a list of packages.
#
# If there is a KMP or livepatch depending on the package remove it as well. If
# there is another package depending on the kernel keep the kernel. If there is
# a package that depends on a KMP keep the KMP and a kernel required to use the
# KMP.
# In each step a KMP or livepatch may be added or a package which cannot be
# removed due to dependencies is marked as taboo and removed from the list.
#
# Finish when packages uninstall successfully or we can't find any packages to
# add or remove from the list to make it uninstallable.
sub remove_packages {
my @packages = @_;
my %taboo_packages;
while (1) {
pipe(my $read, my $write);
my $pid = fork();
if (!defined($pid)) {
die "Cannot fork: $!\n";
} elsif($pid == 0) {
# child
close($read);
open STDOUT, '>&', $write;
open STDERR, '>&', $write;
$ENV{LC_ALL} = "C";
my @cmd = qw(rpm -e);
push(@cmd, "--test") if $test_only;
exec(@cmd, @packages) or die "rpm: $!\n";
}
# parent
close($write);
my @out = <$read>;
chomp @out;
close($read);
waitpid($pid, 0);
if ($? == 0) {
print "Removed:\n ", join("\n ", @packages), "\n";
return 1;
}
my $retry = 0;
my %old_packages = map { $_ => 1 } @packages;
my %new_packages;
for (@out) {
if (/ is needed by \(installed\) (kernel-syms-.*|kgraft-patch-.*|kernel-livepatch-.*|.*-kmp-.*)/ &&
!$old_packages{$1} && !$taboo_packages{$1}) {
push(@packages, $1) unless $new_packages{$1};
$new_packages{$1} = 1;
$retry = 1;
} elsif (/([^ \t]*)(?: = ([^ \t]*))? is needed by \(installed\) /) {
my @unremovable = find_package($1, $2, @packages);
my $match = $unremovable[$#unremovable];
if ($match) {
print STDERR "$0: $_\n";
print STDERR "$0: Keeping " . ($2 ? "$1 = $2" : $1) . " ($match)\n";
@packages = grep { $_ !~ $match } @packages;
$taboo_packages{$match} = 1;
$retry = 1;
last; # Only remove one package providing the dependency from the list
}
}
}
if (!$retry) {
print STDERR join("\n", @out), "\n";
print STDERR "$0: giving up.\n";
return 0;
}
}
}
if (!GetOptions(
"h|help" => sub { usage(); exit; },
"--test" => \$test_only,
"--fake-config=s" => \$fake_config,
"--fake-rpm-qa=s" => \$fake_rpm_qa,
"--fake-uname-r=s" => \$fake_uname_r,
"--fake-uname-m=s" => \$fake_uname_m)) {
usage();
exit 1;
}
load_config();
if (!@keep_spec) {
print STDERR "$0: multiversion.kernels not configured in /etc/zypp/zypp.conf, exiting.\n";
exit 0;
}
load_packages();
if ($want_running) {
$running_version = $fake_uname_r ? $fake_uname_r : `uname -r`;
chomp($running_version);
($running_flavor = $running_version) =~ s/.*-//;
$running_version =~ s/-[^-]*$//;
(my $release = $running_version) =~ s/.*-//;
$running_version =~ s/-[^-]*$//;
# copied from kernel-source/rpm/mkspec
$running_version =~ s/\.0-rc/.rc/;
$running_version =~ s/-rc\d+//;
$running_version =~ s/-/./g;
$running_version .= "-$release";
my $arch = $fake_uname_m ? $fake_uname_m : `uname -m`;
chomp($arch);
$arch =~ s/^i.86$/i586/;
$running_flavor = "$arch/$running_flavor";
}
my @remove = list_old_packages();
if (!@remove) {
print STDERR "$0: Nothing to do.\n";
exit 0;
}
if (remove_packages(@remove)) {
exit 0;
}
exit 1;