mirror of
https://github.com/webmin/webmin.git
synced 2026-08-26 08:30:36 +01:00
Merge pull request #2823 from webmin/dev/dnf-hold
Add DNF versionlock support to Package Updates
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
* Add options to send Webmin and Usermin errors to the systemd journal [forum.virtualmin.com/t/136562](https://forum.virtualmin.com/t/miniserv-webserver-log-growing-too-big-should-be-rotated/136562)
|
||||
* Add webserver logging controls to Usermin Configuration module
|
||||
* Add option to rotate Webmin and Usermin webserver logs using `logrotate` instead of periodically clearing them [#2821](https://github.com/webmin/webmin/pull/2821)
|
||||
* Add DNF 4 and 5 package hold management to the Software Package Updates module
|
||||
* Fix DNF update confirmations by previewing packages and dependencies that will be installed or updated
|
||||
* Fix PostgreSQL initialization on EL systems to use SCRAM-SHA-256 authentication by default
|
||||
|
||||
#### 2.660 (August 20, 2026)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---- Changes since 2.660 ----
|
||||
Added support for managing DNF 4 and 5 version locks when the versionlock command is installed.
|
||||
Added a "New updates are found" choice to the "Send email when" option that sends a complete report only when new updates become available, so notify-only schedules no longer repeat the same message every run.
|
||||
---- Changes since 2.641 ----
|
||||
Added a Held updates view with controls to hold, unhold or explicitly update APT-held packages.
|
||||
|
||||
@@ -107,7 +107,7 @@ update_newver=New version
|
||||
update_confirm=Install Now
|
||||
update_confirmheld=Update Held Packages
|
||||
update_heldnote=These packages are held. This action explicitly updates them once, and leaves them held for future updates.
|
||||
update_enotheld=Package $1 is not currently held by APT
|
||||
update_enotheld=Package $1 is not currently held
|
||||
update_enoheldops=No update operation was found for the selected held packages. Refresh the package list and try again.
|
||||
update_none=None
|
||||
update_ops=Building complete list of packages ..
|
||||
|
||||
@@ -314,8 +314,12 @@ return defined(&software::update_system_updates);
|
||||
# Returns true if the current update system can list and change package holds.
|
||||
sub supports_package_holds
|
||||
{
|
||||
return defined(&software::list_update_system_holds) &&
|
||||
defined(&software::update_system_hold);
|
||||
return 0 if (!defined(&software::list_update_system_holds) ||
|
||||
!defined(&software::update_system_hold) ||
|
||||
!defined(&software::update_system_hold_flags));
|
||||
return &software::supports_update_system_holds()
|
||||
if (defined(&software::supports_update_system_holds));
|
||||
return 1;
|
||||
}
|
||||
|
||||
# list_package_holds()
|
||||
@@ -389,8 +393,9 @@ my ($name, $system, $install, $flags) = @_;
|
||||
$system ||= $software::update_system;
|
||||
my @rv;
|
||||
my $pkg;
|
||||
my $include_held = $system eq 'apt' && defined($flags) &&
|
||||
$flags eq '--allow-change-held-packages';
|
||||
my $include_held = $system eq $software::update_system &&
|
||||
&supports_package_holds() && defined($flags) &&
|
||||
$flags eq &software::update_system_hold_flags();
|
||||
|
||||
# First get from list of updates
|
||||
($pkg) = grep { $_->{'update'} eq $name &&
|
||||
@@ -490,14 +495,13 @@ unlink($current_cache_file);
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# list_package_operations(package|packages, system)
|
||||
# Given a package (or space-separate package list), returns a list of all
|
||||
# dependencies that will be installed
|
||||
# list_package_operations(package|packages, system, [flags])
|
||||
# Returns packages and dependencies that would be installed or updated.
|
||||
sub list_package_operations
|
||||
{
|
||||
my ($name, $system) = @_;
|
||||
my ($name, $system, $flags) = @_;
|
||||
if (defined(&software::update_system_operations)) {
|
||||
my @rv = &software::update_system_operations($name);
|
||||
my @rv = &software::update_system_operations($name, $flags);
|
||||
foreach my $p (@rv) {
|
||||
$p->{'system'} = $system;
|
||||
}
|
||||
|
||||
@@ -73,18 +73,18 @@ else {
|
||||
@pkgs || &error($text{'update_enone'});
|
||||
$allow_held = 0;
|
||||
if ($in{'mode'} eq 'held') {
|
||||
# The held-updates page is the only UI that can explicitly
|
||||
# override an APT hold for a single update transaction.
|
||||
# Only Held updates can override a hold for one transaction.
|
||||
&supports_package_holds() || &error($text{'hold_enotsupported'});
|
||||
@held = &list_package_holds();
|
||||
foreach $ps (@pkgs) {
|
||||
($p, $s) = split(/\//, $ps, 2);
|
||||
$s eq 'apt' && &package_is_held($p, \@held) ||
|
||||
$s eq $software::update_system &&
|
||||
&package_is_held($p, \@held) ||
|
||||
&error(&text('update_enotheld', $p));
|
||||
}
|
||||
$allow_held = 1;
|
||||
}
|
||||
$install_flags = $allow_held ? '--allow-change-held-packages' :
|
||||
$install_flags = $allow_held ? &software::update_system_hold_flags() :
|
||||
$in{'flags'};
|
||||
&ui_print_unbuffered_header(undef,
|
||||
$in{'mode'} eq 'new' ? $text{'update_title2'} : $text{'update_title'}, "");
|
||||
@@ -102,7 +102,8 @@ else {
|
||||
($p, $s) = split(/\//, $ps);
|
||||
push(@pkgnames, $p);
|
||||
}
|
||||
@ops = &list_package_operations(join(" ", @pkgnames), $s);
|
||||
@ops = &list_package_operations(join(" ", @pkgnames), $s,
|
||||
$install_flags);
|
||||
&error($text{'update_enoheldops'}) if (!@ops && $allow_held);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---- Changes since 2.641 ----
|
||||
Fix Alpine Linux mysql/mariadb package installs names due missing server utils (means at least Alpine Linux package installation is supported since Alpine linux v 3.16 up to edge)
|
||||
Added APT functions for listing, holding, unholding and explicitly updating held packages.
|
||||
Added DNF 4 and 5 functions for listing, locking, unlocking and explicitly updating version-locked packages.
|
||||
---- Changes since 1.130 ----
|
||||
Packages can now be installed directly from yum, if installed.
|
||||
The entire system can also be upgraded from yum.
|
||||
|
||||
@@ -20,6 +20,13 @@ $name =~ s/:[A-Za-z0-9][A-Za-z0-9._-]*$//;
|
||||
return $name;
|
||||
}
|
||||
|
||||
# update_system_hold_flags()
|
||||
# Returns the APT option for explicitly updating held packages.
|
||||
sub update_system_hold_flags
|
||||
{
|
||||
return '--allow-change-held-packages';
|
||||
}
|
||||
|
||||
# update_system_install([package], [&in], [no-force], [flags])
|
||||
# Install some package with apt
|
||||
sub update_system_install
|
||||
@@ -105,15 +112,17 @@ $? = $status;
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# update_system_operations(packages)
|
||||
# Given a list of packages, returns a list containing packages that will
|
||||
# actually get installed, each of which is a hash ref with name and version.
|
||||
# update_system_operations(packages, [flags])
|
||||
# Returns packages APT would install or update. The optional hold flag includes
|
||||
# held packages in the simulation.
|
||||
sub update_system_operations
|
||||
{
|
||||
my ($packages) = @_;
|
||||
my ($packages, $flags) = @_;
|
||||
$ENV{'UCF_FORCE_CONFFOLD'} = 'YES';
|
||||
$ENV{'DEBIAN_FRONTEND'} = 'noninteractive';
|
||||
my $cmd = "apt-get -s install ".
|
||||
my $holdflag = defined($flags) &&
|
||||
$flags eq &update_system_hold_flags() ? " $flags" : "";
|
||||
my $cmd = "apt-get -s$holdflag install ".
|
||||
join(" ", map { quotemeta($_) } split(/\s+/, $packages)).
|
||||
" </dev/null 2>&1";
|
||||
&clean_language();
|
||||
@@ -261,8 +270,8 @@ close(DUMP);
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# update_system_updates()
|
||||
# Returns a list of available package updates
|
||||
# update_system_updates([include-holds])
|
||||
# Returns available package updates, optionally including held packages.
|
||||
sub update_system_updates
|
||||
{
|
||||
my ($include_holds) = @_;
|
||||
|
||||
@@ -266,6 +266,11 @@ yum_input=Package from YUM
|
||||
yum_install=Installing package(s) with command $1 ..
|
||||
yum_ok=.. install complete
|
||||
yum_failed=.. install failed!
|
||||
yum_unholdfailed=.. failed to temporarily remove the version lock on $1 : $2
|
||||
yum_reholdfailed=.. failed to restore the version lock on $1 : $2
|
||||
yum_versionlock_missing=The DNF versionlock command is not installed
|
||||
yum_versionlock_none=No packages were specified
|
||||
yum_versionlock_failed=dnf versionlock $1 failed
|
||||
yum_find=Browse YUM ..
|
||||
yum_package=Package
|
||||
yum_version=Version
|
||||
|
||||
@@ -19,17 +19,61 @@ sub list_update_system_commands
|
||||
return ($yum_command);
|
||||
}
|
||||
|
||||
# get_dnf_version()
|
||||
# Returns the DNF major version, or zero when using YUM.
|
||||
sub get_dnf_version
|
||||
{
|
||||
return 0 if ($yum_command !~ /(?:^|\/)dnf(?:-\d+)?$/);
|
||||
if (!defined($dnf_version)) {
|
||||
&clean_language();
|
||||
my $out = &backquote_command(
|
||||
"$yum_command --version 2>&1 </dev/null");
|
||||
&reset_environment();
|
||||
$dnf_version = $out =~ /^dnf5\s+version\s+(\d+)/m ? $1 : 4;
|
||||
}
|
||||
return $dnf_version;
|
||||
}
|
||||
|
||||
# supports_update_system_holds()
|
||||
# Reports whether DNF provides the versionlock command.
|
||||
sub supports_update_system_holds
|
||||
{
|
||||
if (!defined($supports_dnf_versionlock)) {
|
||||
$supports_dnf_versionlock = 0;
|
||||
if (&get_dnf_version()) {
|
||||
&clean_language();
|
||||
my $out = &backquote_command(
|
||||
"$yum_command --help 2>&1 </dev/null");
|
||||
&reset_environment();
|
||||
$supports_dnf_versionlock = 1
|
||||
if ($out =~ /^\s*versionlock\s+/m);
|
||||
}
|
||||
}
|
||||
return $supports_dnf_versionlock;
|
||||
}
|
||||
|
||||
# update_system_hold_flags()
|
||||
# Returns the DNF option that exposes version-locked updates.
|
||||
sub update_system_hold_flags
|
||||
{
|
||||
return &get_dnf_version() >= 5 ? '--setopt=disable_excludes=*' :
|
||||
'--disableplugin=versionlock';
|
||||
}
|
||||
|
||||
# update_system_install([packages], [&in], [no-force], [flags])
|
||||
# Install some package with yum
|
||||
# Installs or updates packages with YUM or DNF.
|
||||
sub update_system_install
|
||||
{
|
||||
local $update = $_[0] || $in{'update'};
|
||||
local $in = $_[1];
|
||||
local $force = !$_[2];
|
||||
local $flags = $_[3];
|
||||
local $versionlock_update = defined($flags) &&
|
||||
$flags eq &update_system_hold_flags();
|
||||
local $runflags = $versionlock_update ? undef : $flags;
|
||||
local $qflags;
|
||||
$qflags = &trim(join(" ", map { quotemeta($_) } split(/ /, $flags)))
|
||||
if ($flags);
|
||||
$qflags = &trim(join(" ", map { quotemeta($_) } split(/ /, $runflags)))
|
||||
if ($runflags);
|
||||
$update =~ s/\.\*/\*/g;
|
||||
local $enable;
|
||||
if ($in->{'enablerepo'}) {
|
||||
@@ -45,7 +89,28 @@ if (@names == 1) {
|
||||
}
|
||||
$update = join(" ", map { quotemeta($_) } @names);
|
||||
|
||||
# Work out command to use - for DNF, upgrades need to use the update command
|
||||
# Temporarily unlock selected packages, then restore their holds after the
|
||||
# transaction.
|
||||
local @relock;
|
||||
if ($versionlock_update) {
|
||||
my @packages = &unique(@updates);
|
||||
my $unlock_error = &delete_update_system_holds(
|
||||
\@packages, \@relock);
|
||||
if ($unlock_error) {
|
||||
my $relock_error = &restore_update_system_holds(\@relock);
|
||||
if ($relock_error) {
|
||||
print &text('yum_reholdfailed',
|
||||
"<tt>".&html_escape(join(" ", @packages))."</tt>",
|
||||
&html_escape($relock_error)),"<p>\n";
|
||||
}
|
||||
print &text('yum_unholdfailed',
|
||||
"<tt>".&html_escape(join(" ", @packages))."</tt>",
|
||||
&html_escape($unlock_error)),"<p>\n";
|
||||
return ( );
|
||||
}
|
||||
}
|
||||
|
||||
# Use update for installed DNF packages and install for everything else.
|
||||
local $cmd;
|
||||
if ($yum_command =~ /dnf$/) {
|
||||
local @pinfo = &package_info($updates[0]);
|
||||
@@ -62,9 +127,9 @@ else {
|
||||
|
||||
# Work out the command to run, which may enable some repos
|
||||
my $uicmd = "$yum_command $enable -y $cmd ".join(" ", @names);
|
||||
$uicmd .= " $flags" if ($flags);
|
||||
$uicmd .= " $runflags" if ($runflags);
|
||||
my $fullcmd = "$yum_command $enable -y $cmd $update";
|
||||
$fullcmd .= " $qflags" if ($flags);
|
||||
$fullcmd .= " $qflags" if ($qflags);
|
||||
foreach my $u (@updates) {
|
||||
my $repo = &update_system_repo($u);
|
||||
if ($repo) {
|
||||
@@ -110,7 +175,7 @@ while(<CMD>) {
|
||||
}
|
||||
}
|
||||
elsif (/^\s+(Updating|Installing|Upgrading)\s+:\s+(\S+)/) {
|
||||
# Line like :
|
||||
# Older DNF and YUM progress lines, for example:
|
||||
# Updating : wbt-virtual-server-theme 1/2
|
||||
# or
|
||||
# Installing : 2:nmap-5.51-2.el6.i686 1/1
|
||||
@@ -119,12 +184,11 @@ while(<CMD>) {
|
||||
$pkg =~ s/\-\d.*$//; # Strip version number from end
|
||||
push(@rv, $pkg);
|
||||
}
|
||||
elsif (/\]\s+(Upgrading|Installing)\s+(\S+)/) {
|
||||
# Line like :
|
||||
elsif (/\]\s+(Upgrading|Installing|Downgrading|Reinstalling)\s+(\S+)/) {
|
||||
# DNF 5 progress line, for example:
|
||||
# [3/8] Upgrading libcurl-0:8.11.1-5.fc42 100% ...
|
||||
local $pkg = $2;
|
||||
$pkg =~ s/:\d.*$//; # Strip version number from end
|
||||
push(@rv, $pkg);
|
||||
local $pkg = &update_system_nevra_name($2);
|
||||
push(@rv, $pkg) if ($pkg);
|
||||
}
|
||||
if (!/ETA/ && !/\%\s+done\s+\d+\/\d+\s*$/) {
|
||||
print &html_escape($_."\n");
|
||||
@@ -134,8 +198,19 @@ while(<CMD>) {
|
||||
}
|
||||
}
|
||||
close(CMD);
|
||||
local $status = $?;
|
||||
|
||||
# Restore holds at the versions now installed.
|
||||
if (@relock) {
|
||||
local $relock_error = &restore_update_system_holds(\@relock);
|
||||
if ($relock_error) {
|
||||
print &text('yum_reholdfailed',
|
||||
"<tt>".&html_escape(join(" ", &unique(@updates)))."</tt>",
|
||||
&html_escape($relock_error)),"<p>\n";
|
||||
}
|
||||
}
|
||||
print "</pre>\n";
|
||||
if ($? || $nopackage) {
|
||||
if ($status || $nopackage) {
|
||||
print "$text{'yum_failed'}<p>\n";
|
||||
return ( );
|
||||
}
|
||||
@@ -167,12 +242,26 @@ for(my $i=0; $i<$n; $i++) {
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# update_system_operations(packages)
|
||||
# Given a list of packages, returns a list containing packages that will
|
||||
# actually get installed, each of which is a hash ref with name and version.
|
||||
# update_system_nevra_name(nevra)
|
||||
# Extracts the package name from a NEVRA string printed by DNF.
|
||||
sub update_system_nevra_name
|
||||
{
|
||||
my ($nevra) = @_;
|
||||
$nevra =~ s/^\d+://; # Older DNF may put the epoch before the name
|
||||
return $1 if ($nevra =~ /^(.+)-\d+:/);
|
||||
return $1 if ($nevra =~ /^(.+)-\d[^-]*-[^-]+(?:\.[^.]+)?$/);
|
||||
return undef;
|
||||
}
|
||||
|
||||
# update_system_operations(packages, [flags])
|
||||
# Returns packages YUM or DNF would install or update. DNF previews the
|
||||
# transaction directly; YUM uses shell mode.
|
||||
sub update_system_operations
|
||||
{
|
||||
my ($packages) = @_;
|
||||
my ($packages, $flags) = @_;
|
||||
if ($yum_command =~ /(?:^|\/)dnf(?:-\d+)?$/) {
|
||||
return &update_system_dnf_operations($packages, $flags);
|
||||
}
|
||||
my $temp = &transname();
|
||||
&open_tempfile(SHELL, ">$temp", 0, 1);
|
||||
&print_tempfile(SHELL, "install $packages\n");
|
||||
@@ -198,6 +287,87 @@ close(SHELL);
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# update_system_dnf_operations(packages, [flags])
|
||||
# Returns packages DNF would install or update in a simulated transaction.
|
||||
sub update_system_dnf_operations
|
||||
{
|
||||
my ($packages, $flags) = @_;
|
||||
my @rv;
|
||||
my $hold_override = defined($flags) &&
|
||||
$flags eq &update_system_hold_flags();
|
||||
my $runflags = defined($flags) && !$hold_override ?
|
||||
&trim(join(" ", map { quotemeta($_) } split(/\s+/, $flags))) : "";
|
||||
my @relock;
|
||||
if ($hold_override) {
|
||||
my @selected = &unique(split(/\s+/, $packages));
|
||||
my $error = &delete_update_system_holds(\@selected, \@relock);
|
||||
if ($error) {
|
||||
&restore_update_system_holds(\@relock) if (@relock);
|
||||
return ( );
|
||||
}
|
||||
}
|
||||
# DNF 5 does not upgrade installed packages with install, so use upgrade for
|
||||
# its previews.
|
||||
my $action = &get_dnf_version() >= 5 ? 'upgrade' : 'install';
|
||||
my $command = "$yum_command --assumeno $action ".
|
||||
join(" ", map { quotemeta($_) } split(/\s+/, $packages));
|
||||
$command .= " $runflags" if ($runflags);
|
||||
$command .= " 2>/dev/null";
|
||||
&clean_language();
|
||||
&open_execute_command(DNF, $command, 1, 1);
|
||||
my ($intable, $skip, $wrapped);
|
||||
while(<DNF>) {
|
||||
s/\r|\n//g;
|
||||
if (/^\s*Package\s+Arch(itecture)?\s+Version\s+Repo/i) {
|
||||
# Start reading the transaction table.
|
||||
$intable = 1;
|
||||
}
|
||||
elsif (/^\s*Transaction\s+Summary/i) {
|
||||
last;
|
||||
}
|
||||
elsif (!$intable || /^=+$/) {
|
||||
next;
|
||||
}
|
||||
elsif (/^\S/) {
|
||||
# Ignore sections that do not add package versions.
|
||||
$skip = !/^(Installing|Upgrading|Reinstalling|Downgrading)/i;
|
||||
$wrapped = undef;
|
||||
}
|
||||
elsif ($skip) {
|
||||
next;
|
||||
}
|
||||
elsif (/^\s+(\S+)\s*$/) {
|
||||
# Save a long package name wrapped onto its own line.
|
||||
$wrapped = $1;
|
||||
}
|
||||
elsif (/^\s+replacing\s/i) {
|
||||
# Ignore an old package shown below its replacement.
|
||||
$wrapped = undef;
|
||||
}
|
||||
elsif (/^\s+\S/) {
|
||||
# Parse a complete row, or the remainder of a wrapped row.
|
||||
my @cols = split(/\s+/, &trim($_));
|
||||
unshift(@cols, $wrapped) if ($wrapped);
|
||||
$wrapped = undef;
|
||||
next if (@cols < 4);
|
||||
my $pkg = { 'name' => $cols[0],
|
||||
'arch' => $cols[1],
|
||||
'version' => $cols[2] };
|
||||
if ($pkg->{'version'} =~ s/^(\S+)://) {
|
||||
$pkg->{'epoch'} = $1;
|
||||
}
|
||||
push(@rv, $pkg);
|
||||
}
|
||||
}
|
||||
close(DNF);
|
||||
&reset_environment();
|
||||
if (@relock) {
|
||||
my $error = &restore_update_system_holds(\@relock);
|
||||
return ( ) if ($error);
|
||||
}
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# show_update_system_opts()
|
||||
# Returns HTML for enabling a repository, if any are disabled
|
||||
sub show_update_system_opts
|
||||
@@ -333,14 +503,19 @@ while(<PKG>) {
|
||||
close(PKG);
|
||||
}
|
||||
|
||||
# update_system_updates()
|
||||
# Returns a list of package updates available from yum
|
||||
# update_system_updates([include-holds])
|
||||
# Returns available package updates, optionally including version-locked ones.
|
||||
sub update_system_updates
|
||||
{
|
||||
my ($include_holds) = @_;
|
||||
local @rv;
|
||||
local %done;
|
||||
if ($yum_command =~ /dnf/) {
|
||||
&open_execute_command(PKG, "$yum_command check-update 2>/dev/null", 1, 1);
|
||||
my $holdflag = $include_holds && &supports_update_system_holds() ?
|
||||
" ".&update_system_hold_flags() : "";
|
||||
$holdflag =~ s/\*/\\*/g;
|
||||
&open_execute_command(PKG,
|
||||
"$yum_command$holdflag check-update 2>/dev/null", 1, 1);
|
||||
}
|
||||
else {
|
||||
&open_execute_command(PKG, "$yum_command check-update 2>/dev/null | tr '\n' '#' | sed -e 's/# / /g' | tr '#' '\n'", 1, 1);
|
||||
@@ -361,10 +536,157 @@ while(<PKG>) {
|
||||
last if (/Obsoleting\s+Packages/i);
|
||||
}
|
||||
close(PKG);
|
||||
if (&supports_update_system_holds()) {
|
||||
my %holds = map { $_, 1 } &list_update_system_holds();
|
||||
foreach my $pkg (@rv) {
|
||||
$pkg->{'held'} = 1 if ($holds{$pkg->{'name'}});
|
||||
}
|
||||
@rv = grep { !$_->{'held'} } @rv if (!$include_holds);
|
||||
}
|
||||
&set_yum_security_field(\%done);
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# update_system_hold_spec_name(spec)
|
||||
# Extracts the package name from an exact DNF 4 versionlock entry.
|
||||
sub update_system_hold_spec_name
|
||||
{
|
||||
my ($spec) = @_;
|
||||
return undef if ($spec =~ /^!/);
|
||||
return $1 if ($spec =~ /^(.+)-\d+:[^-]+-[^-]+\.[^.]+$/);
|
||||
return $1 if ($spec =~ /^\d+:(.+)-[^-]+-[^-]+\.[^.]+$/);
|
||||
return undef;
|
||||
}
|
||||
|
||||
# list_update_system_hold_specs()
|
||||
# Returns exact locks that Webmin can safely manage by package name.
|
||||
sub list_update_system_hold_specs
|
||||
{
|
||||
return ( ) if (!&supports_update_system_holds());
|
||||
my @locks;
|
||||
&clean_language();
|
||||
&open_execute_command(VLOCK,
|
||||
"$yum_command -q versionlock list 2>/dev/null", 1, 1);
|
||||
if (&get_dnf_version() >= 5) {
|
||||
# DNF 5 formats each lock as a multi-line package block.
|
||||
my ($lock, @blocks);
|
||||
while(<VLOCK>) {
|
||||
s/\r|\n//g;
|
||||
if (/^Package name:\s*(\S+)/) {
|
||||
push(@blocks, $lock) if ($lock);
|
||||
$lock = { 'name' => $1, 'spec' => $1 };
|
||||
}
|
||||
elsif ($lock && /^evr\s*=\s*(\S+)/) {
|
||||
$lock->{'exact'} = 1;
|
||||
}
|
||||
elsif ($lock && /\S/ && !/^\s*#/) {
|
||||
$lock->{'custom'} = 1;
|
||||
}
|
||||
elsif (!/\S/) {
|
||||
push(@blocks, $lock) if ($lock);
|
||||
$lock = undef;
|
||||
}
|
||||
}
|
||||
push(@blocks, $lock) if ($lock);
|
||||
|
||||
# Ignore globs, duplicate entries and custom conditions because deleting
|
||||
# them by name could remove rules that Webmin cannot restore.
|
||||
my (%blocks, %custom);
|
||||
foreach my $block (@blocks) {
|
||||
$blocks{$block->{'name'}}++;
|
||||
$custom{$block->{'name'}} = 1
|
||||
if (!$block->{'exact'} || $block->{'custom'});
|
||||
}
|
||||
@locks = grep { !$custom{$_->{'name'}} &&
|
||||
$blocks{$_->{'name'}} == 1 &&
|
||||
$_->{'name'} !~ /[\*\?\[\]]/ } @blocks;
|
||||
}
|
||||
else {
|
||||
# DNF 4 prints standard entries as name-epoch:version-release.arch.
|
||||
while(<VLOCK>) {
|
||||
s/\r|\n//g;
|
||||
my $name = &update_system_hold_spec_name($_);
|
||||
push(@locks, { 'name' => $name, 'spec' => $_ }) if ($name);
|
||||
}
|
||||
}
|
||||
close(VLOCK);
|
||||
&reset_environment();
|
||||
return @locks;
|
||||
}
|
||||
|
||||
# list_update_system_holds()
|
||||
# Returns package names held by exact DNF version locks.
|
||||
sub list_update_system_holds
|
||||
{
|
||||
my %holds = map { $_->{'name'}, 1 } &list_update_system_hold_specs();
|
||||
return sort keys %holds;
|
||||
}
|
||||
|
||||
# run_update_system_hold(action, package)
|
||||
# Runs one versionlock operation. Returns undef on success, or error text.
|
||||
sub run_update_system_hold
|
||||
{
|
||||
my ($action, $package) = @_;
|
||||
my $cmd = "$yum_command -q versionlock $action ".quotemeta($package);
|
||||
my $out;
|
||||
&clean_language();
|
||||
my $status = &execute_command_logged($cmd, undef, \$out, \$out);
|
||||
&reset_environment();
|
||||
if ($status) {
|
||||
$out = &trim($out);
|
||||
return $out || &text('yum_versionlock_failed', $action);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
# delete_update_system_holds(&packages, &removed)
|
||||
# Unlocks selected packages and records each removed hold for restoration.
|
||||
sub delete_update_system_holds
|
||||
{
|
||||
my ($packages, $removed) = @_;
|
||||
my %held = map { $_, 1 } &list_update_system_holds();
|
||||
my $error;
|
||||
foreach my $package (&unique(@$packages)) {
|
||||
next if (!$held{$package});
|
||||
$error = &run_update_system_hold('delete', $package);
|
||||
last if ($error);
|
||||
push(@$removed, $package);
|
||||
}
|
||||
return $error;
|
||||
}
|
||||
|
||||
# restore_update_system_holds(&packages)
|
||||
# Re-locks packages at their currently installed versions.
|
||||
sub restore_update_system_holds
|
||||
{
|
||||
my ($packages) = @_;
|
||||
foreach my $package (&unique(@$packages)) {
|
||||
my $error = &run_update_system_hold('add', $package);
|
||||
return $error if ($error);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
# update_system_hold(&packages, hold)
|
||||
# Holds or unholds DNF packages by name. Returns undef on success, or error text.
|
||||
sub update_system_hold
|
||||
{
|
||||
my ($packages, $hold) = @_;
|
||||
return $text{'yum_versionlock_missing'}
|
||||
if (!&supports_update_system_holds());
|
||||
my @packages = &unique(@$packages);
|
||||
return $text{'yum_versionlock_none'} if (!@packages);
|
||||
if ($hold) {
|
||||
foreach my $package (@packages) {
|
||||
my $error = &run_update_system_hold('add', $package);
|
||||
return $error if ($error);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
my @removed;
|
||||
return &delete_update_system_holds(\@packages, \@removed);
|
||||
}
|
||||
|
||||
# get_yum_config()
|
||||
# Returns entries from the YUM config file, as a list of hash references
|
||||
sub get_yum_config
|
||||
@@ -543,4 +865,3 @@ else {
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@ is(strip_apt_package_arch('ncurses-base'), 'ncurses-base',
|
||||
|
||||
{
|
||||
no warnings qw(once redefine);
|
||||
my $command;
|
||||
local *backquote_command = sub {
|
||||
$command = $_[0];
|
||||
return "Inst libtinfo6:amd64 [6.3-2ubuntu0.1] ".
|
||||
"(6.3-2ubuntu0.2 Ubuntu:22.04/jammy-updates [amd64])\n";
|
||||
};
|
||||
@@ -34,6 +36,9 @@ local *reset_environment = sub { };
|
||||
my @ops = update_system_operations('libtinfo6');
|
||||
is($ops[0]->{'name'}, 'libtinfo6',
|
||||
'normalizes package names from simulated APT operations');
|
||||
update_system_operations('libtinfo6', update_system_hold_flags());
|
||||
like($command, qr/^apt-get -s --allow-change-held-packages install /,
|
||||
'overrides holds while resolving an explicit held-package update');
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
347
t/software-yum.t
Normal file
347
t/software-yum.t
Normal file
@@ -0,0 +1,347 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user