mirror of
https://github.com/webmin/webmin.git
synced 2026-08-24 15:50:32 +01:00
Compare commits
11 Commits
2.660
...
dev/dnf-ho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ac67d82a4 | ||
|
|
16ee9f455a | ||
|
|
86d3b95f3b | ||
|
|
f01d65a26e | ||
|
|
d16d9ea205 | ||
|
|
f7da47cd5e | ||
|
|
0660a18276 | ||
|
|
6183a4006a | ||
|
|
850cc1f498 | ||
|
|
523853963e | ||
|
|
3c26b2be3b |
@@ -1,4 +1,12 @@
|
|||||||
## Changelog
|
## Changelog
|
||||||
|
#### 2.661 (September, 2026)
|
||||||
|
* 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)
|
#### 2.660 (August 20, 2026)
|
||||||
* Add support for creating `vfsv1` Linux quota files for limits above 4 TiB, while preserving existing quota file formats
|
* Add support for creating `vfsv1` Linux quota files for limits above 4 TiB, while preserving existing quota file formats
|
||||||
* Add Btrfs subvolume quota management to the Disk Quotas module, with full and simple accounting modes
|
* Add Btrfs subvolume quota management to the Disk Quotas module, with full and simple accounting modes
|
||||||
|
|||||||
@@ -168,6 +168,31 @@ return $file if (!$local);
|
|||||||
return ©_vendor_config($file, $local);
|
return ©_vendor_config($file, $local);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ensure_writable_config_file(file)
|
||||||
|
# Makes a config file safe to edit, by creating the local override that
|
||||||
|
# shadows it when it belongs to the vendor tree. Callers must use this before
|
||||||
|
# saving directives, as save_directive refuses to write to a vendor file.
|
||||||
|
sub ensure_writable_config_file
|
||||||
|
{
|
||||||
|
my ($file) = @_;
|
||||||
|
if (&is_vendor_main_config($file)) {
|
||||||
|
return &ensure_local_main_config();
|
||||||
|
}
|
||||||
|
elsif (&is_vendor_config_file($file)) {
|
||||||
|
return &ensure_local_config_override($file);
|
||||||
|
}
|
||||||
|
elsif (my $vendor = &get_vendor_config_file($file)) {
|
||||||
|
# A local path that shadows a vendor file, but does not exist yet
|
||||||
|
return &ensure_local_config_override($vendor);
|
||||||
|
}
|
||||||
|
elsif ($file eq &get_config_parent()->{'file'} &&
|
||||||
|
&is_vendor_main_config(&get_main_config_file())) {
|
||||||
|
# New directives go to the main config, which is still the vendor one
|
||||||
|
return &ensure_local_main_config();
|
||||||
|
}
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
|
||||||
# list_config_dir_files(directory, [relative-subdirectory])
|
# list_config_dir_files(directory, [relative-subdirectory])
|
||||||
# Returns relative and absolute paths for regular files below a directory
|
# Returns relative and absolute paths for regular files below a directory
|
||||||
sub list_config_dir_files
|
sub list_config_dir_files
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
---- Changes since 2.660 ----
|
---- 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.
|
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 ----
|
---- Changes since 2.641 ----
|
||||||
Added a Held updates view with controls to hold, unhold or explicitly update APT-held packages.
|
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_confirm=Install Now
|
||||||
update_confirmheld=Update Held Packages
|
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_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_enoheldops=No update operation was found for the selected held packages. Refresh the package list and try again.
|
||||||
update_none=None
|
update_none=None
|
||||||
update_ops=Building complete list of packages ..
|
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.
|
# Returns true if the current update system can list and change package holds.
|
||||||
sub supports_package_holds
|
sub supports_package_holds
|
||||||
{
|
{
|
||||||
return defined(&software::list_update_system_holds) &&
|
return 0 if (!defined(&software::list_update_system_holds) ||
|
||||||
defined(&software::update_system_hold);
|
!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()
|
# list_package_holds()
|
||||||
@@ -389,8 +393,9 @@ my ($name, $system, $install, $flags) = @_;
|
|||||||
$system ||= $software::update_system;
|
$system ||= $software::update_system;
|
||||||
my @rv;
|
my @rv;
|
||||||
my $pkg;
|
my $pkg;
|
||||||
my $include_held = $system eq 'apt' && defined($flags) &&
|
my $include_held = $system eq $software::update_system &&
|
||||||
$flags eq '--allow-change-held-packages';
|
&supports_package_holds() && defined($flags) &&
|
||||||
|
$flags eq &software::update_system_hold_flags();
|
||||||
|
|
||||||
# First get from list of updates
|
# First get from list of updates
|
||||||
($pkg) = grep { $_->{'update'} eq $name &&
|
($pkg) = grep { $_->{'update'} eq $name &&
|
||||||
@@ -490,14 +495,13 @@ unlink($current_cache_file);
|
|||||||
return @rv;
|
return @rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
# list_package_operations(package|packages, system)
|
# list_package_operations(package|packages, system, [flags])
|
||||||
# Given a package (or space-separate package list), returns a list of all
|
# Returns packages and dependencies that would be installed or updated.
|
||||||
# dependencies that will be installed
|
|
||||||
sub list_package_operations
|
sub list_package_operations
|
||||||
{
|
{
|
||||||
my ($name, $system) = @_;
|
my ($name, $system, $flags) = @_;
|
||||||
if (defined(&software::update_system_operations)) {
|
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) {
|
foreach my $p (@rv) {
|
||||||
$p->{'system'} = $system;
|
$p->{'system'} = $system;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,18 +73,18 @@ else {
|
|||||||
@pkgs || &error($text{'update_enone'});
|
@pkgs || &error($text{'update_enone'});
|
||||||
$allow_held = 0;
|
$allow_held = 0;
|
||||||
if ($in{'mode'} eq 'held') {
|
if ($in{'mode'} eq 'held') {
|
||||||
# The held-updates page is the only UI that can explicitly
|
# Only Held updates can override a hold for one transaction.
|
||||||
# override an APT hold for a single update transaction.
|
|
||||||
&supports_package_holds() || &error($text{'hold_enotsupported'});
|
&supports_package_holds() || &error($text{'hold_enotsupported'});
|
||||||
@held = &list_package_holds();
|
@held = &list_package_holds();
|
||||||
foreach $ps (@pkgs) {
|
foreach $ps (@pkgs) {
|
||||||
($p, $s) = split(/\//, $ps, 2);
|
($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));
|
&error(&text('update_enotheld', $p));
|
||||||
}
|
}
|
||||||
$allow_held = 1;
|
$allow_held = 1;
|
||||||
}
|
}
|
||||||
$install_flags = $allow_held ? '--allow-change-held-packages' :
|
$install_flags = $allow_held ? &software::update_system_hold_flags() :
|
||||||
$in{'flags'};
|
$in{'flags'};
|
||||||
&ui_print_unbuffered_header(undef,
|
&ui_print_unbuffered_header(undef,
|
||||||
$in{'mode'} eq 'new' ? $text{'update_title2'} : $text{'update_title'}, "");
|
$in{'mode'} eq 'new' ? $text{'update_title2'} : $text{'update_title'}, "");
|
||||||
@@ -102,7 +102,8 @@ else {
|
|||||||
($p, $s) = split(/\//, $ps);
|
($p, $s) = split(/\//, $ps);
|
||||||
push(@pkgnames, $p);
|
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);
|
&error($text{'update_enoheldops'}) if (!@ops && $allow_held);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -986,6 +986,11 @@ return undef;
|
|||||||
sub setup_postgresql
|
sub setup_postgresql
|
||||||
{
|
{
|
||||||
return undef if (!$config{'setup_cmd'});
|
return undef if (!$config{'setup_cmd'});
|
||||||
|
# Use SCRAM defaults for new clusters when supported by the EL setup wrapper
|
||||||
|
local $ENV{'PGSETUP_INITDB_OPTIONS'} = '--auth-host=scram-sha-256'
|
||||||
|
if ($config{'setup_cmd'} =~ /\bpostgresql-setup\b/ &&
|
||||||
|
&get_postgresql_version(1) >= 10 &&
|
||||||
|
!$ENV{'PGSETUP_INITDB_OPTIONS'});
|
||||||
local $temp = &transname();
|
local $temp = &transname();
|
||||||
local $rv = &system_logged("($config{'setup_cmd'}) >$temp 2>&1");
|
local $rv = &system_logged("($config{'setup_cmd'}) >$temp 2>&1");
|
||||||
local $out = `cat $temp`;
|
local $out = `cat $temp`;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---- Changes since 2.641 ----
|
---- 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)
|
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 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 ----
|
---- Changes since 1.130 ----
|
||||||
Packages can now be installed directly from yum, if installed.
|
Packages can now be installed directly from yum, if installed.
|
||||||
The entire system can also be upgraded from yum.
|
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;
|
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])
|
# update_system_install([package], [&in], [no-force], [flags])
|
||||||
# Install some package with apt
|
# Install some package with apt
|
||||||
sub update_system_install
|
sub update_system_install
|
||||||
@@ -105,15 +112,17 @@ $? = $status;
|
|||||||
return @rv;
|
return @rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
# update_system_operations(packages)
|
# update_system_operations(packages, [flags])
|
||||||
# Given a list of packages, returns a list containing packages that will
|
# Returns packages APT would install or update. The optional hold flag includes
|
||||||
# actually get installed, each of which is a hash ref with name and version.
|
# held packages in the simulation.
|
||||||
sub update_system_operations
|
sub update_system_operations
|
||||||
{
|
{
|
||||||
my ($packages) = @_;
|
my ($packages, $flags) = @_;
|
||||||
$ENV{'UCF_FORCE_CONFFOLD'} = 'YES';
|
$ENV{'UCF_FORCE_CONFFOLD'} = 'YES';
|
||||||
$ENV{'DEBIAN_FRONTEND'} = 'noninteractive';
|
$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)).
|
join(" ", map { quotemeta($_) } split(/\s+/, $packages)).
|
||||||
" </dev/null 2>&1";
|
" </dev/null 2>&1";
|
||||||
&clean_language();
|
&clean_language();
|
||||||
@@ -261,8 +270,8 @@ close(DUMP);
|
|||||||
return @rv;
|
return @rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
# update_system_updates()
|
# update_system_updates([include-holds])
|
||||||
# Returns a list of available package updates
|
# Returns available package updates, optionally including held packages.
|
||||||
sub update_system_updates
|
sub update_system_updates
|
||||||
{
|
{
|
||||||
my ($include_holds) = @_;
|
my ($include_holds) = @_;
|
||||||
|
|||||||
@@ -266,6 +266,11 @@ yum_input=Package from YUM
|
|||||||
yum_install=Installing package(s) with command $1 ..
|
yum_install=Installing package(s) with command $1 ..
|
||||||
yum_ok=.. install complete
|
yum_ok=.. install complete
|
||||||
yum_failed=.. install failed!
|
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_find=Browse YUM ..
|
||||||
yum_package=Package
|
yum_package=Package
|
||||||
yum_version=Version
|
yum_version=Version
|
||||||
|
|||||||
@@ -19,17 +19,61 @@ sub list_update_system_commands
|
|||||||
return ($yum_command);
|
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])
|
# 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
|
sub update_system_install
|
||||||
{
|
{
|
||||||
local $update = $_[0] || $in{'update'};
|
local $update = $_[0] || $in{'update'};
|
||||||
local $in = $_[1];
|
local $in = $_[1];
|
||||||
local $force = !$_[2];
|
local $force = !$_[2];
|
||||||
local $flags = $_[3];
|
local $flags = $_[3];
|
||||||
|
local $versionlock_update = defined($flags) &&
|
||||||
|
$flags eq &update_system_hold_flags();
|
||||||
|
local $runflags = $versionlock_update ? undef : $flags;
|
||||||
local $qflags;
|
local $qflags;
|
||||||
$qflags = &trim(join(" ", map { quotemeta($_) } split(/ /, $flags)))
|
$qflags = &trim(join(" ", map { quotemeta($_) } split(/ /, $runflags)))
|
||||||
if ($flags);
|
if ($runflags);
|
||||||
$update =~ s/\.\*/\*/g;
|
$update =~ s/\.\*/\*/g;
|
||||||
local $enable;
|
local $enable;
|
||||||
if ($in->{'enablerepo'}) {
|
if ($in->{'enablerepo'}) {
|
||||||
@@ -45,7 +89,28 @@ if (@names == 1) {
|
|||||||
}
|
}
|
||||||
$update = join(" ", map { quotemeta($_) } @names);
|
$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;
|
local $cmd;
|
||||||
if ($yum_command =~ /dnf$/) {
|
if ($yum_command =~ /dnf$/) {
|
||||||
local @pinfo = &package_info($updates[0]);
|
local @pinfo = &package_info($updates[0]);
|
||||||
@@ -62,9 +127,9 @@ else {
|
|||||||
|
|
||||||
# Work out the command to run, which may enable some repos
|
# Work out the command to run, which may enable some repos
|
||||||
my $uicmd = "$yum_command $enable -y $cmd ".join(" ", @names);
|
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";
|
my $fullcmd = "$yum_command $enable -y $cmd $update";
|
||||||
$fullcmd .= " $qflags" if ($flags);
|
$fullcmd .= " $qflags" if ($qflags);
|
||||||
foreach my $u (@updates) {
|
foreach my $u (@updates) {
|
||||||
my $repo = &update_system_repo($u);
|
my $repo = &update_system_repo($u);
|
||||||
if ($repo) {
|
if ($repo) {
|
||||||
@@ -110,7 +175,7 @@ while(<CMD>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
elsif (/^\s+(Updating|Installing|Upgrading)\s+:\s+(\S+)/) {
|
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
|
# Updating : wbt-virtual-server-theme 1/2
|
||||||
# or
|
# or
|
||||||
# Installing : 2:nmap-5.51-2.el6.i686 1/1
|
# Installing : 2:nmap-5.51-2.el6.i686 1/1
|
||||||
@@ -119,12 +184,11 @@ while(<CMD>) {
|
|||||||
$pkg =~ s/\-\d.*$//; # Strip version number from end
|
$pkg =~ s/\-\d.*$//; # Strip version number from end
|
||||||
push(@rv, $pkg);
|
push(@rv, $pkg);
|
||||||
}
|
}
|
||||||
elsif (/\]\s+(Upgrading|Installing)\s+(\S+)/) {
|
elsif (/\]\s+(Upgrading|Installing|Downgrading|Reinstalling)\s+(\S+)/) {
|
||||||
# Line like :
|
# DNF 5 progress line, for example:
|
||||||
# [3/8] Upgrading libcurl-0:8.11.1-5.fc42 100% ...
|
# [3/8] Upgrading libcurl-0:8.11.1-5.fc42 100% ...
|
||||||
local $pkg = $2;
|
local $pkg = &update_system_nevra_name($2);
|
||||||
$pkg =~ s/:\d.*$//; # Strip version number from end
|
push(@rv, $pkg) if ($pkg);
|
||||||
push(@rv, $pkg);
|
|
||||||
}
|
}
|
||||||
if (!/ETA/ && !/\%\s+done\s+\d+\/\d+\s*$/) {
|
if (!/ETA/ && !/\%\s+done\s+\d+\/\d+\s*$/) {
|
||||||
print &html_escape($_."\n");
|
print &html_escape($_."\n");
|
||||||
@@ -134,8 +198,19 @@ while(<CMD>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(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";
|
print "</pre>\n";
|
||||||
if ($? || $nopackage) {
|
if ($status || $nopackage) {
|
||||||
print "$text{'yum_failed'}<p>\n";
|
print "$text{'yum_failed'}<p>\n";
|
||||||
return ( );
|
return ( );
|
||||||
}
|
}
|
||||||
@@ -167,12 +242,26 @@ for(my $i=0; $i<$n; $i++) {
|
|||||||
return @rv;
|
return @rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
# update_system_operations(packages)
|
# update_system_nevra_name(nevra)
|
||||||
# Given a list of packages, returns a list containing packages that will
|
# Extracts the package name from a NEVRA string printed by DNF.
|
||||||
# actually get installed, each of which is a hash ref with name and version.
|
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
|
sub update_system_operations
|
||||||
{
|
{
|
||||||
my ($packages) = @_;
|
my ($packages, $flags) = @_;
|
||||||
|
if ($yum_command =~ /(?:^|\/)dnf(?:-\d+)?$/) {
|
||||||
|
return &update_system_dnf_operations($packages, $flags);
|
||||||
|
}
|
||||||
my $temp = &transname();
|
my $temp = &transname();
|
||||||
&open_tempfile(SHELL, ">$temp", 0, 1);
|
&open_tempfile(SHELL, ">$temp", 0, 1);
|
||||||
&print_tempfile(SHELL, "install $packages\n");
|
&print_tempfile(SHELL, "install $packages\n");
|
||||||
@@ -198,6 +287,87 @@ close(SHELL);
|
|||||||
return @rv;
|
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()
|
# show_update_system_opts()
|
||||||
# Returns HTML for enabling a repository, if any are disabled
|
# Returns HTML for enabling a repository, if any are disabled
|
||||||
sub show_update_system_opts
|
sub show_update_system_opts
|
||||||
@@ -333,14 +503,19 @@ while(<PKG>) {
|
|||||||
close(PKG);
|
close(PKG);
|
||||||
}
|
}
|
||||||
|
|
||||||
# update_system_updates()
|
# update_system_updates([include-holds])
|
||||||
# Returns a list of package updates available from yum
|
# Returns available package updates, optionally including version-locked ones.
|
||||||
sub update_system_updates
|
sub update_system_updates
|
||||||
{
|
{
|
||||||
|
my ($include_holds) = @_;
|
||||||
local @rv;
|
local @rv;
|
||||||
local %done;
|
local %done;
|
||||||
if ($yum_command =~ /dnf/) {
|
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 {
|
else {
|
||||||
&open_execute_command(PKG, "$yum_command check-update 2>/dev/null | tr '\n' '#' | sed -e 's/# / /g' | tr '#' '\n'", 1, 1);
|
&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);
|
last if (/Obsoleting\s+Packages/i);
|
||||||
}
|
}
|
||||||
close(PKG);
|
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);
|
&set_yum_security_field(\%done);
|
||||||
return @rv;
|
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()
|
# get_yum_config()
|
||||||
# Returns entries from the YUM config file, as a list of hash references
|
# Returns entries from the YUM config file, as a list of hash references
|
||||||
sub get_yum_config
|
sub get_yum_config
|
||||||
@@ -543,4 +865,3 @@ else {
|
|||||||
}
|
}
|
||||||
|
|
||||||
1;
|
1;
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ is(strip_apt_package_arch('ncurses-base'), 'ncurses-base',
|
|||||||
|
|
||||||
{
|
{
|
||||||
no warnings qw(once redefine);
|
no warnings qw(once redefine);
|
||||||
|
my $command;
|
||||||
local *backquote_command = sub {
|
local *backquote_command = sub {
|
||||||
|
$command = $_[0];
|
||||||
return "Inst libtinfo6:amd64 [6.3-2ubuntu0.1] ".
|
return "Inst libtinfo6:amd64 [6.3-2ubuntu0.1] ".
|
||||||
"(6.3-2ubuntu0.2 Ubuntu:22.04/jammy-updates [amd64])\n";
|
"(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');
|
my @ops = update_system_operations('libtinfo6');
|
||||||
is($ops[0]->{'name'}, 'libtinfo6',
|
is($ops[0]->{'name'}, 'libtinfo6',
|
||||||
'normalizes package names from simulated APT operations');
|
'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();
|
||||||
@@ -43,6 +43,6 @@ $o->{'bootup'} = $in{'bootup'};
|
|||||||
|
|
||||||
sub get_icons
|
sub get_icons
|
||||||
{
|
{
|
||||||
return ( "access" ,"bind" ,"ui" ,"umods" ,"os" ,"lang" ,"upgrade" ,"session" ,"assignment" ,"categories" ,"themes", "referers", "anon", "ssl" ,"configs" ,"acl" ,"restrict" ,"users" ,"defacl", "sessions", "blocked", "advanced" );
|
return ( "access" ,"bind" ,"log" ,"ui" ,"umods" ,"os" ,"lang" ,"upgrade" ,"session" ,"assignment" ,"categories" ,"themes", "referers", "anon", "ssl" ,"configs" ,"acl" ,"restrict" ,"users" ,"defacl", "sessions", "blocked", "advanced" );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
65
usermin/change_log.cgi
Executable file
65
usermin/change_log.cgi
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/local/bin/perl
|
||||||
|
# Save Usermin webserver logging options
|
||||||
|
|
||||||
|
require './usermin-lib.pl';
|
||||||
|
&ReadParse();
|
||||||
|
$access{'log'} || &error($text{'acl_ecannot'});
|
||||||
|
&error_setup($text{'log_err'});
|
||||||
|
&get_usermin_miniserv_config(\%miniserv);
|
||||||
|
|
||||||
|
# Only a change of error destination on a systemd-managed service needs the
|
||||||
|
# drop-in and a full restart; otherwise the option is ignored.
|
||||||
|
my $journal_changed = defined($in{'error_journal'}) &&
|
||||||
|
&webmin::miniserv_systemd_journal_available("usermin.service") &&
|
||||||
|
($miniserv{'errorlog'} eq '-' ? 1 : 0) != ($in{'error_journal'} ? 1 : 0);
|
||||||
|
|
||||||
|
# Either Miniserv clears the logs itself, or logrotate takes them over. There
|
||||||
|
# is no setting for the latter, so it is on when clearing is off and a section
|
||||||
|
# already rotates the logs.
|
||||||
|
my $logrotate = $in{'logclear'} == 2 ? 1 : 0;
|
||||||
|
!$logrotate || &webmin::miniserv_logrotate_available() ||
|
||||||
|
&error($text{'log_elogrotate'});
|
||||||
|
my $was_logrotate = !int($miniserv{'logclear'}) &&
|
||||||
|
&webmin::miniserv_logrotate_available() &&
|
||||||
|
&webmin::get_miniserv_logrotate_section(\%miniserv) ? 1 : 0;
|
||||||
|
|
||||||
|
# Validate and save the access-log settings.
|
||||||
|
$miniserv{'log'} = $in{'log'};
|
||||||
|
$miniserv{'loghost'} = $in{'loghost'};
|
||||||
|
$miniserv{'logtrust'} = $in{'logtrust'};
|
||||||
|
$miniserv{'logclf'} = $in{'logclf'};
|
||||||
|
$miniserv{'logclear'} = $logrotate ? 0 : $in{'logclear'};
|
||||||
|
!$miniserv{'logclear'} || $in{'logtime'} =~ /^[1-9][0-9]*$/ ||
|
||||||
|
&error(&text('log_ehours', $in{'logtime'}));
|
||||||
|
$miniserv{'logtime'} = $in{'logtime'};
|
||||||
|
|
||||||
|
# Save the error destination and matching systemd drop-in when supported.
|
||||||
|
&lock_file($usermin_miniserv_config);
|
||||||
|
if ($journal_changed) {
|
||||||
|
&webmin::set_miniserv_error_destination(\%miniserv,
|
||||||
|
$usermin_miniserv_config, "usermin.service",
|
||||||
|
$in{'error_journal'});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
&put_usermin_miniserv_config(\%miniserv);
|
||||||
|
}
|
||||||
|
&unlock_file($usermin_miniserv_config);
|
||||||
|
|
||||||
|
# Hand the Miniserv logs to logrotate, or take them back from it
|
||||||
|
if ($logrotate) {
|
||||||
|
&webmin::setup_miniserv_logrotate(\%miniserv, "usermin");
|
||||||
|
}
|
||||||
|
elsif ($was_logrotate) {
|
||||||
|
&webmin::remove_miniserv_logrotate(\%miniserv);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restart through systemd when stderr needs to be re-attached.
|
||||||
|
if ($journal_changed) {
|
||||||
|
&webmin::restart_miniserv_systemd_service("usermin.service", 2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
&restart_usermin_miniserv();
|
||||||
|
}
|
||||||
|
|
||||||
|
&webmin_log("log");
|
||||||
|
&redirect("");
|
||||||
@@ -29,3 +29,4 @@ advanced=1
|
|||||||
mobile=1
|
mobile=1
|
||||||
twofactor=1
|
twofactor=1
|
||||||
web=1
|
web=1
|
||||||
|
log=1
|
||||||
|
|||||||
46
usermin/edit_log.cgi
Executable file
46
usermin/edit_log.cgi
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/local/bin/perl
|
||||||
|
# Display Usermin webserver logging options
|
||||||
|
|
||||||
|
require './usermin-lib.pl';
|
||||||
|
$access{'log'} || &error($text{'acl_ecannot'});
|
||||||
|
&ui_print_header(undef, $text{'log_title'}, "");
|
||||||
|
&get_usermin_miniserv_config(\%miniserv);
|
||||||
|
|
||||||
|
print &text('log_desc', "<tt>$miniserv{'logfile'}</tt>"),"<p>\n";
|
||||||
|
print &ui_form_start("change_log.cgi", "post");
|
||||||
|
print &ui_table_start($text{'log_header'}, undef, 2);
|
||||||
|
|
||||||
|
# Control the Usermin access log and its built-in expiry mechanism.
|
||||||
|
print &ui_table_row($text{'log_status'},
|
||||||
|
&ui_radio("log", $miniserv{'log'} ? 1 : 0,
|
||||||
|
[ [ 1, $text{'log_enable'} ],
|
||||||
|
[ 0, $text{'log_disable'} ] ]));
|
||||||
|
print &ui_table_row($text{'log_resolv'},
|
||||||
|
&ui_yesno_radio("loghost", int($miniserv{'loghost'})));
|
||||||
|
print &ui_table_row($text{'log_trust'},
|
||||||
|
&ui_yesno_radio("logtrust", int($miniserv{'logtrust'})));
|
||||||
|
print &ui_table_row($text{'log_clf'},
|
||||||
|
&ui_yesno_radio("logclf", int($miniserv{'logclf'})));
|
||||||
|
my @clear_opts = ( [ 1, &text('log_period',
|
||||||
|
&ui_textbox("logtime", $miniserv{'logtime'}, 10)) ] );
|
||||||
|
my $logclear = int($miniserv{'logclear'});
|
||||||
|
if (&webmin::miniserv_logrotate_available()) {
|
||||||
|
push(@clear_opts, [ 2, $text{'log_logrotate'} ]);
|
||||||
|
$logclear = 2 if (!$logclear &&
|
||||||
|
&webmin::get_miniserv_logrotate_section(\%miniserv));
|
||||||
|
}
|
||||||
|
push(@clear_opts, [ 0, $text{'no'} ]);
|
||||||
|
print &ui_table_row($text{'log_clear'},
|
||||||
|
&ui_radio("logclear", $logclear, \@clear_opts));
|
||||||
|
|
||||||
|
# Only systemd services can safely inherit stderr into the journal.
|
||||||
|
if (&webmin::miniserv_systemd_journal_available("usermin.service")) {
|
||||||
|
print &ui_table_row($text{'log_error'},
|
||||||
|
&ui_radio("error_journal", $miniserv{'errorlog'} eq '-' ? 1 : 0,
|
||||||
|
[ [ 0, $text{'log_error_file'} ],
|
||||||
|
[ 1, $text{'log_error_journal'} ] ]));
|
||||||
|
}
|
||||||
|
|
||||||
|
print &ui_table_end();
|
||||||
|
print &ui_form_end([ [ "save", $text{'save'} ] ]);
|
||||||
|
&ui_print_footer("", $text{'index_return'});
|
||||||
BIN
usermin/images/log.gif
Normal file
BIN
usermin/images/log.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 470 B |
@@ -55,6 +55,7 @@ if (!$miniserv{'root'} && !-d $miniserv{'root'}) {
|
|||||||
|
|
||||||
@links = ( "edit_access.cgi",
|
@links = ( "edit_access.cgi",
|
||||||
"edit_bind.cgi",
|
"edit_bind.cgi",
|
||||||
|
"edit_log.cgi",
|
||||||
"edit_ui.cgi",
|
"edit_ui.cgi",
|
||||||
"edit_mods.cgi",
|
"edit_mods.cgi",
|
||||||
"edit_os.cgi",
|
"edit_os.cgi",
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
index_title=Usermin Configuration
|
index_title=Usermin Configuration
|
||||||
|
log_title=Logging
|
||||||
|
log_desc=Usermin can be configured to write a log of web server hits, in the standard CLF log file format. If logging is enabled, you can also choose whether IP addresses or hostnames are recorded, and how often the log file is cleared. When enabled, logs are written to the file $1.
|
||||||
|
log_header=Webserver logging options
|
||||||
|
log_status=Logging active?
|
||||||
|
log_enable=Enable logging
|
||||||
|
log_disable=Disable logging
|
||||||
|
log_resolv=Log resolved hostnames?
|
||||||
|
log_trust=Log client IP address when behind proxy?
|
||||||
|
log_clf=Use combined log format (including referrer and user agent)?
|
||||||
|
log_clear=Periodically clear log files?
|
||||||
|
log_period=Yes, every $1 hours
|
||||||
|
log_logrotate=Yes, rotate using <tt>logrotate</tt>
|
||||||
|
log_elogrotate=The <tt>logrotate</tt> configuration is not available on this system
|
||||||
|
log_error=Error log destination
|
||||||
|
log_error_file=Write to <tt>miniserv.error</tt>
|
||||||
|
log_error_journal=Write to the <tt>systemd journal</tt>
|
||||||
|
log_err=Failed to save logging options
|
||||||
|
log_ehours='$1' is not a valid number of hours
|
||||||
index_econfig=The directory $1 either does not exist on your system, or is not the Usermin configuration directory. Maybe Usermin is not installed, or your <a href='$2'>module configuration</a> is incorrect.
|
index_econfig=The directory $1 either does not exist on your system, or is not the Usermin configuration directory. Maybe Usermin is not installed, or your <a href='$2'>module configuration</a> is incorrect.
|
||||||
index_return=Usermin configuration
|
index_return=Usermin configuration
|
||||||
index_stop=Stop Usermin
|
index_stop=Stop Usermin
|
||||||
|
|||||||
@@ -4859,11 +4859,25 @@ return if ($nocache == 2);
|
|||||||
if (!$system_hostname[$m]) {
|
if (!$system_hostname[$m]) {
|
||||||
if ($gconfig{'os_type'} ne 'windows') {
|
if ($gconfig{'os_type'} ne 'windows') {
|
||||||
# If systemd system try /etc/hostname straight away
|
# If systemd system try /etc/hostname straight away
|
||||||
my $initsys = &trim(&backquote_command("cat /proc/1/comm 2>/dev/null"));
|
if (-d "/run/systemd/system") {
|
||||||
if ($initsys eq 'systemd') {
|
|
||||||
my $hostname = &read_file_contents("/etc/hostname");
|
my $hostname = &read_file_contents("/etc/hostname");
|
||||||
$hostname =~ s/\r|\n//g;
|
$hostname =~ s/\r|\n//g;
|
||||||
|
# Cache the short name before resolving the full name
|
||||||
if ($hostname) {
|
if ($hostname) {
|
||||||
|
my $shortname = $hostname;
|
||||||
|
$shortname =~ s/\..*$//;
|
||||||
|
$system_hostname[1] = $shortname;
|
||||||
|
}
|
||||||
|
# Resolve a short static hostname before caching it
|
||||||
|
if ($hostname && !$m && $hostname !~ /\./ &&
|
||||||
|
!$gconfig{'no_hostname_f'}) {
|
||||||
|
my $fqdn;
|
||||||
|
my $ex = &execute_command("hostname -f", undef,
|
||||||
|
\$fqdn, undef, 0, 1);
|
||||||
|
$fqdn = &trim($fqdn);
|
||||||
|
$hostname = $fqdn if (!$ex && $fqdn =~ /\./);
|
||||||
|
}
|
||||||
|
if ($hostname && ($m || $hostname =~ /\./)) {
|
||||||
$hostname =~ s/\..*$// if ($m);
|
$hostname =~ s/\..*$// if ($m);
|
||||||
$system_hostname[$m] = $hostname;
|
$system_hostname[$m] = $hostname;
|
||||||
return $hostname;
|
return $hostname;
|
||||||
@@ -7607,6 +7621,14 @@ my ($param_action,
|
|||||||
|
|
||||||
my $m = $param_module ? $param_module : &get_module_name();
|
my $m = $param_module ? $param_module : &get_module_name();
|
||||||
|
|
||||||
|
# When logrotate has emptied the actions log, throw away the file changes
|
||||||
|
# and annotations that belonged to the rotated entries
|
||||||
|
if ($gconfig{'logrotate'} && -e $webmin_logfile && !-s _) {
|
||||||
|
&unlink_file("$ENV{'WEBMIN_VAR'}/diffs");
|
||||||
|
&unlink_file("$ENV{'WEBMIN_VAR'}/files");
|
||||||
|
&unlink_file("$ENV{'WEBMIN_VAR'}/annotations");
|
||||||
|
}
|
||||||
|
|
||||||
if ($gconfig{'logclear'}) {
|
if ($gconfig{'logclear'}) {
|
||||||
# check if it is time to clear the log
|
# check if it is time to clear the log
|
||||||
my @st = stat("$webmin_logfile.time");
|
my @st = stat("$webmin_logfile.time");
|
||||||
|
|||||||
@@ -9,12 +9,29 @@ require './webmin-lib.pl';
|
|||||||
&lock_file($ENV{'MINISERV_CONFIG'});
|
&lock_file($ENV{'MINISERV_CONFIG'});
|
||||||
&get_miniserv_config(\%miniserv);
|
&get_miniserv_config(\%miniserv);
|
||||||
my ($miniserv_log, $in_log) = ($miniserv{'log'}, $in{'log'});
|
my ($miniserv_log, $in_log) = ($miniserv{'log'}, $in{'log'});
|
||||||
|
|
||||||
|
# Only a change of error destination on a systemd-managed service needs the
|
||||||
|
# drop-in and a full restart; otherwise the option is ignored.
|
||||||
|
my $journal_changed = defined($in{'error_journal'}) &&
|
||||||
|
&miniserv_systemd_journal_available("webmin.service") &&
|
||||||
|
($miniserv{'errorlog'} eq '-' ? 1 : 0) != ($in{'error_journal'} ? 1 : 0);
|
||||||
|
|
||||||
$miniserv{'log'} = $in{'log'};
|
$miniserv{'log'} = $in{'log'};
|
||||||
$miniserv{'loghost'} = $in{'loghost'};
|
$miniserv{'loghost'} = $in{'loghost'};
|
||||||
$miniserv{'logtrust'} = $in{'logtrust'};
|
$miniserv{'logtrust'} = $in{'logtrust'};
|
||||||
$miniserv{'logclf'} = $in{'logclf'};
|
$miniserv{'logclf'} = $in{'logclf'};
|
||||||
$miniserv{'logclear'} = $in{'logclear'};
|
|
||||||
!$in{'logclear'} || $in{'logtime'} =~ /^[1-9][0-9]*$/ ||
|
# Either Miniserv clears the logs itself, or logrotate takes them over. There
|
||||||
|
# is no setting for the latter, so it is on when clearing is off and a section
|
||||||
|
# already rotates the logs.
|
||||||
|
my $logrotate = $in{'logclear'} == 2 ? 1 : 0;
|
||||||
|
!$logrotate || &miniserv_logrotate_available() ||
|
||||||
|
&error($text{'log_elogrotate'});
|
||||||
|
my $was_logrotate = !int($miniserv{'logclear'}) &&
|
||||||
|
&miniserv_logrotate_available() &&
|
||||||
|
&get_miniserv_logrotate_section(\%miniserv) ? 1 : 0;
|
||||||
|
$miniserv{'logclear'} = $logrotate ? 0 : $in{'logclear'};
|
||||||
|
!$miniserv{'logclear'} || $in{'logtime'} =~ /^[1-9][0-9]*$/ ||
|
||||||
&error(&text('log_ehours', $in{'logtime'}));
|
&error(&text('log_ehours', $in{'logtime'}));
|
||||||
$miniserv{'logtime'} = $in{'logtime'};
|
$miniserv{'logtime'} = $in{'logtime'};
|
||||||
if ($in{'perms_def'}) {
|
if ($in{'perms_def'}) {
|
||||||
@@ -42,12 +59,29 @@ if (defined($in{'login'})) {
|
|||||||
delete($miniserv{'failed_script'});
|
delete($miniserv{'failed_script'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&put_miniserv_config(\%miniserv);
|
# Save the error destination and matching systemd drop-in when supported.
|
||||||
|
if ($journal_changed) {
|
||||||
|
&set_miniserv_error_destination(\%miniserv,
|
||||||
|
$ENV{'MINISERV_CONFIG'}, "webmin.service",
|
||||||
|
$in{'error_journal'});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
&put_miniserv_config(\%miniserv);
|
||||||
|
}
|
||||||
&unlock_file($ENV{'MINISERV_CONFIG'});
|
&unlock_file($ENV{'MINISERV_CONFIG'});
|
||||||
|
|
||||||
|
# Hand the Miniserv and actions logs to logrotate, or take them back from it
|
||||||
|
if ($logrotate) {
|
||||||
|
&setup_miniserv_logrotate(\%miniserv, "webmin", [ $webmin_logfile ]);
|
||||||
|
}
|
||||||
|
elsif ($was_logrotate) {
|
||||||
|
&remove_miniserv_logrotate(\%miniserv, [ $webmin_logfile ]);
|
||||||
|
}
|
||||||
|
|
||||||
$gconfig{'log'} = $in{'log'};
|
$gconfig{'log'} = $in{'log'};
|
||||||
$gconfig{'logtime'} = $in{'logtime'};
|
$gconfig{'logtime'} = $in{'logtime'};
|
||||||
$gconfig{'logclear'} = $in{'logclear'};
|
$gconfig{'logclear'} = $miniserv{'logclear'};
|
||||||
|
$gconfig{'logrotate'} = $logrotate;
|
||||||
$gconfig{'logusers'} =
|
$gconfig{'logusers'} =
|
||||||
$in{'uall'} ? '' : join(" ", split(/\0/, $in{'users'}));
|
$in{'uall'} ? '' : join(" ", split(/\0/, $in{'users'}));
|
||||||
$gconfig{'logmodules'} =
|
$gconfig{'logmodules'} =
|
||||||
@@ -71,6 +105,12 @@ if ($miniserv_log != $in_log) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&show_restart_page();
|
# Restart through systemd when stderr needs to be re-attached.
|
||||||
|
if ($journal_changed) {
|
||||||
|
&restart_miniserv_systemd_service("webmin.service", 2);
|
||||||
|
&redirect("");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
&show_restart_page();
|
||||||
|
}
|
||||||
&webmin_log("log", undef, undef, \%in);
|
&webmin_log("log", undef, undef, \%in);
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,26 @@ print &ui_table_row($text{'log_trust'},
|
|||||||
print &ui_table_row($text{'log_clf'},
|
print &ui_table_row($text{'log_clf'},
|
||||||
&ui_yesno_radio("logclf", int($miniserv{'logclf'})));
|
&ui_yesno_radio("logclf", int($miniserv{'logclf'})));
|
||||||
|
|
||||||
# Clear logs regularly
|
# Clear logs regularly, or hand them over to logrotate when available
|
||||||
|
my @clear_opts = ( [ 1, &text('log_period',
|
||||||
|
&ui_textbox("logtime", $miniserv{'logtime'}, 10)) ] );
|
||||||
|
my $logclear = int($miniserv{'logclear'});
|
||||||
|
if (&miniserv_logrotate_available()) {
|
||||||
|
push(@clear_opts, [ 2, $text{'log_logrotate'} ]);
|
||||||
|
$logclear = 2 if (!$logclear &&
|
||||||
|
&get_miniserv_logrotate_section(\%miniserv));
|
||||||
|
}
|
||||||
|
push(@clear_opts, [ 0, $text{'no'} ]);
|
||||||
print &ui_table_row($text{'log_clear2'},
|
print &ui_table_row($text{'log_clear2'},
|
||||||
&ui_radio("logclear", int($miniserv{'logclear'}),
|
&ui_radio("logclear", $logclear, \@clear_opts));
|
||||||
[ [ 1, &text('log_period',
|
|
||||||
&ui_textbox("logtime", $miniserv{'logtime'}, 10)) ],
|
# A systemd service can keep Miniserv errors in the journal instead.
|
||||||
[ 0, $text{'no'} ] ]));
|
if (&miniserv_systemd_journal_available("webmin.service")) {
|
||||||
|
print &ui_table_row($text{'log_error'},
|
||||||
|
&ui_radio("error_journal", $miniserv{'errorlog'} eq '-' ? 1 : 0,
|
||||||
|
[ [ 0, $text{'log_error_file'} ],
|
||||||
|
[ 1, $text{'log_error_journal'} ] ]));
|
||||||
|
}
|
||||||
|
|
||||||
print &ui_table_hr();
|
print &ui_table_hr();
|
||||||
|
|
||||||
@@ -101,4 +115,3 @@ print &ui_table_end();
|
|||||||
print &ui_form_end([ [ "save", $text{'save'} ] ]);
|
print &ui_form_end([ [ "save", $text{'save'} ] ]);
|
||||||
|
|
||||||
&ui_print_footer("", $text{'index_return'});
|
&ui_print_footer("", $text{'index_return'});
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ log_trust=Log client IP address when behind proxy?
|
|||||||
log_clf=Use combined log format (including referrer and user agent)?
|
log_clf=Use combined log format (including referrer and user agent)?
|
||||||
log_clear2=Periodically clear log files?
|
log_clear2=Periodically clear log files?
|
||||||
log_period=Yes, every $1 hours
|
log_period=Yes, every $1 hours
|
||||||
|
log_logrotate=Yes, rotate using <tt>logrotate</tt>
|
||||||
|
log_elogrotate=The <tt>logrotate</tt> configuration is not available on this system
|
||||||
|
log_error=Error log destination
|
||||||
|
log_error_file=Write to <tt>miniserv.error</tt>
|
||||||
|
log_error_journal=Write to the <tt>systemd journal</tt>
|
||||||
log_forusers=Users to log Webmin actions for
|
log_forusers=Users to log Webmin actions for
|
||||||
log_uall=Log actions by all users
|
log_uall=Log actions by all users
|
||||||
log_users=Only log actions by ..
|
log_users=Only log actions by ..
|
||||||
|
|||||||
@@ -43,6 +43,211 @@ if (!-r $update_cache) {
|
|||||||
|
|
||||||
our $primary_host = "webmin.com";
|
our $primary_host = "webmin.com";
|
||||||
our $primary_ssl = $can_http_ssl;
|
our $primary_ssl = $can_http_ssl;
|
||||||
|
|
||||||
|
=head2 miniserv_systemd_journal_available(unit)
|
||||||
|
|
||||||
|
Returns 1 if a systemd service is available for receiving Miniserv errors.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
sub miniserv_systemd_journal_available
|
||||||
|
{
|
||||||
|
my ($unit) = @_;
|
||||||
|
return &has_command("systemctl") &&
|
||||||
|
(-r "/etc/systemd/system/$unit" ||
|
||||||
|
-r "/usr/lib/systemd/system/$unit" ||
|
||||||
|
-r "/lib/systemd/system/$unit");
|
||||||
|
}
|
||||||
|
|
||||||
|
=head2 set_miniserv_error_destination(&miniserv, config, unit, journal)
|
||||||
|
|
||||||
|
Selects the Miniserv error file or the systemd journal, using a local systemd
|
||||||
|
drop-in so that the packaged service unit remains unchanged.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
sub set_miniserv_error_destination
|
||||||
|
{
|
||||||
|
my ($miniserv, $config_file, $unit, $journal) = @_;
|
||||||
|
$unit =~ /^(webmin|usermin)\.service$/ || return 0;
|
||||||
|
my $dropin_dir = "/etc/systemd/system/$unit.d";
|
||||||
|
my $dropin = "$dropin_dir/10-miniserv-error-log.conf";
|
||||||
|
|
||||||
|
# The dash keeps Miniserv's stderr attached to the service manager.
|
||||||
|
if ($journal) {
|
||||||
|
$miniserv->{'errorlog'} = '-';
|
||||||
|
&make_dir($dropin_dir, 0755) if (!-d $dropin_dir);
|
||||||
|
&write_file_contents($dropin,
|
||||||
|
"[Service]\nStandardError=journal\n");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
my $logfile = $miniserv->{'logfile'};
|
||||||
|
$logfile =~ s![^/]+$!miniserv.error!;
|
||||||
|
$miniserv->{'errorlog'} = $logfile;
|
||||||
|
&unlink_file($dropin) if (-e $dropin);
|
||||||
|
# Take the directory too, unless other drop-ins are still using it
|
||||||
|
rmdir($dropin_dir) if (-d $dropin_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply the Miniserv and service-manager settings as one UI operation.
|
||||||
|
&write_file($config_file, $miniserv);
|
||||||
|
&system_logged("systemctl daemon-reload >/dev/null 2>&1");
|
||||||
|
}
|
||||||
|
|
||||||
|
=head2 restart_miniserv_systemd_service(unit, [delay])
|
||||||
|
|
||||||
|
Fully restarts a Miniserv systemd service so that stderr is re-attached to the
|
||||||
|
selected destination. A delay allows the current Webmin response to finish.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
sub restart_miniserv_systemd_service
|
||||||
|
{
|
||||||
|
my ($unit, $delay) = @_;
|
||||||
|
$unit =~ /^(webmin|usermin)\.service$/ || return 0;
|
||||||
|
$delay = int($delay);
|
||||||
|
if ($delay) {
|
||||||
|
&system_logged("(sleep $delay; systemctl --no-block restart $unit) " .
|
||||||
|
">/dev/null 2>&1 </dev/null &");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
&system_logged("systemctl restart $unit >/dev/null 2>&1 </dev/null");
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
=head2 miniserv_logrotate_available()
|
||||||
|
|
||||||
|
Returns 1 if logrotate is installed and can be configured from Webmin.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
sub miniserv_logrotate_available
|
||||||
|
{
|
||||||
|
return &foreign_available("logrotate") && &foreign_installed("logrotate")
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
=head2 miniserv_log_files(&miniserv)
|
||||||
|
|
||||||
|
Returns the Miniserv access and error log paths that rotation applies to.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
sub miniserv_log_files
|
||||||
|
{
|
||||||
|
my ($miniserv) = @_;
|
||||||
|
my $errorlog = $miniserv->{'logfile'};
|
||||||
|
$errorlog =~ s![^/]+$!miniserv.error!;
|
||||||
|
$errorlog = $miniserv->{'errorlog'} if ($miniserv->{'errorlog'} =~ /^\//);
|
||||||
|
return &unique($miniserv->{'logfile'}, $errorlog);
|
||||||
|
}
|
||||||
|
|
||||||
|
=head2 get_miniserv_logrotate_section(&miniserv)
|
||||||
|
|
||||||
|
Returns the logrotate section that covers the Miniserv access log, if any.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
sub get_miniserv_logrotate_section
|
||||||
|
{
|
||||||
|
my ($miniserv) = @_;
|
||||||
|
&foreign_require("logrotate");
|
||||||
|
my ($logfile) = &miniserv_log_files($miniserv);
|
||||||
|
# Global directives like weekly and rotate share the list with log sections,
|
||||||
|
# but their name is a plain string instead of a list of log files
|
||||||
|
foreach my $c (@{&logrotate::get_config()}) {
|
||||||
|
next if (ref($c->{'name'}) ne 'ARRAY');
|
||||||
|
return $c if (&indexof($logfile, @{$c->{'name'}}) >= 0);
|
||||||
|
}
|
||||||
|
return undef;
|
||||||
|
}
|
||||||
|
|
||||||
|
=head2 setup_miniserv_logrotate(&miniserv, name, [&extra-logs])
|
||||||
|
|
||||||
|
Adds any missing Miniserv and extra logs to the access-log section, or creates
|
||||||
|
a section when none exists. Copy-truncate rotation lets Miniserv keep its open
|
||||||
|
error log handle, as it only re-opens that log when the file disappears.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
sub setup_miniserv_logrotate
|
||||||
|
{
|
||||||
|
my ($miniserv, $name, $extra) = @_;
|
||||||
|
my @logs = &unique(&miniserv_log_files($miniserv), @{$extra || [ ]});
|
||||||
|
my $lconf = &get_miniserv_logrotate_section($miniserv);
|
||||||
|
|
||||||
|
# Skip any log that some other section already rotates
|
||||||
|
my %covered;
|
||||||
|
foreach my $c (@{&logrotate::get_config()}) {
|
||||||
|
next if (ref($c->{'name'}) ne 'ARRAY');
|
||||||
|
foreach my $log (@{$c->{'name'}}) {
|
||||||
|
$covered{$log} = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
my @missing = grep { !$covered{$_} } @logs;
|
||||||
|
return 0 if ($lconf && !@missing);
|
||||||
|
|
||||||
|
# Vendor files cannot be edited in place, so copy first on systems that
|
||||||
|
# keep their logrotate config under /usr
|
||||||
|
my $file = $lconf ? $lconf->{'file'} : &logrotate::get_add_file($name);
|
||||||
|
&logrotate::ensure_writable_config_file($file);
|
||||||
|
my $parent = &logrotate::get_config_parent();
|
||||||
|
if ($lconf) {
|
||||||
|
# Copying re-reads the config, so look the section up again
|
||||||
|
$lconf = &get_miniserv_logrotate_section($miniserv);
|
||||||
|
push(@{$lconf->{'name'}}, @missing);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$lconf = { 'file' => &logrotate::get_add_file($name),
|
||||||
|
'name' => [ grep { !$covered{$_} } @logs ],
|
||||||
|
'members' => [ { 'name' => 'weekly' },
|
||||||
|
{ 'name' => 'rotate', 'value' => 7 },
|
||||||
|
{ 'name' => 'missingok' },
|
||||||
|
{ 'name' => 'notifempty' },
|
||||||
|
{ 'name' => 'compress' },
|
||||||
|
{ 'name' => 'copytruncate' } ] };
|
||||||
|
}
|
||||||
|
&lock_file($lconf->{'file'});
|
||||||
|
&logrotate::save_directive($parent,
|
||||||
|
defined($lconf->{'index'}) ? $lconf : undef,
|
||||||
|
$lconf);
|
||||||
|
&flush_file_lines($lconf->{'file'});
|
||||||
|
&unlock_file($lconf->{'file'});
|
||||||
|
&logrotate::flush_logrotate_config_cache();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
=head2 remove_miniserv_logrotate(&miniserv, [&extra-logs])
|
||||||
|
|
||||||
|
Removes the Miniserv logs and any extra logs from their logrotate section. The
|
||||||
|
section and its file are deleted when they cover nothing else.
|
||||||
|
|
||||||
|
=cut
|
||||||
|
sub remove_miniserv_logrotate
|
||||||
|
{
|
||||||
|
my ($miniserv, $extra) = @_;
|
||||||
|
my $lconf = &get_miniserv_logrotate_section($miniserv);
|
||||||
|
return 0 if (!$lconf);
|
||||||
|
&logrotate::ensure_writable_config_file($lconf->{'file'});
|
||||||
|
|
||||||
|
# Copying re-reads the config, so look the section up again
|
||||||
|
$lconf = &get_miniserv_logrotate_section($miniserv);
|
||||||
|
my %logs = map { $_, 1 } (&miniserv_log_files($miniserv), @{$extra || [ ]});
|
||||||
|
my @leftover = grep { !$logs{$_} } @{$lconf->{'name'}};
|
||||||
|
my $parent = &logrotate::get_config_parent();
|
||||||
|
&lock_file($lconf->{'file'});
|
||||||
|
if (@leftover) {
|
||||||
|
# Other logs share the section, so only drop ours
|
||||||
|
$lconf->{'name'} = \@leftover;
|
||||||
|
&logrotate::save_directive($parent, $lconf, $lconf);
|
||||||
|
&flush_file_lines($lconf->{'file'});
|
||||||
|
&unlock_file($lconf->{'file'});
|
||||||
|
&logrotate::flush_logrotate_config_cache();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
&logrotate::save_directive($parent, $lconf, undef);
|
||||||
|
&flush_file_lines($lconf->{'file'});
|
||||||
|
&unlock_file($lconf->{'file'});
|
||||||
|
&logrotate::flush_logrotate_config_cache();
|
||||||
|
&logrotate::delete_if_empty($lconf->{'file'});
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
our $primary_port = $primary_ssl ? 443 : 80;
|
our $primary_port = $primary_ssl ? 443 : 80;
|
||||||
|
|
||||||
our $webmin_key_email = "jcameron\@webmin.com";
|
our $webmin_key_email = "jcameron\@webmin.com";
|
||||||
|
|||||||
Reference in New Issue
Block a user