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";