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
release-18.03-flake
Rodney Lorrimar 2017-09-23 15:58:07 +01:00 committed by Robin Gloster
parent 75ba415fbc
commit 56eba66f77
4 changed files with 112 additions and 18 deletions

View File

@ -6,10 +6,28 @@ let
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
@ -26,17 +44,16 @@ 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.
'';
@ -66,16 +83,49 @@ in
};
config = mkIf config.services.mysqlBackup.enable {
config = mkIf cfg.enable {
users.extraUsers = optionalAttrs (cfg.user == defaultUser) (singleton
{ name = defaultUser;
isSystemUser = true;
createHome = false;
home = cfg.location;
group = "nogroup";
});
services.cron.systemCronJobs = map mysqlBackupCron config.services.mysqlBackup.databases;
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;
};
};
};
}

View File

@ -283,6 +283,7 @@ in rec {
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; };

View File

@ -0,0 +1,42 @@
# 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");
'';
})

View File

@ -8,3 +8,4 @@ insert into tests values (1, 'a');
insert into tests values (2, 'b');
insert into tests values (3, 'c');
insert into tests values (4, 'd');
insert into tests values (5, 'hello');