mysqlBackup service: let it work with default settings

* Grants enough privileges to the configured user so that it can run
mysqldump.

* Adds a nixos test.

* Use systemd timers instead of a cronjob (by @fadenb).

* Creates a new user for backups by default, instead of using mysql
user.

* Ensures that backup user has write permissions on backup location.

* Write backup to a temporary file before renaming so that a failed
backup won't overwrite the previous backup, and so that the backup
location will never contain a partial backup.

Breaking changes:

* Renamed period to calendar to reflect the change in how to
configure the backup time.

* A failed backup will no longer result in cron sending an e-mail --
users' monitoring systems must be updated.

Resolves #24728

Changed files
+112 -18
nixos
+68 -18
nixos/modules/services/backup/mysql-backup.nix
···
inherit (pkgs) mysql gzip;
-
cfg = config.services.mysqlBackup ;
-
location = cfg.location ;
-
mysqlBackupCron = db : ''
-
${cfg.period} ${cfg.user} ${mysql}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > ${location}/${db}.gz
+
cfg = config.services.mysqlBackup;
+
defaultUser = "mysqlbackup";
+
+
backupScript = ''
+
set -o pipefail
+
failed=""
+
${concatMapStringsSep "\n" backupDatabaseScript cfg.databases}
+
if [ -n "$failed" ]; then
+
echo "Backup of database(s) failed:$failed"
+
exit 1
+
fi
+
'';
+
backupDatabaseScript = db: ''
+
dest="${cfg.location}/${db}.gz"
+
if ${mysql}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then
+
mv $dest.tmp $dest
+
echo "Backed up to $dest"
+
else
+
echo "Failed to back up to $dest"
+
rm -f $dest.tmp
+
failed="$failed ${db}"
+
fi
'';
in
···
'';
};
-
period = mkOption {
-
default = "15 01 * * *";
+
calendar = mkOption {
+
type = types.str;
+
default = "01:15:00";
description = ''
-
This option defines (in the format used by cron) when the
-
databases should be dumped.
-
The default is to update at 01:15 (at night) every day.
+
Configured when to run the backup service systemd unit (DayOfWeek Year-Month-Day Hour:Minute:Second).
'';
};
user = mkOption {
-
default = "mysql";
+
default = defaultUser;
description = ''
User to be used to perform backup.
'';
···
};
-
config = mkIf config.services.mysqlBackup.enable {
-
-
services.cron.systemCronJobs = map mysqlBackupCron config.services.mysqlBackup.databases;
+
config = mkIf cfg.enable {
+
users.extraUsers = optionalAttrs (cfg.user == defaultUser) (singleton
+
{ name = defaultUser;
+
isSystemUser = true;
+
createHome = false;
+
home = cfg.location;
+
group = "nogroup";
+
});
-
system.activationScripts.mysqlBackup = stringAfter [ "stdio" "users" ]
-
''
-
mkdir -m 0700 -p ${config.services.mysqlBackup.location}
-
chown ${config.services.mysqlBackup.user} ${config.services.mysqlBackup.location}
-
'';
+
services.mysql.ensureUsers = [{
+
name = cfg.user;
+
ensurePermissions = with lib;
+
let
+
privs = "SELECT, SHOW VIEW, TRIGGER, LOCK TABLES";
+
grant = db: nameValuePair "${db}.*" privs;
+
in
+
listToAttrs (map grant cfg.databases);
+
}];
+
systemd = {
+
timers."mysql-backup" = {
+
description = "Mysql backup timer";
+
wantedBy = [ "timers.target" ];
+
timerConfig = {
+
OnCalendar = cfg.calendar;
+
AccuracySec = "5m";
+
Unit = "mysql-backup.service";
+
};
+
};
+
services."mysql-backup" = {
+
description = "Mysql backup service";
+
enable = true;
+
serviceConfig = {
+
User = cfg.user;
+
PermissionsStartOnly = true;
+
};
+
preStart = ''
+
mkdir -m 0700 -p ${cfg.location}
+
chown -R ${cfg.user} ${cfg.location}
+
'';
+
script = backupScript;
+
};
+
};
};
}
+1
nixos/release.nix
···
tests.mumble = callTest tests/mumble.nix {};
tests.munin = callTest tests/munin.nix {};
tests.mysql = callTest tests/mysql.nix {};
+
tests.mysqlBackup = callTest tests/mysql-backup.nix {};
tests.mysqlReplication = callTest tests/mysql-replication.nix {};
tests.nat.firewall = callTest tests/nat.nix { withFirewall = true; };
tests.nat.firewall-conntrack = callTest tests/nat.nix { withFirewall = true; withConntrackHelpers = true; };
+42
nixos/tests/mysql-backup.nix
···
+
# Test whether mysqlBackup option works
+
import ./make-test.nix ({ pkgs, ... } : {
+
name = "mysql-backup";
+
meta = with pkgs.stdenv.lib.maintainers; {
+
maintainers = [ rvl ];
+
};
+
+
nodes = {
+
master = { config, pkgs, ... }: {
+
services.mysql = {
+
enable = true;
+
initialDatabases = [ { name = "testdb"; schema = ./testdb.sql; } ];
+
package = pkgs.mysql;
+
};
+
+
services.mysqlBackup = {
+
enable = true;
+
databases = [ "doesnotexist" "testdb" ];
+
};
+
};
+
};
+
+
testScript =
+
'' startAll;
+
+
# Need to have mysql started so that it can be populated with data.
+
$master->waitForUnit("mysql.service");
+
+
# Wait for testdb to be populated.
+
$master->sleep(10);
+
+
# Do a backup and wait for it to finish.
+
$master->startJob("mysql-backup.service");
+
$master->waitForJob("mysql-backup.service");
+
+
# Check that data appears in backup
+
$master->succeed("${pkgs.gzip}/bin/zcat /var/backup/mysql/testdb.gz | grep hello");
+
+
# Check that a failed backup is logged
+
$master->succeed("journalctl -u mysql-backup.service | grep 'fail.*doesnotexist' > /dev/null");
+
'';
+
})
+1
nixos/tests/testdb.sql
···
insert into tests values (2, 'b');
insert into tests values (3, 'c');
insert into tests values (4, 'd');
+
insert into tests values (5, 'hello');