Add MySQL binary log management

This PR adds a "Binary Logging" page to configure log format, file size and retention, rotate or purge logs, and support restore sessions that do not write back to binary logs.
This commit is contained in:
Ilia Ross
2026-08-28 02:16:50 +02:00
parent 6ff25cad99
commit b3b81af34f
21 changed files with 785 additions and 11 deletions

View File

@@ -1,5 +1,6 @@
---- Changes since 2.641 ----
Added defaults for alpine linux and fix package installs
Added a Binary Logging page for enabling binary logs, setting their format, maximum file size and retention period, rotating logs on demand, and deleting old log files.
---- Changes since 1.140 ----
The not null flag and a default value can be specified for fields in new tables.
The form for creating an initial table in a new database is now the same as the one for adding a table to an existing database.

169
mysql/edit_binlogs.cgi Executable file
View File

@@ -0,0 +1,169 @@
#!/usr/local/bin/perl
# Show binary logging status, configuration options and log cleanup
require './mysql-lib.pl';
$access{'perms'} == 1 || &error($text{'binlogs_ecannot'});
&ReadParse();
&ui_print_header(undef, $text{'binlogs_title'}, "", "binlogs");
# Make sure config exists
$conf = &get_mysql_config();
if (!$conf) {
print &text('cnf_efile', "<tt>$config{'my_cnf'}</tt>",
"../config.cgi?$module_name"),"<p>\n";
&ui_print_footer("", $text{'index_return'});
exit;
}
# Prefer the main server section over generic MariaDB sections that can
# belong to a plugin-specific include file
($mysqld) = grep { $_->{'name'} eq 'mysqld' } @$conf;
($mysqld) = grep { $_->{'name'} eq 'mariadbd' } @$conf if (!$mysqld);
($mysqld) = grep { $_->{'name'} eq 'mariadb' } @$conf if (!$mysqld);
$mysqld || &error($text{'cnf_emysqld'});
$mems = $mysqld->{'members'};
$local = &is_mysql_local();
# Get the status reported by the running server
($curfile, $curpos) = &get_binary_log_status();
@logs = &list_binary_logs();
$liveformat = eval {
local $main::error_must_die = 1;
my $d = &execute_sql($master_db,
"show variables like 'binlog_format'");
@{$d->{'data'}} ? $d->{'data'}->[0]->[1] : undef;
};
# Show tabs for configuration and log cleanup
@tabs = ( [ 'options', $text{'binlogs_tab_options'} ],
$curfile ? ( [ 'purge', $text{'binlogs_tab_purge'} ] ) : ( ) );
%valid = map { $_->[0] => 1 } @tabs;
$mode = $in{'mode'} && $valid{$in{'mode'}} ? $in{'mode'} : 'options';
print &ui_tabs_start(\@tabs, "mode", $mode, 1);
# Configuration tab
print &ui_tabs_start_tab("mode", "options");
print &ui_div($text{'binlogs_desc_options'});
print &ui_form_start("save_binlogs.cgi", "post");
print &ui_table_start($text{'binlogs_header'}, "width=100%", 2);
# Current state, as one read-only row
if ($curfile) {
$total = 0;
foreach $l (@logs) {
$total += $l->{'size'};
}
print &ui_table_row($text{'binlogs_active'},
&text('binlogs_activeyes', "<tt>".&html_escape($curfile)."</tt>",
$curpos, scalar(@logs), &nice_size($total)).
($liveformat ? " ".&text('binlogs_activefmt',
uc($liveformat)) : ""));
}
else {
print &ui_table_row($text{'binlogs_active'}, $text{'no'});
}
if ($local) {
# Enabled state considers both positive and negative directives, the
# live server status, and finally whether this server version logs
# by default when nothing is configured (as MySQL 8+ does)
$lbdir = &find("log_bin", $mems) || &find("log-bin", $mems);
$skipdir = &find("skip-log-bin", $mems) ||
&find("skip_log_bin", $mems) ||
&find("disable-log-bin", $mems) ||
&find("disable_log_bin", $mems);
$lb = $lbdir ? $lbdir->{'value'} : undef;
$enabled = $skipdir ? 0 : $lbdir ? 1 : $curfile ? 1 :
&get_binlog_default_on();
print &ui_table_row(&hlink($text{'binlogs_enabled'},
"binlogs_enabled"),
&ui_yesno_radio("enabled", $enabled));
print &ui_table_row(&hlink($text{'binlogs_base'}, "binlogs_base"),
&ui_opt_textbox("base", $lb, 30, $text{'binlogs_base_def'}));
$sid = &find_value("server_id", $mems);
$sid = &find_value("server-id", $mems) if (!defined($sid));
print &ui_table_row(&hlink($text{'binlogs_serverid'},
"binlogs_serverid"),
&ui_opt_textbox("serverid", $sid, 10, $text{'default'}));
# Log format, which must be ROW for point-in-time recovery
$fmt = &find_value("binlog_format", $mems);
$fmt = &find_value("binlog-format", $mems) if (!defined($fmt));
print &ui_table_row(&hlink($text{'binlogs_format'},
"binlogs_format"),
&ui_select("format", uc($fmt // ''),
[ [ '', $text{'default'} ],
[ 'ROW', $text{'binlogs_format_row'} ],
[ 'MIXED', $text{'binlogs_format_mixed'} ],
[ 'STATEMENT',
$text{'binlogs_format_statement'} ] ]));
# Maximum size of one log file before rotation
$maxsize = &find_value("max_binlog_size", $mems);
$maxsize = &find_value("max-binlog-size", $mems)
if (!defined($maxsize));
print &ui_table_row(&hlink($text{'binlogs_maxsize'},
"binlogs_maxsize"),
&ui_radio("maxsize_def", defined($maxsize) ? 0 : 1,
[ [ 1, $text{'default'} ], [ 0, " " ] ])."\n".
&mysql_size_input("maxsize", $maxsize));
# Retention period, stored in a variant-specific variable
($ver, $variant) = &get_mysql_variant_cached();
$secs = &find_value("binlog_expire_logs_seconds", $mems);
$days = &find_value("expire_logs_days", $mems);
$expire = defined($secs) ? &format_binlog_expire_days($secs) :
defined($days) ? $days : undef;
print &ui_table_row(&hlink($text{'binlogs_expire'},
"binlogs_expire"),
&ui_opt_textbox("expire", $expire, 6,
$text{'binlogs_expire_def'}).
" ".$text{'binlogs_days'});
print &ui_table_end();
print &ui_form_end([ [ "save", $text{'save'} ],
&is_mysql_running() > 0 ?
( [ "restart",
$text{'binlogs_restart'} ] ) : ( ) ]);
}
else {
# Config file editing only works for a local database server
print &ui_table_end();
print &ui_form_end();
print &ui_alert($text{'binlogs_eremote'}, 'info');
}
print &ui_tabs_end_tab("mode", "options");
# Log cleanup tab
if ($curfile) {
print &ui_tabs_start_tab("mode", "purge");
print &ui_div($text{'binlogs_desc_purge'});
print &ui_form_start("purge_binlogs.cgi", "post");
print &ui_table_start($text{'binlogs_purgeheader'},
"width=100%", 2);
print &ui_table_row(&hlink($text{'binlogs_purge'},
"binlogs_purge"),
&ui_radio("mode", 0,
[ [ 0, &text('binlogs_purgedays',
&ui_textbox("days", 7, 6))."<br>" ],
[ 1, $text{'binlogs_purgeall'} ] ]));
print &ui_table_end();
print &ui_form_end([ [ "purge", $text{'binlogs_purgeok'} ] ]);
print &ui_tabs_end_tab("mode", "purge");
}
print &ui_tabs_end();
# Button to close the current log and start a new one, which gives backups
# and log deletion a clean boundary to work with
if ($curfile) {
print &ui_hr();
print &ui_buttons_start();
print &ui_buttons_row("flush_binlogs.cgi", $text{'binlogs_rotateok'},
&text('binlogs_rotatedesc',
"<tt>".&html_escape($curfile)."</tt>"));
print &ui_buttons_end();
}
&ui_print_footer("", $text{'index_return'});

View File

@@ -13,9 +13,11 @@ if (!$conf) {
&ui_print_footer("", $text{'index_return'});
exit;
}
($mysqld) = grep { $_->{'name'} eq 'mysqld' ||
$_->{'name'} eq 'mariadbd' ||
$_->{'name'} eq 'mariadb' } @$conf;
# Prefer the main server section over generic MariaDB sections that can
# belong to a plugin-specific include file
($mysqld) = grep { $_->{'name'} eq 'mysqld' } @$conf;
($mysqld) = grep { $_->{'name'} eq 'mariadbd' } @$conf if (!$mysqld);
($mysqld) = grep { $_->{'name'} eq 'mariadb' } @$conf if (!$mysqld);
$mysqld || &error($text{'cnf_emysqld'});
$mems = $mysqld->{'members'};
@@ -110,4 +112,3 @@ print &ui_form_end([ [ "save", $text{'save'} ],
[ "restart", $text{'cnf_restart'} ] ]);
&ui_print_footer("", $text{'index_return'});

13
mysql/flush_binlogs.cgi Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/local/bin/perl
# Close the current binary log and start writing a new one
require './mysql-lib.pl';
$access{'perms'} == 1 || &error($text{'binlogs_ecannot'});
&error_setup($text{'binlogs_rotateerr'});
&ReadParse();
my ($curfile) = &get_binary_log_status();
$curfile || &error($text{'binlogs_epurgeoff'});
&rotate_binary_logs();
&webmin_log("flushbinlogs");
&redirect("edit_binlogs.cgi");

18
mysql/help/binlogs.html Normal file
View File

@@ -0,0 +1,18 @@
<header>Binary Logging</header>
This page can be used to enable or disable binary logging on the MySQL or
MariaDB database server, and to manage existing binary log files. Binary logs
record all changes made to databases, and are needed for replication and for
point-in-time recovery of backups. <p>
When binary logging is enabled, log files are written under the configured
base name and grow as changes are made. To stop them from filling up the disk,
a retention period can be set after which old logs are automatically deleted,
or old logs can be deleted immediately using the form at the bottom of the
page. <p>
For any changes on this page to take effect, the MySQL server must be
re-started, either with the <b>Save and Restart MySQL</b> button or using the
buttons on the module's main page. <p>
<footer>

View File

@@ -0,0 +1,6 @@
<header>Binary log base name</header>
<p>The base name for binary log files, to which a sequence number like
<tt>.000001</tt> is appended. A relative name creates the logs in the server's
data directory, while an absolute path can be used to store them elsewhere,
such as on a separate disk.</p>

View File

@@ -0,0 +1,6 @@
<header>Binary logging enabled?</header>
<p>When set to <b>Yes</b>, the database server writes all changes made to
databases to binary log files. These logs are needed for replication, and for
point-in-time recovery of backups. The server must be re-started for a change
to this option to take effect.</p>

View File

@@ -0,0 +1,7 @@
<header>Automatically delete logs older than</header>
<p>The number of days that binary log files are kept before the server deletes
them automatically. Without a limit, logs will grow until they fill up the
disk. For point-in-time recovery the retention period must be at least as long
as the interval between full backups, so that all transactions since the last
backup remain available for replay.</p>

View File

@@ -0,0 +1,10 @@
<header>Binary log format</header>
<p>Controls how changes are recorded in the binary logs. <b>Row based</b>
logs the actual row changes, which is the most reliable format and is required
for point-in-time recovery of individual databases. <b>Statement based</b>
logs the SQL statements that were executed, and <b>Mixed</b> switches between
the two automatically.</p>
<p>Row based logging is recommended, as statement and mixed logs cannot be
safely filtered by database when replaying transactions.</p>

View File

@@ -0,0 +1,6 @@
<header>Maximum size of one log file</header>
<p>When a binary log file reaches this size, the server closes it and starts
writing a new one. Smaller files make deleting old logs and point-in-time
recovery more granular, at the cost of more files. The server accepts sizes
between 4 kB and 1 GB, which is also the default.</p>

View File

@@ -0,0 +1,7 @@
<header>Binary logs to delete</header>
<p>Deletes old binary log files from the server immediately to free up disk
space, either those older than the chosen number of days, or all logs except
the one currently being written. Logs that have been deleted can no longer be
used for replication or point-in-time recovery, so make sure they are not
needed by any replica or for replaying transactions since the last backup.</p>

View File

@@ -0,0 +1,7 @@
<header>Unique server ID</header>
<p>A numeric ID identifying this server in a replication setup, which must be
different on every server that replicates with others. MySQL requires an ID to
be set when binary logging is enabled, so when this option is left as
<b>Default</b> and no ID is configured yet, an ID of <tt>1</tt> is used. On a
stand-alone server the value does not matter.</p>

8
mysql/images/binlogs.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<ellipse cx="20" cy="10" rx="14" ry="5" fill="#eef4ff" stroke="#2563eb" stroke-width="2"/>
<path d="M6 10v22c0 2.8 6.3 5 14 5 2.1 0 4.1-.2 5.9-.5" fill="none" stroke="#2563eb" stroke-width="2"/>
<path d="M34 10v10" fill="none" stroke="#2563eb" stroke-width="2"/>
<path d="M6 21c0 2.8 6.3 5 14 5 2.6 0 5-.3 7-.7" fill="none" stroke="#2563eb" stroke-width="2"/>
<circle cx="34" cy="32" r="10" fill="#eef4ff" stroke="#2563eb" stroke-width="2"/>
<path d="M34 26v6l4.5 3" fill="none" stroke="#1e3a8a" stroke-width="2.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 643 B

View File

@@ -261,14 +261,16 @@ else {
@links = ( 'list_users.cgi', 'list_dbs.cgi',
$canhosts ? ( 'list_hosts.cgi' ) : ( ),
'list_tprivs.cgi', 'list_cprivs.cgi',
'edit_cnf.cgi', 'edit_manual.cgi', 'list_procs.cgi',
'edit_cnf.cgi', 'edit_binlogs.cgi',
'edit_manual.cgi', 'list_procs.cgi',
$canvars ? ( 'list_vars.cgi' ) : ( ),
'edit_ssl.cgi', 'root_form.cgi',
);
@titles = ( $text{'users_title'}, $text{'dbs_title'},
$canhosts ? ( $text{'hosts_title'} ) : ( ),
$text{'tprivs_title'}, $text{'cprivs_title'},
$text{'cnf_title'}, $text{'manual_title'},
$text{'cnf_title'}, $text{'binlogs_title'},
$text{'manual_title'},
$text{'procs_title'},
$canvars ? ( $text{'vars_title'} ) : ( ),
$text{'ssl_title'}, $text{'root_title'},
@@ -276,7 +278,8 @@ else {
@images = ( 'images/users.gif', 'images/dbs.gif',
$canhosts ? ( 'images/hosts.gif' ) : ( ),
'images/tprivs.gif', 'images/cprivs.gif',
'images/cnf.gif', 'images/manual.gif',
'images/cnf.gif', 'images/binlogs.svg',
'images/manual.gif',
'images/procs.gif',
$canvars ? ( 'images/vars.gif' ) : ( ),
'images/ssl.gif', 'images/root.gif',

View File

@@ -921,4 +921,53 @@ ssl_ekeyexists=SSL key file $1 already exists!
stop_ecannot=You are not allowed to stop or start the MySQL server
binlogs_title=Binary Logging
binlogs_tab_options=Binary Logging Options
binlogs_tab_purge=Delete Logs
binlogs_desc_options=Binary logs record all changes made to databases, and are needed for replication and point-in-time recovery of backups. These options control whether the database server writes them, in what format, and for how long they are kept.
binlogs_desc_purge=Old binary logs can be deleted immediately to free up disk space, as long as they are no longer needed by any replica or for replaying transactions since the last backup.
binlogs_active=Binary logging active?
binlogs_activeyes=Yes, currently writing to $1 at position $2 ($3 files of total size $4)
binlogs_activefmt=in $1 format
binlogs_header=Binary logging options
binlogs_enabled=Binary logging enabled?
binlogs_base=Binary log base name
binlogs_base_def=Default <tt>(mysql-bin)</tt>
binlogs_serverid=Unique server ID
binlogs_format=Binary log format
binlogs_format_row=Row based
binlogs_format_mixed=Mixed
binlogs_format_statement=Statement based
binlogs_maxsize=Maximum size of one log file
binlogs_expire=Automatically delete logs older than
binlogs_expire_def=Server default
binlogs_days=days
binlogs_restart=Save and Restart MySQL
binlogs_rotatedesc=Click this button to close the current binary log $1 and start writing a new one, giving backups and log deletion a clean boundary.
binlogs_rotateok=Rotate Binary Logs
binlogs_rotateerr=Failed to rotate binary logs
binlogs_purgeheader=Delete old binary logs
binlogs_purge=Binary logs to delete
binlogs_purgedays=Logs older than $1 days
binlogs_purgeall=All except the current log
binlogs_purgeok=Delete Now
binlogs_err=Failed to save binary logging options
binlogs_ecannot=You are not allowed to configure binary logging
binlogs_eremote=Binary logging can only be configured when the database server is running on this system
binlogs_ebase=Missing or invalid binary log base name
binlogs_eserverid=Server ID must be a number between 1 and 4294967295
binlogs_eformat=Invalid binary log format
binlogs_eexpire=Missing or invalid number of days to keep logs
binlogs_eexpiremax=The number of days to keep logs cannot be more than $1
binlogs_eexpireint=Only a whole number of days to keep logs is supported on this MySQL version
binlogs_emaxsize=Missing or invalid maximum log file size
binlogs_emaxsizerange=The maximum log file size must be between 4 kB and 1 GB
binlogs_purgeerr=Failed to delete binary logs
binlogs_epurgedays=Missing or invalid number of days to keep logs
binlogs_epurgeoff=Binary logging is not active
log_binlogs=Changed binary logging options
log_purgebinlogs=Deleted old binary logs
log_flushbinlogs=Rotated binary logs
__norefs=1

View File

@@ -20,6 +20,15 @@ elsif ($action eq 'restart') {
elsif ($action eq 'cnf') {
return $text{'log_cnf'};
}
elsif ($action eq 'binlogs') {
return $text{'log_binlogs'};
}
elsif ($action eq 'purgebinlogs') {
return $text{'log_purgebinlogs'};
}
elsif ($action eq 'flushbinlogs') {
return $text{'log_flushbinlogs'};
}
elsif ($action eq 'ssl') {
return $text{'log_ssl'};
}

View File

@@ -370,6 +370,18 @@ if ($driver_handle && !$config{'nodbi'}) {
local $dbh = $driver_handle->connect($cstr, $mysql_login, $mysql_pass,
{ });
$dbh || &error("DBI connect failed : ",$driver_handle->errstr);
if ($disable_session_binlog) {
# A point-in-time restore must not add its own SQL to the log that
# it is replaying, because that would make later retries unsafe
local $nologsql = "set session sql_log_bin = 0";
local $nologcmd = $dbh->prepare($nologsql);
if (!$nologcmd || !$nologcmd->execute()) {
&error(&text('esql',
"<tt>".&html_escape($nologsql)."</tt>",
"<tt>".&html_escape($dbh->errstr)."</tt>"));
}
$nologcmd->finish();
}
if ($sql_charset) {
# Switch to correct character set
local $sql = "set names '$sql_charset'";
@@ -411,6 +423,10 @@ else {
$sql = &replace_sql_parameters($sql, @params);
}
open(TEMP, ">$temp");
if ($disable_session_binlog) {
# Keep point-in-time restore statements out of the binary log
print TEMP "set session sql_log_bin = 0;\n";
}
if ($sql_charset) {
print TEMP "set names '$sql_charset';\n";
}
@@ -928,6 +944,38 @@ if ($ver && $variant_ &&
return ($rv, $variant);
}
# get_binary_log_status()
# Returns the current binary log file and position, if binary logging is
# active on the server
sub get_binary_log_status
{
# The statement name varies between MySQL and MariaDB releases
foreach my $sql ("show master status", "show binary log status",
"show binlog status") {
my $d = eval {
local $main::error_must_die = 1;
&execute_sql($master_db, $sql);
};
next if ($@ || !$d);
return ( ) if (!@{$d->{'data'}});
return ($d->{'data'}->[0]->[0], $d->{'data'}->[0]->[1]);
}
return ( );
}
# list_binary_logs()
# Returns a list of binary log files on the server, each as a hash ref with
# name and size keys
sub list_binary_logs
{
my $d = eval {
local $main::error_must_die = 1;
&execute_sql($master_db, "show binary logs");
};
return ( ) if ($@ || !$d);
return map { { 'name' => $_->[0], 'size' => $_->[1] } } @{$d->{'data'}};
}
# save_mysql_version([number])
# Update the saved local MySQL version number
sub save_mysql_version
@@ -1122,6 +1170,10 @@ my $cs = $sql_charset ? "--default-character-set=".quotemeta($sql_charset)
my $temp = &transname();
$file = &fix_collation($file);
&open_tempfile(TEMP, ">$temp");
if ($disable_session_binlog) {
# Keep an imported backup or decoded binary log out of the source log
&print_tempfile(TEMP, "set session sql_log_bin = 0;\n");
}
&print_tempfile(TEMP, "source ".$file.";\n");
&close_tempfile(TEMP);
&set_ownership_permissions(undef, undef, 0644, $temp);
@@ -1207,6 +1259,81 @@ return $config{'host'} eq '' || $config{'host'} eq 'localhost' ||
&to_ipaddress($config{'host'}) eq &to_ipaddress(&get_system_hostname());
}
# supports_disable_session_binlog()
# Returns 1 if restore callers can request that their SQL sessions not be
# written to the server's binary log.
sub supports_disable_session_binlog
{
return 1;
}
# format_binlog_expire_days(seconds)
# Converts a seconds-based binary log retention period to a days value with
# enough decimal places to convert back to the exact same number of seconds
sub format_binlog_expire_days
{
my ($secs) = @_;
my $days = sprintf("%.6f", $secs / 86400);
$days =~ s/0+$//;
$days =~ s/\.$//;
return $days;
}
# parse_binlog_expire_days(days)
# Converts a days-based retention period back to the whole number of seconds
# stored on the server
sub parse_binlog_expire_days
{
my ($days) = @_;
return int($days * 86400 + 0.5);
}
# get_mysql_variant_cached()
# Like get_remote_mysql_variant, but falls back to the version detected at
# module setup time when the server cannot be queried, such as when it is
# stopped
sub get_mysql_variant_cached
{
my ($ver, $variant) = &get_remote_mysql_variant();
if (!$ver || $ver <= 0) {
$ver = $mysql_version;
$variant = $ver =~ /mariadb/i ? "mariadb" : "mysql";
$ver =~ s/[^0-9\.].*$//;
}
return ($ver, $variant);
}
# get_binlog_default_on()
# Returns 1 if this database server enables binary logging by default when
# no directive is present, as MySQL does from version 8.0 onwards
sub get_binlog_default_on
{
my ($ver, $variant) = &get_mysql_variant_cached();
return $variant eq "mysql" && $ver &&
&compare_version_numbers($ver, "8.0") >= 0 ? 1 : 0;
}
# parse_binlog_max_size(number, units)
# Converts a maximum log file size input to bytes, returning undef if it is
# invalid or outside the range from 4 kB to 1 GB that servers support
sub parse_binlog_max_size
{
my ($num, $units) = @_;
$num =~ /^\d+$/ || return undef;
my %mult = ( '' => 1, 'K' => 1024, 'M' => 1024*1024,
'G' => 1024*1024*1024 );
defined($mult{$units}) || return undef;
my $bytes = $num * $mult{$units};
return $bytes >= 4096 && $bytes <= 1024*1024*1024 ? $bytes : undef;
}
# rotate_binary_logs()
# Closes the binary log currently being written and starts a new one
sub rotate_binary_logs
{
&execute_sql_logged($master_db, "flush binary logs");
}
# get_mysql_config()
# Returns the parsed my.cnf file
sub get_mysql_config

25
mysql/purge_binlogs.cgi Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/local/bin/perl
# Delete old binary logs from the server
require './mysql-lib.pl';
$access{'perms'} == 1 || &error($text{'binlogs_ecannot'});
&error_setup($text{'binlogs_purgeerr'});
&ReadParse();
my ($curfile) = &get_binary_log_status();
$curfile || &error($text{'binlogs_epurgeoff'});
if ($in{'mode'} == 0) {
# Purge logs older than some number of days
$in{'days'} =~ /^\d+$/ && $in{'days'} > 0 ||
&error($text{'binlogs_epurgedays'});
&execute_sql_logged($master_db,
"purge binary logs before date_sub(now(), interval ".
$in{'days'}." day)");
}
else {
# Purge all logs except the one currently being written
&execute_sql_logged($master_db,
"purge binary logs to '".$curfile."'");
}
&webmin_log("purgebinlogs");
&redirect("edit_binlogs.cgi");

160
mysql/save_binlogs.cgi Executable file
View File

@@ -0,0 +1,160 @@
#!/usr/local/bin/perl
# Save binary logging configuration options
require './mysql-lib.pl';
$access{'perms'} == 1 || &error($text{'binlogs_ecannot'});
&is_mysql_local() || &error($text{'binlogs_eremote'});
&error_setup($text{'binlogs_err'});
&ReadParse();
# Get the mysqld section
foreach my $l (&get_all_mysqld_files()) {
&lock_file($l);
}
$conf = &get_mysql_config();
# Prefer the main server section over generic MariaDB sections that can
# belong to a plugin-specific include file
($mysqld) = grep { $_->{'name'} eq 'mysqld' } @$conf;
($mysqld) = grep { $_->{'name'} eq 'mariadbd' } @$conf if (!$mysqld);
($mysqld) = grep { $_->{'name'} eq 'mariadb' } @$conf if (!$mysqld);
$mysqld || &error($text{'cnf_emysqld'});
$mems = $mysqld->{'members'};
if ($in{'enabled'}) {
# Work out the base directive, preserving an existing or active default
# so that merely saving this page cannot start a new binary log chain.
# On servers that log by default the directive is also left absent, as
# writing one would change the default base name even when stopped
my $lbdir = &find("log_bin", $mems) || &find("log-bin", $mems);
my $baseargs;
if ($in{'base_def'}) {
if ($lbdir) {
$baseargs = [ $lbdir->{'value'} // "" ];
}
else {
my ($curfile) = &get_binary_log_status();
$baseargs = $curfile || &get_binlog_default_on() ?
[ ] : [ "mysql-bin" ];
}
}
else {
$in{'base'} =~ /^[a-zA-Z0-9\.\-\_\/]+$/ ||
&error($text{'binlogs_ebase'});
$baseargs = [ $in{'base'} ];
}
&save_directive($conf, $mysqld, "log-bin", [ ]);
&save_directive($conf, $mysqld, "log_bin", $baseargs);
# Remove any directives that force binary logging off
foreach my $s ("skip-log-bin", "skip_log_bin",
"disable-log-bin", "disable_log_bin") {
&save_directive($conf, $mysqld, $s, [ ]);
}
# A unique server ID is required by MySQL for binary logging
my $sid = &find_value("server_id", $mems);
$sid = &find_value("server-id", $mems) if (!defined($sid));
if ($in{'serverid_def'}) {
if (!defined($sid)) {
&save_directive($conf, $mysqld, "server_id", [ 1 ]);
}
}
else {
$in{'serverid'} =~ /^\d+$/ && $in{'serverid'} >= 1 &&
$in{'serverid'} <= 4294967295 ||
&error($text{'binlogs_eserverid'});
&save_directive($conf, $mysqld, "server-id", [ ]);
&save_directive($conf, $mysqld, "server_id",
[ $in{'serverid'} ]);
}
}
else {
# Remove the directive under both possible names, and force logging
# off in case the server enables it by default (as MySQL 8+ does)
&save_directive($conf, $mysqld, "log-bin", [ ]);
&save_directive($conf, $mysqld, "log_bin", [ ]);
foreach my $s ("skip_log_bin", "disable-log-bin",
"disable_log_bin") {
&save_directive($conf, $mysqld, $s, [ ]);
}
&save_directive($conf, $mysqld, "skip-log-bin", [ "" ]);
}
# Save the binary log format
if (defined($in{'format'})) {
$in{'format'} =~ /^(|ROW|MIXED|STATEMENT)$/ ||
&error($text{'binlogs_eformat'});
&save_directive($conf, $mysqld, "binlog-format", [ ]);
&save_directive($conf, $mysqld, "binlog_format",
$in{'format'} ? [ $in{'format'} ] : [ ]);
}
# Save the maximum size of one log file, which the server limits to the
# range from 4 kB to 1 GB
if ($in{'maxsize_def'}) {
&save_directive($conf, $mysqld, "max-binlog-size", [ ]);
&save_directive($conf, $mysqld, "max_binlog_size", [ ]);
}
else {
$in{'maxsize'} =~ /^\d+$/ ||
&error($text{'binlogs_emaxsize'});
defined(&parse_binlog_max_size($in{'maxsize'},
$in{'maxsize_units'})) ||
&error($text{'binlogs_emaxsizerange'});
&save_directive($conf, $mysqld, "max-binlog-size", [ ]);
&save_directive($conf, $mysqld, "max_binlog_size",
[ $in{'maxsize'}.$in{'maxsize_units'} ]);
}
# Save the retention period, preferring the seconds-based variable on
# servers that support it as it also allows fractional days
my ($ver, $variant) = &get_mysql_variant_cached();
my $newexpire = $variant eq "mysql" &&
&compare_version_numbers($ver, "8.0") >= 0 ||
$variant eq "mariadb" &&
&compare_version_numbers($ver, "10.6") >= 0;
if ($in{'expire_def'}) {
&save_directive($conf, $mysqld, "binlog_expire_logs_seconds", [ ]);
&save_directive($conf, $mysqld, "expire_logs_days", [ ]);
}
else {
# Fractional days are allowed, and zero disables automatic deletion
$in{'expire'} =~ /^\d+(\.\d+)?$/ ||
&error($text{'binlogs_eexpire'});
my $days = $in{'expire'} + 0;
if ($newexpire) {
# The seconds variable is limited to 32 bits
my $secs = &parse_binlog_expire_days($days);
$secs <= 4294967295 ||
&error(&text('binlogs_eexpiremax',
&format_binlog_expire_days(4294967295)));
&save_directive($conf, $mysqld, "expire_logs_days", [ ]);
&save_directive($conf, $mysqld, "binlog_expire_logs_seconds",
[ $secs ]);
}
else {
# The days variable is limited to 99 whole days on servers
# without the seconds-based variable
$days <= 99 ||
&error(&text('binlogs_eexpiremax', 99));
$days == int($days) ||
&error($text{'binlogs_eexpireint'});
&save_directive($conf, $mysqld, "binlog_expire_logs_seconds",
[ ]);
&save_directive($conf, $mysqld, "expire_logs_days",
[ $days ]);
}
}
# Write out file, and restart if requested
foreach my $l (&get_all_mysqld_files()) {
&flush_file_lines($l, undef, 1);
&unlock_file($l);
}
if ($in{'restart'} && &is_mysql_running() > 0) {
&stop_mysql();
$err = &start_mysql();
&error($err) if ($err);
}
&webmin_log("binlogs");
&redirect("edit_binlogs.cgi");

View File

@@ -11,9 +11,11 @@ foreach my $l (&get_all_mysqld_files()) {
&lock_file($l);
}
$conf = &get_mysql_config();
($mysqld) = grep { $_->{'name'} eq 'mysqld' ||
$_->{'name'} eq 'mariadbd' ||
$_->{'name'} eq 'mariadb' } @$conf;
# Prefer the main server section over generic MariaDB sections that can
# belong to a plugin-specific include file
($mysqld) = grep { $_->{'name'} eq 'mysqld' } @$conf;
($mysqld) = grep { $_->{'name'} eq 'mariadbd' } @$conf if (!$mysqld);
($mysqld) = grep { $_->{'name'} eq 'mariadb' } @$conf if (!$mysqld);
$mysqld || &error($text{'cnf_emysqld'});
$mems = $mysqld->{'members'};
@@ -134,4 +136,3 @@ if ($in{'restart'} && &is_mysql_running() > 0) {
}
&webmin_log("cnf");
&redirect("");

141
t/mysql-lib.t Normal file
View File

@@ -0,0 +1,141 @@
#!/usr/bin/perl
# Unit tests for MySQL module binary logging helpers.
use strict;
use warnings;
no warnings 'once';
use Test::More;
use File::Basename qw(dirname);
use File::Spec;
use Cwd qw(abs_path);
BEGIN {
# mysql-lib.pl imports WebminCore and initializes module globals at
# load time. Stub only those load-time dependencies so its pure helpers
# can be tested without a Webmin installation.
$INC{'WebminCore.pm'} = 1;
$INC{'view-lib.pl'} = 1;
package WebminCore;
sub import
{
my $caller = caller;
no strict 'refs';
*{$caller.'::init_config'} = sub { };
*{$caller.'::get_module_acl'} = sub { return (); };
*{$caller.'::read_file_contents'} = sub { return "10.5.29-MariaDB\n"; };
*{$caller.'::compare_version_numbers'} = sub {
my ($left, $right) = @_;
my @left = split(/\./, $left);
my @right = split(/\./, $right);
my $count = @left > @right ? scalar(@left) : scalar(@right);
for(my $i = 0; $i < $count; $i++) {
my $cmp = ($left[$i] || 0) <=> ($right[$i] || 0);
return $cmp if ($cmp);
}
return 0;
};
*{$caller.'::has_command'} = sub { return $_[0]; };
*{$caller.'::quote_path'} = sub { return $_[0]; };
*{$caller.'::backquote_command'} = sub { return ''; };
}
}
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..'));
chdir($root) or die "chdir($root): $!";
do './mysql/mysql-lib.pl' or die $@ || $!;
subtest 'binary log retention display round-trips exactly' => sub {
# The page shows seconds as days, and saving converts the shown days
# back with int(days * 86400 + 0.5), which must reproduce the exact
# stored seconds even for non-round values
foreach my $secs (0, 1, 43200, 86400, 100000, 123456789, 2592000,
4294967295) {
my $days = main::format_binlog_expire_days($secs);
like($days, qr/^\d+(\.\d+)?$/,
"$secs seconds display as plain decimal ($days)");
is(int($days * 86400 + 0.5), $secs,
"$secs seconds round-trip through $days days");
}
is(main::format_binlog_expire_days(43200), '0.5',
'half a day is shown without trailing zeros');
is(main::format_binlog_expire_days(2592000), '30',
'whole days are shown without a decimal point');
};
subtest 'default logging state considers variant, version and downtime' => sub {
no warnings 'redefine';
my $live;
local *main::get_remote_mysql_variant = sub {
return $live ? @$live : (-1);
};
$live = [ '8.0.36', 'mysql' ];
ok(main::get_binlog_default_on(),
'a running MySQL 8 logs by default');
$live = [ '5.7.44', 'mysql' ];
ok(!main::get_binlog_default_on(),
'a running MySQL 5.7 does not log by default');
$live = [ '10.11.6', 'mariadb' ];
ok(!main::get_binlog_default_on(),
'a running MariaDB does not log by default');
# When the server is stopped, the version cached at module setup
# time must be used instead
$live = undef;
local $main::mysql_version = '8.0.36';
ok(main::get_binlog_default_on(),
'a stopped MySQL 8 still counts as logging by default');
is_deeply([ main::get_mysql_variant_cached() ],
[ '8.0.36', 'mysql' ],
'a stopped MySQL 8 falls back to the cached version');
local $main::mysql_version = '10.5.29-MariaDB';
ok(!main::get_binlog_default_on(),
'a stopped MariaDB does not count as logging by default');
is_deeply([ main::get_mysql_variant_cached() ],
[ '10.5.29', 'mariadb' ],
'a stopped MariaDB falls back to the cached version');
};
subtest 'maximum log file size units and range' => sub {
is(main::parse_binlog_max_size(100, 'M'), 104857600,
'100 MB converts to bytes');
is(main::parse_binlog_max_size(4, 'K'), 4096,
'the 4 kB minimum is accepted');
is(main::parse_binlog_max_size(1, 'G'), 1073741824,
'the 1 GB maximum is accepted');
is(main::parse_binlog_max_size(8192, ''), 8192,
'plain bytes are accepted');
ok(!defined(main::parse_binlog_max_size(1, 'K')),
'below the 4 kB minimum is rejected');
ok(!defined(main::parse_binlog_max_size(2, 'G')),
'above the 1 GB maximum is rejected');
ok(!defined(main::parse_binlog_max_size('abc', 'M')),
'a non-numeric size is rejected');
ok(!defined(main::parse_binlog_max_size(100, 'T')),
'an unknown unit is rejected');
};
subtest 'rotation issues the expected statement' => sub {
no warnings 'redefine';
my @sql;
local *main::execute_sql_logged = sub { push(@sql, [ @_ ]); };
main::rotate_binary_logs();
is_deeply(\@sql, [ [ 'mysql', 'flush binary logs' ] ],
'rotation runs FLUSH BINARY LOGS on the master database');
};
subtest 'binary log retention limit covers the full 32-bit range' => sub {
# Saving validates the converted seconds against the variable's
# 32-bit maximum, so the displayed maximum must be accepted while
# anything above it is rejected
my $maxdays = main::format_binlog_expire_days(4294967295);
is($maxdays, '49710.269618', 'the 32-bit maximum displays in days');
is(main::parse_binlog_expire_days($maxdays), 4294967295,
'the displayed maximum converts back within the limit');
cmp_ok(main::parse_binlog_expire_days('49710.27'), '>', 4294967295,
'days just past the maximum exceed the seconds limit');
cmp_ok(main::parse_binlog_expire_days(49710), '<=', 4294967295,
'whole days below the maximum stay within the limit');
};
done_testing();