]> git.immae.eu Git - perso/Immae/Config/Nix.git/commitdiff
Add rsync backup
authorIsmaël Bouya <ismael.bouya@normalesup.org>
Wed, 23 Oct 2019 22:36:35 +0000 (00:36 +0200)
committerIsmaël Bouya <ismael.bouya@normalesup.org>
Wed, 23 Oct 2019 22:36:35 +0000 (00:36 +0200)
modules/default.nix
modules/private/system/backup-2.nix
modules/rsync_backup/default.nix [new file with mode: 0644]

index 18bee9af14fa14504ff57b69a99b426fe975e256..9ff6ea62a2096f4f7f0b02439407d7911ad25a59 100644 (file)
@@ -14,6 +14,7 @@
   openarc = ./openarc.nix;
 
   duplyBackup = ./duply_backup;
+  rsyncBackup = ./rsync_backup;
   naemon = ./naemon;
 
   php-application = ./websites/php-application.nix;
index 80fa36df1253fa883eca1e1371a34720a41c8587..615167150a10c27b4788fd90bc463f078d75e006 100644 (file)
     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 (file)
index 0000000..2ff47aa
--- /dev/null
@@ -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 <<EOF
+      ${builtins.concatStringsSep "\n" part.exclude_from}
+      EOF
+      cat > $FILES_FROM <<EOF
+      ${builtins.concatStringsSep "\n" part.files_from}
+      EOF
+
+      OUT=$RSYNC_OUTPUT/$LOCAL
+      ${pkgs.rsync}/bin/rsync -XAavbrz --fake-super -e "ssh -i ${ssh_key} -p $PORT" --numeric-ids --delete \
+        --backup-dir=$BAK_BASE/$LOCAL \${
+        lib.optionalString (part.args != null) "\n  ${part.args} \\"}${
+        lib.optionalString (builtins.length part.exclude_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;
+      }
+    ];
+  };
+}