backups: Add rsync backups using hard links for efficiency

This commit is contained in:
Peter Jones 2019-08-28 16:31:30 -07:00
parent a96b46c4d9
commit 1b62b52dfa
No known key found for this signature in database
GPG key ID: 9DAFAA8D01941E49
6 changed files with 270 additions and 1 deletions

View file

@ -39,6 +39,12 @@ Module List
Simple backups for PostgreSQL via `pg_dump`.
* `phoebe.backup.rsync`:
Sync files from a remote machine creating a set of backups that
use hard links for files that don't change from day to day. This
is a simple and efficient way to backup a remote host.
* `phoebe.services.networking.wireguard`:
Simple way to configure a whole network of WireGuard machines.

View file

@ -1,7 +1,53 @@
{ config, lib, pkgs, ...}:
{ config, lib, pkgs, ...}: with lib;
let
cfg = config.phoebe.backup;
user = "backup";
in
{
imports = [
./postgresql.nix
./rsync.nix
];
#### Interface
options.phoebe.backup = {
user = {
enable = mkEnableOption "Backup user and group.";
name = mkOption {
type = types.str;
default = user;
description = "User to perform backups as.";
};
group = mkOption {
type = types.str;
default = user;
description = "Group for the backup user.";
};
};
directory = mkOption {
type = types.path;
default = "/var/backup";
description = ''
Base directory where backups will be stored. Each host to
back up will get a directory under this base directory.
'';
};
};
#### Implementation
config = mkIf cfg.user.enable {
users.users."${cfg.user.name}" = {
description = "Backup user.";
home = cfg.directory;
createHome = true;
group = cfg.user.group;
isSystemUser = true;
};
users.groups."${cfg.user.group}" = {};
};
}

162
modules/backup/rsync.nix Normal file
View file

@ -0,0 +1,162 @@
# Hard-linked backups via rsync.
{ config, lib, pkgs, ...}: with lib;
let
cfg = config.phoebe.backup.rsync;
plib = config.phoebe.lib;
user = "backup";
scripts = (import ../../pkgs/default.nix { inherit pkgs; }).backup-scripts;
##############################################################################
# Backup options.
backupOpts = {
options = {
host = mkOption {
type = types.str;
example = "example.com";
description = "Host name for the machine to back up.";
};
port = mkOption {
type = types.ints.positive;
default = builtins.head config.services.openssh.ports;
example = 22;
description = "SSH port on the remote machine.";
};
user = mkOption {
type = types.str;
default = "backup";
example = "root";
description = "User name on the remote machine to use.";
};
directory = mkOption {
type = types.path;
default = "/var/backup";
example = "/var/lib/backup";
description = "Remote directory to sync to the local machine.";
};
key = mkOption {
type = types.nullOr types.path;
default = null;
example = "/home/backup/.ssh/id_ed25519";
description = ''
Optional SSH key to use when connecting to the remote
machine. If the key is provided by NixOps then this backup
will wait until the key is available.
'';
};
schedule = mkOption {
type = types.str;
default = "*-*-* 02:00:00";
example = "*-*-* *:00/30:00";
description = ''
A systemd calendar specification to designate the frequency
of the backup. You can use the "systemd-analyze calendar"
command to validate your calendar specification.
When increasing the frequency of the backups you should
consider changing the number of backups that you keep.
'';
};
keep = mkOption {
type = types.ints.positive;
default = 7;
example = 14;
description = "Number of backups to keep when deleting older backups.";
};
};
};
##############################################################################
# Sanitize the name of a directory.
dir = path: replaceStrings ["/"] ["-"] (removePrefix "/" path);
##############################################################################
# Generate a service/timer name (without the suffix):
name = opts: "rsync-${opts.host}-${dir opts.directory}";
##############################################################################
# Generate a systemd service for a backup.
service = opts:
let base = "${config.phoebe.backup.directory}/rsync";
localdir = "${base}/${opts.host}/${dir opts.directory}";
in rec {
description = "rsync backup for ${opts.host}:${opts.directory}";
path = [ pkgs.coreutils scripts ];
wants = plib.keyService opts.key;
after = wants;
serviceConfig = {
Type = "simple";
PermissionsStartOnly = "true";
User = cfg.user;
};
preStart = ''
mkdir -p "${localdir}"
chown -R ${cfg.user}:${cfg.group} "${localdir}"
chmod -R 0700 "${localdir}"
'';
script = ''
export BACKUP_LIB_DIR=${scripts}/lib
export BACKUP_LOG_DIR=stdout
export BACKUP_SSH_KEY=${toString opts.key}
export BACKUP_SSH_PORT=${toString opts.port}
. "${scripts}/lib/backup.sh"
backup_via_rsync "${opts.user}@${opts.host}:${opts.directory}" "${localdir}"
backup-purge.sh -k "${toString opts.keep}" -d "${localdir}"
'';
};
##############################################################################
# Generate a systemd timer for a backup.
timer = opts: {
description = "Scheduled Backup of ${opts.host}:${opts.directory}";
wantedBy = [ "timers.target" ];
timerConfig.OnCalendar = opts.schedule;
timerConfig.RandomizedDelaySec = "5m";
timerConfig.Unit = "${name opts}.service";
};
##############################################################################
# Generate systemd services and timers.
toSystemd = f: foldr (a: b: b // {"${name a}" = f a;}) {} cfg.schedules;
in
{
#### Interface
options.phoebe.backup.rsync = {
enable = mkEnableOption "rsync backups";
user = mkOption {
type = types.str;
default = user;
description = "User to perform backups as.";
};
group = mkOption {
type = types.str;
default = config.phoebe.backup.user.group;
description = "Group for the backup user.";
};
schedules = mkOption {
type = types.listOf (types.submodule backupOpts);
default = [];
description = "List of backups to perform.";
};
};
#### Implementation
config = mkIf cfg.enable {
phoebe.backup.user.enable = cfg.user == user;
systemd.services = toSystemd service;
systemd.timers = toSystemd timer;
};
}

View file

@ -0,0 +1,47 @@
{ pkgs ? import <nixpkgs> {}
}:
let
service = "rsync-localhost-tmp-backup.service";
in
pkgs.nixosTest {
name = "rsync-backup-test";
nodes = {
simple = {config, pkgs, ...}: {
imports = [ ../../../modules ];
phoebe.security.enable = false;
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [
(builtins.readFile ../../data/ssh.id_ed25519.pub)
];
phoebe.backup.rsync = {
enable = true;
schedules = [
{ host = "localhost";
directory = "/tmp/backup";
user = "root";
key = "/tmp/key";
}
];
};
};
};
testScript = ''
$simple->start;
$simple->copyFileFromHost("${../../data/ssh.id_ed25519}", "/tmp/key");
$simple->succeed("chmod 0600 /tmp/key");
$simple->succeed("chown backup:backup /tmp/key");
$simple->succeed("mkdir /tmp/backup");
$simple->succeed("echo OKAY > /tmp/backup/file");
$simple->waitForUnit("sshd.service");
$simple->systemctl("start ${service}");
$simple->waitForUnit("${service}");
$simple->waitUntilFails("systemctl status ${service} | grep -q 'Active: active'");
$simple->succeed("cat /var/backup/rsync/localhost/tmp-backup/*/file") =~ /OKAY/ or die;
'';
}

7
test/data/ssh.id_ed25519 Normal file
View file

@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACDvu8YUGTtH+Dr9JM6fk0lvTatPduUZleYMDxIw8ludwAAAAIg8mzDNPJsw
zQAAAAtzc2gtZWQyNTUxOQAAACDvu8YUGTtH+Dr9JM6fk0lvTatPduUZleYMDxIw8ludwA
AAAEDvidTlFEJvyV9Bn2rY2rMieHx/GMVdm2T8I/noFxdCb++7xhQZO0f4Ov0kzp+TSW9N
q0925RmV5gwPEjDyW53AAAAABHRlc3QB
-----END OPENSSH PRIVATE KEY-----

View file

@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO+7xhQZO0f4Ov0kzp+TSW9Nq0925RmV5gwPEjDyW53A test