mirror of
https://github.com/webmin/webmin.git
synced 2026-09-16 02:20:39 +01:00
This PR adds DNF 4 and DNF 5 versionlock support to Package Updates, including listing, holding, unholding, and explicitly updating held packages. DNF 4 requires the versionlock plugin to be installed, while DNF 5 supports versionlock natively. Hold controls are shown only when the versionlock command is available. It also previews DNF transactions before confirmation so users can see which packages will be installed or updated in the table, similar to APT.
348 lines
11 KiB
Perl
348 lines
11 KiB
Perl
#!/usr/local/bin/perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Test::More;
|
|
use File::Basename qw(dirname);
|
|
use File::Spec;
|
|
use Cwd qw(abs_path);
|
|
|
|
our (%config, %packages, %text);
|
|
our ($yum_command, $supports_dnf_versionlock, $dnf_version);
|
|
|
|
sub has_command
|
|
{
|
|
return $_[0] eq 'dnf' ? '/usr/bin/dnf' : undef;
|
|
}
|
|
|
|
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..'));
|
|
chdir($root) or die "chdir($root): $!";
|
|
|
|
do './software/yum-lib.pl' or die $@ || $!;
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
local *clean_language = sub { };
|
|
local *reset_environment = sub { };
|
|
local *backquote_command = sub {
|
|
return $_[0] =~ /--version/ ? "4.14.0\n" :
|
|
" versionlock control package version locks\n";
|
|
};
|
|
local $dnf_version;
|
|
local $supports_dnf_versionlock;
|
|
is(get_dnf_version(), 4, 'detects DNF 4');
|
|
ok(supports_update_system_holds(),
|
|
'detects the DNF 4 versionlock command');
|
|
is(update_system_hold_flags(), '--disableplugin=versionlock',
|
|
'uses the DNF 4 held-update discovery option');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
local *clean_language = sub { };
|
|
local *reset_environment = sub { };
|
|
local *backquote_command = sub {
|
|
return $_[0] =~ /--version/ ? "dnf5 version 5.4.2.1\n" :
|
|
" versionlock Manage versionlock configuration\n";
|
|
};
|
|
local $dnf_version;
|
|
local $supports_dnf_versionlock;
|
|
is(get_dnf_version(), 5, 'detects DNF 5');
|
|
ok(supports_update_system_holds(),
|
|
'detects the DNF 5 versionlock command');
|
|
is(update_system_hold_flags(), '--setopt=disable_excludes=*',
|
|
'uses the DNF 5 held-update discovery option');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
local *clean_language = sub { };
|
|
local *reset_environment = sub { };
|
|
local *backquote_command = sub {
|
|
return $_[0] =~ /--version/ ? "4.14.0\n" :
|
|
"No such command: versionlock\n";
|
|
};
|
|
local $dnf_version;
|
|
local $supports_dnf_versionlock;
|
|
ok(!supports_update_system_holds(),
|
|
'hides holds when DNF does not expose versionlock');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
my $output =
|
|
"bash-0:5.1.8-8.el9.*\n".
|
|
"python3-*\n".
|
|
"0:python3-pip-21.2.3-8.el9.*\n".
|
|
"!blocked-0:2.0-1.el9.*\n";
|
|
local *supports_update_system_holds = sub { return 1; };
|
|
local *get_dnf_version = sub { return 4; };
|
|
local *clean_language = sub { };
|
|
local *reset_environment = sub { };
|
|
local *open_execute_command = sub {
|
|
my ($fh) = @_;
|
|
no strict 'refs';
|
|
open(ref($fh) ? $fh : \*{$fh}, '<', \$output)
|
|
or die "open simulated DNF 4 locks: $!";
|
|
};
|
|
|
|
is_deeply([ list_update_system_holds() ], [ qw(bash python3-pip) ],
|
|
'lists DNF 4 exact locks without raw patterns or excludes');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
my $output =
|
|
"# Added by 'dnf versionlock add nano'\n".
|
|
"Package name: nano\n".
|
|
"evr = 8.7.1-2.fc44\n\n".
|
|
"Package name: coreutils\n".
|
|
"evr != 9.10-4.fc44\n\n".
|
|
"Package name: bash\n".
|
|
"evr = 5.3.0-2.fc44\n".
|
|
"arch = aarch64\n\n".
|
|
"Package name: python3-*\n".
|
|
"evr = 3.14.0-1.fc44\n\n".
|
|
"Package name: nano\n".
|
|
"evr > 9\n";
|
|
local *supports_update_system_holds = sub { return 1; };
|
|
local *get_dnf_version = sub { return 5; };
|
|
local *clean_language = sub { };
|
|
local *reset_environment = sub { };
|
|
local *open_execute_command = sub {
|
|
my ($fh) = @_;
|
|
no strict 'refs';
|
|
open(ref($fh) ? $fh : \*{$fh}, '<', \$output)
|
|
or die "open simulated DNF 5 locks: $!";
|
|
};
|
|
|
|
is_deeply([ list_update_system_holds() ], [ ],
|
|
'skips DNF 5 names with duplicate, custom or glob lock rules');
|
|
|
|
$output = "Package name: nano\nevr = 8.7.1-2.fc44\n";
|
|
is_deeply([ list_update_system_holds() ], [ 'nano' ],
|
|
'lists a simple DNF 5 exact lock by package name');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
is(update_system_hold_spec_name('bash-0:5.1.8-8.el9.*'), 'bash',
|
|
'decodes DNF 4 name-epoch-version entries');
|
|
is(update_system_hold_spec_name('0:python3-pip-21.2.3-8.el9.*'),
|
|
'python3-pip', 'decodes legacy DNF 4 epoch-name entries');
|
|
is(update_system_hold_spec_name('python3-*'), undef,
|
|
'does not treat a raw pattern as an exact lock');
|
|
is(update_system_hold_spec_name('!bash-0:5.1.8-8.el9.*'), undef,
|
|
'does not treat an exclude as an exact lock');
|
|
is(update_system_nevra_name('nano-0:8.7.1-2.fc44'), 'nano',
|
|
'extracts a DNF 5 package name from transaction output');
|
|
is(update_system_nevra_name(
|
|
'webmin-virtualmin-support-2:4.3.202602061237-1.noarch'),
|
|
'webmin-virtualmin-support', 'extracts a hyphenated NEVRA name');
|
|
is(update_system_nevra_name('nano-8.7.1-2.fc44.aarch64'), 'nano',
|
|
'extracts a package name when no epoch is printed');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
my @commands;
|
|
local *supports_update_system_holds = sub { return 1; };
|
|
local *unique = sub {
|
|
my %seen;
|
|
return grep { !$seen{$_}++ } @_;
|
|
};
|
|
local *clean_language = sub { };
|
|
local *reset_environment = sub { };
|
|
local *trim = sub {
|
|
my ($value) = @_;
|
|
$value =~ s/^\s+|\s+$//g;
|
|
return $value;
|
|
};
|
|
local *list_update_system_holds = sub { return ('bash'); };
|
|
local *execute_command_logged = sub {
|
|
my ($command, undef, $stdout) = @_;
|
|
push(@commands, $command);
|
|
$$stdout = '';
|
|
return 0;
|
|
};
|
|
|
|
is(update_system_hold([ 'bash', 'bash', 'coreutils' ], 1), undef,
|
|
'adds DNF version locks successfully');
|
|
is(update_system_hold([ 'bash', 'coreutils' ], 0), undef,
|
|
'removes only package names reported as exactly locked');
|
|
is_deeply(\@commands,
|
|
[ '/usr/bin/dnf -q versionlock add bash',
|
|
'/usr/bin/dnf -q versionlock add coreutils',
|
|
'/usr/bin/dnf -q versionlock delete bash' ],
|
|
'uses name-based add and delete commands on both DNF generations');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
my $dnf_output =
|
|
"[3/6] Upgrading bash-0:5.3.0-2.fc44 100% | 1.0 MiB/s | 1.0 MiB | 00m01s\n";
|
|
my $executed_command;
|
|
my @lock_actions;
|
|
local *update_system_hold_flags = sub { return 'held-update'; };
|
|
local *append_architectures = sub { return @_; };
|
|
local *package_info = sub { return ('bash'); };
|
|
local *update_system_repo = sub { return undef; };
|
|
local *additional_log = sub { };
|
|
local *html_escape = sub { return $_[0]; };
|
|
local *text = sub { return $_[0]; };
|
|
local *unique = sub {
|
|
my %seen;
|
|
return grep { !$seen{$_}++ } @_;
|
|
};
|
|
local *delete_update_system_holds = sub {
|
|
my ($packages, $removed) = @_;
|
|
push(@lock_actions, [ 'delete', [ @$packages ] ]);
|
|
push(@$removed, 'bash');
|
|
return undef;
|
|
};
|
|
local *restore_update_system_holds = sub {
|
|
my ($packages) = @_;
|
|
push(@lock_actions, [ 'restore', [ @$packages ] ]);
|
|
return undef;
|
|
};
|
|
local *open_execute_command = sub {
|
|
my ($fh, $command) = @_;
|
|
$executed_command = $command;
|
|
no strict 'refs';
|
|
open(ref($fh) ? $fh : \*{$fh}, '<', \$dnf_output)
|
|
or die "open simulated DNF install: $!";
|
|
};
|
|
|
|
my $printed = '';
|
|
open(my $stdout, '>', \$printed) or die "open captured stdout: $!";
|
|
local *STDOUT = $stdout;
|
|
$? = 0;
|
|
my @installed = update_system_install('bash', { }, 1, 'held-update');
|
|
is_deeply(\@installed, [ 'bash' ],
|
|
'returns a DNF 5 package updated while held');
|
|
unlike($executed_command, qr/held-update|disableplugin|disable_excludes/,
|
|
'runs the transaction without a global versionlock bypass');
|
|
is_deeply(\@lock_actions,
|
|
[ [ 'delete', [ 'bash' ] ], [ 'restore', [ 'bash' ] ] ],
|
|
'temporarily unlocks and then re-locks only the selected package');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
my $dnf_output =
|
|
"bash.aarch64 5.1.8-10.el9 baseos\n".
|
|
"coreutils.aarch64 8.32-40.el9 baseos\n";
|
|
my @commands;
|
|
local *supports_update_system_holds = sub { return 1; };
|
|
local *list_update_system_holds = sub { return ('bash'); };
|
|
local *set_yum_security_field = sub { };
|
|
local *get_dnf_version = sub { return 4; };
|
|
local *open_execute_command = sub {
|
|
my ($fh, $command) = @_;
|
|
push(@commands, $command);
|
|
no strict 'refs';
|
|
open(ref($fh) ? $fh : \*{$fh}, '<', \$dnf_output)
|
|
or die "open simulated DNF updates: $!";
|
|
};
|
|
|
|
my @normal = update_system_updates(0);
|
|
is_deeply([ map { $_->{'name'} } @normal ], [ 'coreutils' ],
|
|
'DNF 4 regular updates exclude held packages');
|
|
my @with_holds = update_system_updates(1);
|
|
is_deeply([ map { $_->{'name'} } @with_holds ],
|
|
[ 'bash', 'coreutils' ], 'DNF 4 held-update query includes locks');
|
|
like($commands[1], qr/--disableplugin=versionlock check-update/,
|
|
'DNF 4 uses its plugin bypass for held-update discovery');
|
|
|
|
@commands = ( );
|
|
local *get_dnf_version = sub { return 5; };
|
|
@with_holds = update_system_updates(1);
|
|
like($commands[0], qr/--setopt=disable_excludes=\\\* check-update/,
|
|
'DNF 5 disables excludes for held-update discovery');
|
|
ok($with_holds[0]->{'held'}, 'marks a DNF 5 locked update as held');
|
|
}
|
|
|
|
{
|
|
no warnings qw(once redefine);
|
|
my $dnf_output =
|
|
"Last metadata expiration check: 0:10:00 ago.\n".
|
|
"Dependencies resolved.\n".
|
|
"================================================================\n".
|
|
" Package Architecture Version Repository Size\n".
|
|
"================================================================\n".
|
|
"Upgrading:\n".
|
|
" tzdata noarch 2026c-1.fc44 updates 497 k\n".
|
|
" vim-minimal aarch64 2:9.1.2000-2.fc44 updates 647 k\n".
|
|
"Installing dependencies:\n".
|
|
" wbt-virtual-server-theme\n".
|
|
" noarch 21.20-1 virtualmin 2.5 M\n".
|
|
"Removing dependent packages:\n".
|
|
" oldpkg noarch 1.0-1 system 1 k\n".
|
|
"\nTransaction Summary:\n".
|
|
"Upgrade 2 Packages\n";
|
|
my $command;
|
|
my @lock_actions;
|
|
my $dnf_major = 4;
|
|
local *update_system_hold_flags = sub {
|
|
return '--setopt=disable_excludes=*';
|
|
};
|
|
local *get_dnf_version = sub { return $dnf_major; };
|
|
local *open_execute_command = sub {
|
|
my ($fh, $cmd) = @_;
|
|
$command = $cmd;
|
|
no strict 'refs';
|
|
open(ref($fh) ? $fh : \*{$fh}, '<', \$dnf_output)
|
|
or die "open simulated DNF transaction: $!";
|
|
};
|
|
local *clean_language = sub { };
|
|
local *reset_environment = sub { };
|
|
local *trim = sub {
|
|
my ($value) = @_;
|
|
$value =~ s/^\s+|\s+$//g;
|
|
return $value;
|
|
};
|
|
local *unique = sub {
|
|
my %seen;
|
|
return grep { !$seen{$_}++ } @_;
|
|
};
|
|
local *delete_update_system_holds = sub {
|
|
my ($packages, $removed) = @_;
|
|
push(@lock_actions, [ 'delete', [ @$packages ] ]);
|
|
push(@$removed, @$packages);
|
|
return undef;
|
|
};
|
|
local *restore_update_system_holds = sub {
|
|
my ($packages) = @_;
|
|
push(@lock_actions, [ 'restore', [ @$packages ] ]);
|
|
return undef;
|
|
};
|
|
|
|
my @ops = update_system_operations('tzdata vim-minimal');
|
|
like($command, qr{^/usr/bin/dnf --assumeno install },
|
|
'uses a simulated install for DNF 4 operations');
|
|
unlike($command, qr/disableplugin|disable_excludes/,
|
|
'keeps versionlock active for regular operations');
|
|
is_deeply([ map { $_->{'name'} } @ops ],
|
|
[ qw(tzdata vim-minimal wbt-virtual-server-theme) ],
|
|
'parses install and upgrade rows from a DNF transaction table');
|
|
|
|
$dnf_major = 5;
|
|
@ops = update_system_operations('tzdata vim-minimal',
|
|
'--setopt=disable_excludes=*');
|
|
like($command, qr{^/usr/bin/dnf --assumeno upgrade },
|
|
'uses a simulated upgrade for installed packages on DNF 5');
|
|
unlike($command, qr/disableplugin|disable_excludes/,
|
|
'does not pass the discovery-only bypass to a transaction preview');
|
|
is_deeply(\@lock_actions,
|
|
[ [ 'delete', [ qw(tzdata vim-minimal) ] ],
|
|
[ 'restore', [ qw(tzdata vim-minimal) ] ] ],
|
|
'temporarily unlocks selected packages for a held-update preview');
|
|
is_deeply([ map { $_->{'name'} } @ops ],
|
|
[ qw(tzdata vim-minimal wbt-virtual-server-theme) ],
|
|
'parses the held-package preview after restoring its locks');
|
|
is($ops[1]->{'epoch'}, '2', 'splits epochs from preview versions');
|
|
is($ops[2]->{'arch'}, 'noarch', 'handles wrapped package names');
|
|
}
|
|
|
|
done_testing();
|