From 285380fe566700ab3bf4f69b0a1a10fb4d9bba3a Mon Sep 17 00:00:00 2001 From: =?utf8?q?Isma=C3=ABl=20Bouya?= Date: Thu, 24 Oct 2019 00:36:35 +0200 Subject: [PATCH] Add rsync backup --- modules/default.nix | 1 + modules/private/system/backup-2.nix | 16 ++ modules/rsync_backup/default.nix | 262 ++++++++++++++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 modules/rsync_backup/default.nix diff --git a/modules/default.nix b/modules/default.nix index 18bee9a..9ff6ea6 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -14,6 +14,7 @@ openarc = ./openarc.nix; duplyBackup = ./duply_backup; + rsyncBackup = ./rsync_backup; naemon = ./naemon; php-application = ./websites/php-application.nix; diff --git a/modules/private/system/backup-2.nix b/modules/private/system/backup-2.nix index 80fa36d..6151671 100644 --- a/modules/private/system/backup-2.nix +++ b/modules/private/system/backup-2.nix @@ -30,6 +30,22 @@ interfaces."ens3".ipv6.addresses = pkgs.lib.flatten (pkgs.lib.attrsets.mapAttrsToList (n: ips: map (ip: { address = ip; prefixLength = (if n == "main" && ip == pkgs.lib.head ips.ip6 then 64 else 128); }) (ips.ip6 or [])) myconfig.env.servers.backup-2.ips); + + defaultMailServer = { + directDelivery = true; + hostName = "eldiron.immae.eu:25"; + useTLS = true; + useSTARTTLS = true; + root = "postmaster@immae.eu"; + }; + }; + + services.rsyncBackup = { + mountpoint = "/backup2"; + mailto = myconfig.env.rsync_backup.mailto; + profiles = myconfig.env.rsync_backup.profiles; + ssh_key_public = myconfig.env.rsync_backup.ssh_key.public; + ssh_key_private = myconfig.env.rsync_backup.ssh_key.private; }; # This value determines the NixOS release with which your system is diff --git a/modules/rsync_backup/default.nix b/modules/rsync_backup/default.nix new file mode 100644 index 0000000..2ff47aa --- /dev/null +++ b/modules/rsync_backup/default.nix @@ -0,0 +1,262 @@ +{ lib, pkgs, config, myconfig, ... }: +let + partModule = lib.types.submodule { + options = { + remote_folder = lib.mkOption { + type = lib.types.path; + description = '' + Path to backup + ''; + }; + exclude_from = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = []; + description = '' + Paths to exclude from the backup + ''; + }; + files_from = lib.mkOption { + type = lib.types.listOf lib.types.path; + default = []; + description = '' + Paths to take for the backup + (if empty: whole folder minus exclude_from) + ''; + }; + args = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + additional arguments for rsync + ''; + }; + }; + }; + profileModule = lib.types.submodule { + options = { + keep = lib.mkOption { + type = lib.types.int; + default = 7; + description = '' + Number of backups to keep + ''; + }; + login = lib.mkOption { + type = lib.types.str; + description = '' + login to connect to + ''; + }; + host = lib.mkOption { + type = lib.types.str; + description = '' + host to connect to + ''; + }; + port = lib.mkOption { + type = lib.types.str; + default = "22"; + description = '' + port to connect to + ''; + }; + host_key = lib.mkOption { + type = lib.types.str; + description = '' + Host key to use as known host + ''; + }; + host_key_type = lib.mkOption { + type = lib.types.str; + description = '' + Host key type + ''; + }; + parts = lib.mkOption { + type = lib.types.attrsOf partModule; + description = '' + folders to backup in the host + ''; + }; + }; + }; + cfg = config.services.rsyncBackup; + + ssh_key = config.secrets.fullPaths."rsync_backup/identity"; + + backup_head = mailto: '' + #!${pkgs.stdenv.shell} + EXCL_FROM=`mktemp` + FILES_FROM=`mktemp` + TMP_STDERR=`mktemp` + + on_exit() { + ${lib.optionalString (mailto != null) '' + MAILTO="${mailto}" + if [ -s "$TMP_STDERR" ]; then + cat "$TMP_STDERR" | ${pkgs.mailutils}/bin/mail -s "save_distant rsync error" "$MAILTO" + fi + ''} + rm -f $TMP_STDERR $EXCL_FROM $FILES_FROM + } + + trap "on_exit" EXIT + + exec 2> "$TMP_STDERR" + exec < /dev/null + + set -e + ''; + + backup_profile = name: profile: builtins.concatStringsSep "\n" ( + [(backup_profile_head name profile)] + ++ lib.mapAttrsToList (backup_part name) profile.parts + ++ [(backup_profile_tail name profile)]); + + backup_profile_head = name: profile: '' + ##### ${name} ##### + PORT="${profile.port}" + DEST="${profile.login}@${profile.host}" + BASE="${cfg.mountpoint}/${name}" + OLD_BAK_BASE=$BASE/older/j + BAK_BASE=''${OLD_BAK_BASE}0 + RSYNC_OUTPUT=$BASE/rsync_output + NBR=${builtins.toString profile.keep} + + if ! ssh \ + -o PreferredAuthentications=publickey \ + -o StrictHostKeyChecking=yes \ + -o ClearAllForwardings=yes \ + -p $PORT \ + -i ${ssh_key} \ + $DEST backup; then + echo "Fichier de verrouillage backup sur $DEST ou impossible de se connecter" >&2 + skip=$DEST + fi + + rm -rf ''${OLD_BAK_BASE}''${NBR} + for j in `seq -w $(($NBR-1)) -1 0`; do + [ ! -d ''${OLD_BAK_BASE}$j ] && continue + mv ''${OLD_BAK_BASE}$j ''${OLD_BAK_BASE}$(($j+1)) + done + mkdir $BAK_BASE + mv $RSYNC_OUTPUT $BAK_BASE + mkdir $RSYNC_OUTPUT + + if [ "$skip" != "$DEST" ]; then + ''; + + backup_profile_tail = name: profile: '' + ssh -i ${ssh_key} -p $PORT $DEST sh -c "date > .cache/last_backup" + fi # [ "$skip" != "$DEST" ] + ##### End ${name} ##### + ''; + + backup_part = profile_name: part_name: part: '' + ### ${profile_name} ${part_name} ### + LOCAL="${part_name}" + REMOTE="${part.remote_folder}" + + if [ ! -d "$BASE/$LOCAL" ]; then + mkdir $BASE/$LOCAL + fi + cd $BASE/$LOCAL + cat > $EXCL_FROM < $FILES_FROM < 0) "\n --exclude-from=$EXCL_FROM \\"}${ + lib.optionalString (builtins.length part.files_from > 0) "\n --files-from=$FILES_FROM \\"} + $DEST:$REMOTE . > $OUT || true + ### End ${profile_name} ${part_name} ### + ''; +in +{ + options.services.rsyncBackup = { + mountpoint = lib.mkOption { + type = lib.types.path; + description = "Path to the base folder for backups"; + }; + mailto = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "E-mail to send the report to"; + }; + profiles = lib.mkOption { + type = lib.types.attrsOf profileModule; + default = {}; + description = '' + Profiles to backup + ''; + }; + ssh_key_public = lib.mkOption { + type = lib.types.str; + description = "Public key for the backup"; + }; + ssh_key_private = lib.mkOption { + type = lib.types.str; + description = "Private key for the backup"; + }; + }; + + config = lib.mkIf (builtins.length (builtins.attrNames cfg.profiles) > 0) { + # FIXME: monitoring to check that backup is less than 14h old + users.users.backup = { + isSystemUser = true; + uid = config.ids.uids.backup; + group = "backup"; + extraGroups = [ "keys" ]; + }; + + users.groups.backup = { + gid = config.ids.gids.backup; + }; + + services.cron.systemCronJobs = let + backup = pkgs.writeScript "backup.sh" (builtins.concatStringsSep "\n" ([ + (backup_head cfg.mailto) + ] ++ lib.mapAttrsToList backup_profile cfg.profiles)); + in [ + '' + 25 3,15 * * * backup ${backup} + '' + ]; + + programs.ssh.knownHosts = lib.attrsets.mapAttrs' (name: profile: lib.attrsets.nameValuePair name { + hostNames = [ profile.host ]; + publicKey = "${profile.host_key_type} ${profile.host_key}"; + }) cfg.profiles; + + system.activationScripts.rsyncBackup = { + deps = [ "users" ]; + text = builtins.concatStringsSep "\n" (map (v: '' + install -m 0700 -o backup -g backup -d ${cfg.mountpoint}/${v} ${cfg.mountpoint}/${v}/older ${cfg.mountpoint}/${v}/rsync_output + '') (builtins.attrNames cfg.profiles) + ); + }; + + secrets.keys = [ + { + dest = "rsync_backup/identity"; + user = "backup"; + group = "backup"; + permissions = "0400"; + text = cfg.ssh_key_private; + } + { + dest = "rsync_backup/identity.pub"; + user = "backup"; + group = "backup"; + permissions = "0444"; + text = cfg.ssh_key_public; + } + ]; + }; +} -- 2.41.0