diff --git a/usermin/change_log.cgi b/usermin/change_log.cgi
index c95a2a11c..4a165b60e 100755
--- a/usermin/change_log.cgi
+++ b/usermin/change_log.cgi
@@ -13,14 +13,24 @@ 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.
-!$in{'logclear'} || $in{'logtime'} =~ /^[1-9][0-9]*$/ ||
- &error(&text('log_ehours', $in{'logtime'}));
$miniserv{'log'} = $in{'log'};
$miniserv{'loghost'} = $in{'loghost'};
$miniserv{'logtrust'} = $in{'logtrust'};
$miniserv{'logclf'} = $in{'logclf'};
-$miniserv{'logclear'} = $in{'logclear'};
+$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.
@@ -35,6 +45,14 @@ else {
}
&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);
diff --git a/usermin/edit_log.cgi b/usermin/edit_log.cgi
index 746be0d46..094b21633 100755
--- a/usermin/edit_log.cgi
+++ b/usermin/edit_log.cgi
@@ -21,11 +21,17 @@ 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", int($miniserv{'logclear'}),
- [ [ 1, &text('log_period',
- &ui_textbox("logtime", $miniserv{'logtime'}, 10)) ],
- [ 0, $text{'no'} ] ]));
+ &ui_radio("logclear", $logclear, \@clear_opts));
# Only systemd services can safely inherit stderr into the journal.
if (&webmin::miniserv_systemd_journal_available("usermin.service")) {
diff --git a/usermin/lang/en b/usermin/lang/en
index d19370253..089046662 100644
--- a/usermin/lang/en
+++ b/usermin/lang/en
@@ -10,6 +10,8 @@ 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 logrotate
+log_elogrotate=The logrotate configuration is not available on this system
log_error=Error log destination
log_error_file=Write to miniserv.error
log_error_journal=Write to the systemd journal
diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl
index 03310b033..8c754760f 100755
--- a/web-lib-funcs.pl
+++ b/web-lib-funcs.pl
@@ -7621,6 +7621,14 @@ my ($param_action,
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'}) {
# check if it is time to clear the log
my @st = stat("$webmin_logfile.time");
diff --git a/webmin/change_log.cgi b/webmin/change_log.cgi
index 0ef1cc2ca..ad5a6f39f 100755
--- a/webmin/change_log.cgi
+++ b/webmin/change_log.cgi
@@ -20,8 +20,18 @@ $miniserv{'log'} = $in{'log'};
$miniserv{'loghost'} = $in{'loghost'};
$miniserv{'logtrust'} = $in{'logtrust'};
$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'}));
$miniserv{'logtime'} = $in{'logtime'};
if ($in{'perms_def'}) {
@@ -60,9 +70,18 @@ else {
}
&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{'logtime'} = $in{'logtime'};
-$gconfig{'logclear'} = $in{'logclear'};
+$gconfig{'logclear'} = $miniserv{'logclear'};
+$gconfig{'logrotate'} = $logrotate;
$gconfig{'logusers'} =
$in{'uall'} ? '' : join(" ", split(/\0/, $in{'users'}));
$gconfig{'logmodules'} =
@@ -95,4 +114,3 @@ else {
&show_restart_page();
}
&webmin_log("log", undef, undef, \%in);
-
diff --git a/webmin/edit_log.cgi b/webmin/edit_log.cgi
index 107843594..e221e736d 100755
--- a/webmin/edit_log.cgi
+++ b/webmin/edit_log.cgi
@@ -31,12 +31,18 @@ print &ui_table_row($text{'log_trust'},
print &ui_table_row($text{'log_clf'},
&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'},
- &ui_radio("logclear", int($miniserv{'logclear'}),
- [ [ 1, &text('log_period',
- &ui_textbox("logtime", $miniserv{'logtime'}, 10)) ],
- [ 0, $text{'no'} ] ]));
+ &ui_radio("logclear", $logclear, \@clear_opts));
# A systemd service can keep Miniserv errors in the journal instead.
if (&miniserv_systemd_journal_available("webmin.service")) {
diff --git a/webmin/lang/en b/webmin/lang/en
index da1cd4c6e..8c8e19c9f 100644
--- a/webmin/lang/en
+++ b/webmin/lang/en
@@ -93,6 +93,8 @@ log_trust=Log client IP address when behind proxy?
log_clf=Use combined log format (including referrer and user agent)?
log_clear2=Periodically clear log files?
log_period=Yes, every $1 hours
+log_logrotate=Yes, rotate using logrotate
+log_elogrotate=The logrotate configuration is not available on this system
log_error=Error log destination
log_error_file=Write to miniserv.error
log_error_journal=Write to the systemd journal
diff --git a/webmin/webmin-lib.pl b/webmin/webmin-lib.pl
index e8e6ffe6c..5398ea1e5 100755
--- a/webmin/webmin-lib.pl
+++ b/webmin/webmin-lib.pl
@@ -83,6 +83,8 @@ else {
$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.
@@ -110,6 +112,142 @@ else {
}
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 $webmin_key_email = "jcameron\@webmin.com";