diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix index 28f607861f7..3f533fa457d 100644 --- a/nixos/modules/services/backup/mysql-backup.nix +++ b/nixos/modules/services/backup/mysql-backup.nix @@ -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; + }; + }; }; } diff --git a/nixos/release.nix b/nixos/release.nix index ac7755a160f..06f1c73410c 100644 --- a/nixos/release.nix +++ b/nixos/release.nix @@ -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; }; diff --git a/nixos/tests/mysql-backup.nix b/nixos/tests/mysql-backup.nix new file mode 100644 index 00000000000..f5bcc460cba --- /dev/null +++ b/nixos/tests/mysql-backup.nix @@ -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"); + ''; +}) diff --git a/nixos/tests/testdb.sql b/nixos/tests/testdb.sql index 4fb28fea3df..3c68c49ae82 100644 --- a/nixos/tests/testdb.sql +++ b/nixos/tests/testdb.sql @@ -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');