mirror of
https://github.com/webmin/webmin.git
synced 2026-10-29 06:31:04 +00:00
This patch fixes package installation when a request contains both installed and missing packages. DNF 4 uses one `install` transaction, while DNF 5 updates installed packages first and installs missing packages only after the update succeeds. It preserves architecture package handling, always installs required repository packages, and prevents an empty package list from becoming a full system update. The complete transaction reads from `/dev/null`, and tests cover DNF 4, DNF 5, installed, missing, mixed, and empty package sets. Live AlmaLinux testing also confirmed both the single-command and chained-command paths.
929 lines
25 KiB
Prolog
Executable File
929 lines
25 KiB
Prolog
Executable File
# yum-lib.pl
|
|
# Functions for installing packages with yum
|
|
|
|
if ($config{'yum_config'}) {
|
|
$yum_config = $config{'yum_config'};
|
|
}
|
|
elsif (&has_command("yum")) {
|
|
$yum_config = "/etc/yum.conf";
|
|
}
|
|
elsif (&has_command("dnf")) {
|
|
$yum_config = "/etc/dnf/dnf.conf";
|
|
}
|
|
|
|
$yum_command = &has_command("dnf") || &has_command("yum") || "yum";
|
|
$yum_repos_dir = "/etc/yum.repos.d";
|
|
|
|
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])
|
|
# Installs or updates space-separated packages with YUM or DNF. The package
|
|
# list defaults to form input, the input hash can select a repository, and
|
|
# flags are passed to the transaction. No-force is kept for compatibility but
|
|
# has no effect.
|
|
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(/ /, $runflags)))
|
|
if ($runflags);
|
|
$update =~ s/\.\*/\*/g;
|
|
local $enable;
|
|
# Add the repository selected on the install form.
|
|
if ($in->{'enablerepo'}) {
|
|
$enable = "enablerepo=".quotemeta($in->{'enablerepo'});
|
|
}
|
|
local (@rv, @newpacks);
|
|
|
|
# Split installed packages by architecture and record missing packages.
|
|
local @updates = split(/\s+/, $update);
|
|
local (@names, @installed, @missing);
|
|
foreach my $u (@updates) {
|
|
my $installed;
|
|
my @package_names = &append_architectures($u, \$installed);
|
|
push(@names, @package_names);
|
|
if ($installed) {
|
|
# Queue installed architectures for update.
|
|
push(@installed, @package_names);
|
|
}
|
|
else {
|
|
# Queue missing package specifications for installation.
|
|
push(@missing, @package_names);
|
|
}
|
|
}
|
|
if (@names == 1) {
|
|
# Keep the original specification when no architecture split was needed.
|
|
@names = ( $update );
|
|
}
|
|
$update = join(" ", map { quotemeta($_) } @names);
|
|
|
|
# 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 ( );
|
|
}
|
|
}
|
|
|
|
# DNF 4 installs and upgrades a mixed package set in one transaction. DNF 5
|
|
# needs separate actions because install does not upgrade packages by name.
|
|
my @actions;
|
|
if ($yum_command =~ /dnf$/ && @installed && @missing &&
|
|
&get_dnf_version() >= 5) {
|
|
@actions = ( [ 'update', \@installed ], [ 'install', \@missing ] );
|
|
}
|
|
else {
|
|
# Use one action for YUM and non-mixed DNF sets.
|
|
my $cmd = $yum_command =~ /dnf$/ && @installed && !@missing ?
|
|
'update' : 'install';
|
|
@actions = ( [ $cmd, \@names ] );
|
|
}
|
|
|
|
# Join actions so DNF 5 installs only after its update succeeds.
|
|
my (@uicmds, @fullcmds);
|
|
foreach my $action (@actions) {
|
|
my ($cmd, $packages) = @$action;
|
|
my $uicmd = "$yum_command $enable -y $cmd ".join(" ", @$packages);
|
|
$uicmd .= " $runflags" if ($runflags);
|
|
push(@uicmds, $uicmd);
|
|
my $fullcmd = "$yum_command $enable -y $cmd ".
|
|
join(" ", map { quotemeta($_) } @$packages);
|
|
$fullcmd .= " $qflags" if ($qflags);
|
|
push(@fullcmds, $fullcmd);
|
|
}
|
|
my $uicmd = join(" && ", @uicmds);
|
|
my $fullcmd = join(" && ", @fullcmds);
|
|
foreach my $u (@updates) {
|
|
my $repo = &update_system_repo($u);
|
|
if ($repo) {
|
|
# Install a required repository package first.
|
|
$fullcmd = "$yum_command -y install $repo ; $fullcmd";
|
|
}
|
|
}
|
|
|
|
print &text('yum_install', "<tt>".&html_escape($uicmd)."</tt>"),"\n";
|
|
print "<pre data-installer>";
|
|
&additional_log('exec', undef, $fullcmd);
|
|
$SIG{'TERM'} = 'ignore'; # Installing webmin itself may kill this script
|
|
&open_execute_command(CMD, "($fullcmd) </dev/null", 2);
|
|
while(<CMD>) {
|
|
s/\r|\n//g;
|
|
if (/^\[(update|install|deps):\s+(\S+)\s+/) {
|
|
push(@rv, $2);
|
|
}
|
|
elsif (/^(Installed|Dependency Installed|Updated|Dependency Updated|Upgraded):\s*(.*)/) {
|
|
# Line like :
|
|
# Updated:
|
|
# wbt-virtual-server-theme.x86
|
|
local @pkgs = split(/\s+/, $2);
|
|
if (!@pkgs) {
|
|
# Wrapped to next line(s)
|
|
while(1) {
|
|
local $pkgs = <CMD>;
|
|
last if (!$pkgs);
|
|
print &html_escape($pkgs);
|
|
$pkgs =~ s/^\s+//;
|
|
$pkgs =~ s/\s+$//;
|
|
my @linepkgs = split(/\s+/, $_);
|
|
last if (!@linepkgs);
|
|
push(@pkgs, @linepkgs);
|
|
}
|
|
}
|
|
foreach my $p (@pkgs) {
|
|
if ($p !~ /:/ && $p =~ /^(\S+)\.(\S+)$/) {
|
|
my $pname = $1;
|
|
if ($p =~ /[^0-9\.\-\_i]/) {
|
|
push(@rv, $pname);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
elsif (/^\s+(Updating|Installing|Upgrading)\s+:\s+(\S+)/) {
|
|
# 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
|
|
local $pkg = $2;
|
|
$pkg =~ s/^\d://; # Strip epoch from front
|
|
$pkg =~ s/\-\d.*$//; # Strip version number from end
|
|
push(@rv, $pkg);
|
|
}
|
|
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 = &update_system_nevra_name($2);
|
|
push(@rv, $pkg) if ($pkg);
|
|
}
|
|
if (!/ETA/ && !/\%\s+done\s+\d+\/\d+\s*$/) {
|
|
print &html_escape($_."\n");
|
|
}
|
|
if ($update =~ /perl\(/ && /No\s+package\s+.*available/i) {
|
|
$nopackage = 1;
|
|
}
|
|
}
|
|
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 ($status || $nopackage) {
|
|
print "$text{'yum_failed'}<p>\n";
|
|
return ( );
|
|
}
|
|
else {
|
|
print "$text{'yum_ok'}<p>\n";
|
|
return &unique(@rv);
|
|
}
|
|
}
|
|
|
|
# append_architectures(package, [installed])
|
|
# Returns installed package names with their architectures. Sets installed
|
|
# when any are found, or returns the original specification when none are.
|
|
sub append_architectures
|
|
{
|
|
my ($name, $installed) = @_;
|
|
local %packages;
|
|
my $n = &list_packages($name);
|
|
$$installed = $n ? 1 : 0 if ($installed);
|
|
# Keep an unresolved specification for installation.
|
|
return ( $name ) if (!$n);
|
|
my @rv;
|
|
for(my $i=0; $i<$n; $i++) {
|
|
if ($packages{$i,'arch'}) {
|
|
# Include the installed architecture.
|
|
push(@rv, $packages{$i,'name'}.".".$packages{$i,'arch'});
|
|
}
|
|
else {
|
|
# Keep names with no architecture metadata.
|
|
push(@rv, $packages{$i,'name'});
|
|
}
|
|
}
|
|
@rv = &unique(@rv);
|
|
return @rv;
|
|
}
|
|
|
|
# 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, $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");
|
|
&print_tempfile(SHELL, "transaction solve\n");
|
|
&close_tempfile(SHELL);
|
|
my @rv;
|
|
open(SHELL, "$yum_command shell $temp |");
|
|
while(<SHELL>) {
|
|
if (/Package\s+(\S+)\s+(\S+)\s+(set|will\s+be\s+an\s+update)/i) {
|
|
my $pkg = { 'name' => $1,
|
|
'version' => $2 };
|
|
if ($pkg->{'name'} =~ s/\.([^\.]+)$//) {
|
|
$pkg->{'arch'} = $1;
|
|
}
|
|
if ($pkg->{'version'} =~ s/^(\S+)://) {
|
|
$pkg->{'epoch'} = $1;
|
|
}
|
|
push(@rv, $pkg);
|
|
}
|
|
}
|
|
close(SHELL);
|
|
&unlink_file($temp);
|
|
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
|
|
{
|
|
local @pinfo = &package_info("yum");
|
|
if (&compare_versions($pinfo[4], "2.1.10") > 0) {
|
|
local $conf = &get_yum_config();
|
|
local @ena;
|
|
foreach my $r (@$conf) {
|
|
if ($r->{'values'}->{'enabled'} eq '0') {
|
|
push(@ena, $r->{'name'});
|
|
}
|
|
}
|
|
if (@ena) {
|
|
return $text{'yum_enable'}." ".
|
|
&ui_select("enablerepo", "",
|
|
[ [ "", $text{'yum_none'} ],
|
|
map { [ $_ ] } @ena ]);
|
|
}
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
# update_system_resolve(name)
|
|
# Converts a standard package name like apache, sendmail or squid into
|
|
# the name used by YUM.
|
|
sub update_system_resolve
|
|
{
|
|
my ($name) = @_;
|
|
$name = $name eq "apache" ? "httpd mod_.*" :
|
|
$name eq "dhcpd" ? "dhcp dhcp-server" :
|
|
$name eq "mysql" ? "perl-DBD-MySQL mariadb mariadb-server mysql mysql-server" :
|
|
$name eq "openssh" ? "openssh openssh-server" :
|
|
$name eq "postgresql" ? "postgresql postgresql-libs postgresql-server" :
|
|
$name eq "openldap" ? "openldap-servers openldap-clients" :
|
|
$name eq "ldap" ? "nss-pam-ldapd pam_ldap nss_ldap" :
|
|
$name eq "virtualmin-modules" ? "wbm-.*" : $name;
|
|
my $flags;
|
|
$flags = '--skip-broken' if ($_[0] =~ /^dhcpd|mysql$/);
|
|
return wantarray ? ($name, $flags) : $name;
|
|
}
|
|
|
|
# update_system_repo(package)
|
|
# Returns the extra repo package that needs to be installed first before
|
|
# installing some package, if needed
|
|
sub update_system_repo
|
|
{
|
|
local ($name) = @_;
|
|
return $name eq "certbot" ? "epel-release" : undef;
|
|
}
|
|
|
|
# update_system_available()
|
|
# Returns a list of package names and versions that are available from YUM
|
|
sub update_system_available
|
|
{
|
|
local @rv;
|
|
local %done;
|
|
&open_execute_command(PKG, "$yum_command info", 1, 1);
|
|
while(<PKG>) {
|
|
s/\r|\n//g;
|
|
if (/^Name\s*:\s*(\S+)/) {
|
|
if ($done{$1}) {
|
|
# Seen before .. update with newer info. This can happen
|
|
# when YUM shows the installed version first.
|
|
$pkg = $done{$1};
|
|
delete($pkg->{'epoch'});
|
|
delete($pkg->{'version'});
|
|
}
|
|
else {
|
|
# Start of a new package
|
|
$pkg = { 'name' => $1 };
|
|
$done{$pkg->{'name'}} = $pkg;
|
|
push(@rv, $pkg);
|
|
}
|
|
}
|
|
elsif (/^Arch\s*:\s*(\S+)/) {
|
|
$pkg->{'arch'} = $1;
|
|
}
|
|
elsif (/^Version\s*:\s*(\S+)/) {
|
|
$pkg->{'version'} = $1;
|
|
if ($pkg->{'version'} =~ s/^(\S+)://) {
|
|
$pkg->{'epoch'} = $1;
|
|
}
|
|
}
|
|
elsif (/^Release\s*:\s*(\S+)/) {
|
|
$pkg->{'version'} .= "-".$1;
|
|
}
|
|
elsif (/^Repo\s*:\s*(\S+)/) {
|
|
$pkg->{'source'} = $1;
|
|
}
|
|
elsif (/^Summary\s*:\s*(\S.*)/) {
|
|
$pkg->{'desc'} = $1;
|
|
}
|
|
elsif (/^Epoch\s*:\s*(\S.*)/) {
|
|
$pkg->{'epoch'} = $1;
|
|
}
|
|
}
|
|
close(PKG);
|
|
@rv = grep { $_->{'arch'} ne 'src' } @rv;
|
|
&set_yum_security_field(\%done);
|
|
return @rv;
|
|
}
|
|
|
|
# set_yum_security_field(&package-hash)
|
|
# Set security field on packages which are security updates
|
|
sub set_yum_security_field
|
|
{
|
|
local ($done) = @_;
|
|
&open_execute_command(PKG, "$yum_command updateinfo list sec 2>/dev/null", 1, 1);
|
|
while(<PKG>) {
|
|
s/\r|\n//g;
|
|
if (/^\S+\s+\S+\s+(\S+?)\-([0-9]\S+)\.([^\.]+)$/) {
|
|
local ($name, $ver) = ($1, $2);
|
|
if ($done->{$name}) {
|
|
$done->{$name}->{'source'} ||= 'security';
|
|
$done->{$name}->{'security'} = 1;
|
|
}
|
|
}
|
|
}
|
|
close(PKG);
|
|
&open_execute_command(PKG, "$yum_command list-sec 2>/dev/null", 1, 1);
|
|
while(<PKG>) {
|
|
s/\r|\n//g;
|
|
next if (/^(Loaded|updateinfo)/);
|
|
if (/^\S+\s+\S+\s+(\S+?)\-([0-9]\S+)\.([^\.]+)$/) {
|
|
local ($name, $ver) = ($1, $2);
|
|
if ($done->{$name}) {
|
|
$done->{$name}->{'source'} ||= 'security';
|
|
$done->{$name}->{'security'} = 1;
|
|
}
|
|
}
|
|
}
|
|
close(PKG);
|
|
}
|
|
|
|
# update_system_updates([include-holds])
|
|
# Returns available package updates, optionally including version-locked ones.
|
|
# On DNF, a failed check returns an empty list and sets $update_system_error
|
|
# to the error output. Any other result clears it, so callers can tell a
|
|
# failure from no updates.
|
|
sub update_system_updates
|
|
{
|
|
my ($include_holds) = @_;
|
|
local @rv;
|
|
local %done;
|
|
$update_system_error = undef;
|
|
if ($yum_command =~ /dnf/) {
|
|
# With DNF, add the flag that also lists version-locked updates if
|
|
# asked, escaping * for the shell
|
|
my $holdflag = $include_holds && &supports_update_system_holds() ?
|
|
" ".&update_system_hold_flags() : "";
|
|
$holdflag =~ s/\*/\\*/g;
|
|
|
|
# Capture the exit status: DNF exits with 0 for no updates, 100 for
|
|
# updates found and any other status on failure. Stderr goes to a file
|
|
# rather than a second pipe, which could fill up and block DNF while
|
|
# stdout is still being read. Stdin comes from /dev/null so a prompt,
|
|
# such as for a repository key import, cannot hang.
|
|
my $out = "";
|
|
my $errfile = &transname();
|
|
my $ex = &execute_command("$yum_command$holdflag check-update",
|
|
"/dev/null", \$out, $errfile, undef, 1);
|
|
my $code = $ex >> 8;
|
|
my $err = &read_file_contents($errfile);
|
|
unlink($errfile);
|
|
if ($code != 0 && $code != 100) {
|
|
# The check failed, so return no updates and set the error from
|
|
# stderr, or from stdout or the exit status if stderr is empty
|
|
$err =~ s/\s+$//;
|
|
$out =~ s/\s+$//;
|
|
$update_system_error = $err || $out ||
|
|
"$yum_command check-update exited with status $code";
|
|
return ( );
|
|
}
|
|
# Parse the captured output with the shared loop below
|
|
open(PKG, "<", \$out);
|
|
}
|
|
else {
|
|
# YUM wraps long entries onto indented lines, so join them first.
|
|
# Failed YUM checks are not detected.
|
|
&open_execute_command(PKG, "$yum_command check-update 2>/dev/null | tr '\n' '#' | sed -e 's/# / /g' | tr '#' '\n'", 1, 1);
|
|
}
|
|
while(<PKG>) {
|
|
s/\r|\n//g;
|
|
if (/^(\S+)\.([^\.]+)\s+(\S+)\s+(\S+)/ && $2 ne 'src') {
|
|
local $pkg = { 'name' => $1,
|
|
'arch' => $2,
|
|
'version' => $3,
|
|
'source' => $4 };
|
|
if ($pkg->{'version'} =~ s/^(\S+)://) {
|
|
$pkg->{'epoch'} = $1;
|
|
}
|
|
$done{$pkg->{'name'}} = $pkg;
|
|
push(@rv, $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
|
|
{
|
|
local @rv;
|
|
local $sect;
|
|
open(CONF, "<".$yum_config);
|
|
while(<CONF>) {
|
|
s/\r|\n//g;
|
|
s/^\s*#.*$//;
|
|
if (/^\s*\[(.*)\]/) {
|
|
# Start of a section
|
|
$sect = { 'name' => $1,
|
|
'values' => { } };
|
|
push(@rv, $sect);
|
|
}
|
|
elsif (/^\s*(\S+)\s*=\s*(.*)/ && $sect) {
|
|
# Value in a section
|
|
$sect->{'values'}->{lc($1)} = $2;
|
|
}
|
|
}
|
|
close(CONF);
|
|
return \@rv;
|
|
}
|
|
|
|
# list_package_repos()
|
|
# Returns a list of configured repositories
|
|
sub list_package_repos
|
|
{
|
|
my @rv;
|
|
|
|
# Parse the raw repo files
|
|
my $repo;
|
|
foreach my $f (glob("$yum_repos_dir/*.repo")) {
|
|
my $lref = &read_file_lines($f, 1);
|
|
my $lnum = 0;
|
|
foreach my $l (@$lref) {
|
|
$l =~ s/#.*$//;
|
|
if ($l =~ /^\[(\S+)\]/) {
|
|
# Start of a new repo
|
|
$repo = { 'file' => $f,
|
|
'line' => $lnum,
|
|
'eline' => $lnum,
|
|
'id' => $1,
|
|
};
|
|
push(@rv, $repo);
|
|
}
|
|
elsif ($l =~ /^([^= ]+)=(.*)$/ && $repo) {
|
|
# Line in a repo
|
|
$repo->{'raw'}->{$1} = $2;
|
|
$repo->{'eline'} = $lnum;
|
|
}
|
|
$lnum++;
|
|
}
|
|
}
|
|
|
|
# Extract common information
|
|
foreach my $repo (@rv) {
|
|
my $name = $repo->{'raw'}->{'name'};
|
|
$name =~ s/\s*-.*//;
|
|
$name =~ s/\s*\$[a-z0-9]+//gi;
|
|
$repo->{'name'} = $repo->{'id'}." (".$name.")";
|
|
$repo->{'url'} = $repo->{'raw'}->{'baseurl'} ||
|
|
$repo->{'raw'}->{'mirrorlist'};
|
|
$repo->{'enabled'} = defined($repo->{'raw'}->{'enabled'}) ?
|
|
$repo->{'raw'}->{'enabled'} : 1;
|
|
}
|
|
|
|
return @rv;
|
|
}
|
|
|
|
# create_repo_form()
|
|
# Returns HTML for a package repository creation form
|
|
sub create_repo_form
|
|
{
|
|
my $rv;
|
|
$rv .= &ui_table_row($text{'yum_repo_id'},
|
|
&ui_textbox("id", undef, 20));
|
|
$rv .= &ui_table_row($text{'yum_repo_name'},
|
|
&ui_textbox("name", undef, 60));
|
|
$rv .= &ui_table_row($text{'yum_repo_url'},
|
|
&ui_textbox("url", undef, 60));
|
|
$rv .= &ui_table_row($text{'yum_repo_gpg'},
|
|
&ui_opt_textbox("gpg", undef, 60, $text{'yum_repo_none'}));
|
|
return $rv;
|
|
}
|
|
|
|
# create_repo_parse(&in)
|
|
# Parses input from create_repo_form, and returns either a new repo object or
|
|
# an error string
|
|
sub create_repo_parse
|
|
{
|
|
my ($in) = @_;
|
|
my $repo = { 'raw' => { 'enabled' => 1 } };
|
|
|
|
# ID must be valid and unique
|
|
$in->{'id'} =~ /^[a-z0-9\-\_]+$/i || return $text{'yum_repo_eid'};
|
|
my ($clash) = grep { $_->{'id'} eq $in->{'id'} } &list_package_repos();
|
|
$clash && return $text{'yum_repo_eidclash'};
|
|
$repo->{'id'} = $in->{'id'};
|
|
|
|
# Human-readable repo name
|
|
$in->{'name'} =~ /\S/ || return $text{'yum_repo_ename'};
|
|
$repo->{'raw'}->{'name'} = $in->{'name'};
|
|
|
|
# Base URL
|
|
$in->{'url'} =~ /^(http|https):/ || return $text{'yum_repo_eurl'};
|
|
$repo->{'raw'}->{'baseurl'} = $in->{'url'};
|
|
|
|
# GPG key file
|
|
if (!$in->{'gpg_def'}) {
|
|
-r $in->{'gpg'} || return $text{'yum_repo_egpg'};
|
|
$repo->{'raw'}->{'gpgcheck'} = 1;
|
|
$repo->{'raw'}->{'gpgkey'} = 'file://'.$in->{'gpg'};
|
|
}
|
|
|
|
return $repo;
|
|
}
|
|
|
|
# create_package_repo(&repo)
|
|
# Creates a new repository from the given hash (returned by create_repo_parse)
|
|
sub create_package_repo
|
|
{
|
|
my ($repo) = @_;
|
|
my $file = "$yum_repos_dir/$repo->{'id'}.repo";
|
|
-r $file && return $text{'yum_repo_efile'};
|
|
|
|
&lock_file($file);
|
|
my $lref = &read_file_lines($file);
|
|
push(@$lref, "[$repo->{'id'}]");
|
|
foreach my $r (keys %{$repo->{'raw'}}) {
|
|
push(@$lref, $r."=".$repo->{'raw'}->{$r});
|
|
}
|
|
&flush_file_lines($file);
|
|
&unlock_file($file);
|
|
|
|
return undef;
|
|
}
|
|
|
|
# delete_package_repo(&repo)
|
|
# Delete a repository from it's config file. Does not delete the file even if
|
|
# empty, to prevent it from being re-created if it came from an RPM package.
|
|
sub delete_package_repo
|
|
{
|
|
my ($repo) = @_;
|
|
&lock_file($repo->{'file'});
|
|
my $lref = &read_file_lines($repo->{'file'});
|
|
splice(@$lref, $repo->{'line'}, $repo->{'eline'}-$repo->{'line'}+1);
|
|
&flush_file_lines($repo->{'file'});
|
|
&unlock_file($repo->{'file'});
|
|
}
|
|
|
|
# enable_package_repo(&repo, enable?)
|
|
# Enable or disable a repository
|
|
sub enable_package_repo
|
|
{
|
|
my ($repo, $enable) = @_;
|
|
&lock_file($repo->{'file'});
|
|
my $lref = &read_file_lines($repo->{'file'});
|
|
my $e = "enabled=".($enable ? 1 : 0);
|
|
if (defined($repo->{'raw'}->{'enabled'})) {
|
|
# There's a line to update already
|
|
for(my $i=$repo->{'line'}; $i<=$repo->{'eline'}; $i++) {
|
|
if ($lref->[$i] =~ /^enabled=/) {
|
|
$lref->[$i] = $e;
|
|
last;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
# Need to add a line
|
|
splice(@$lref, $repo->{'eline'}, 0, $e);
|
|
}
|
|
&flush_file_lines($repo->{'file'});
|
|
&unlock_file($repo->{'file'});
|
|
}
|
|
|
|
1;
|