aboutsummaryrefslogblamecommitdiff
path: root/flakes/rsync_backup/flake.nix
blob: d81d1762318f26b579fc1a5485d6e9ff2afe7c16 (plain) (tree)
























































































































































































                                                                                                     
                                                                                                                                              











































                                                                                                                                                                                      
                                                













                                                                                                                                              
{
  description = "Rsync backups";

  outputs = { self }: {
    nixosModule = { lib, pkgs, config, ... }: {
      options.services.rsyncBackup = {
        mountpoint = lib.mkOption {
          type = lib.types.path;
          description = "Path to the base folder for backups";
        };
        profiles = lib.mkOption {
          type = lib.types.attrsOf (lib.types.submodule {
            options = {
              keep = lib.mkOption {
                type = lib.types.int;
                default = 7;
                description = ''
                  Number of backups to keep
                  '';
              };
              check_command = lib.mkOption {
                type = lib.types.str;
                default = "backup";
                description = ''
                  command to check if backup needs to be done
                '';
              };
              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 (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
                        '';
                    };
                  };
                });
                description = ''
                  folders to backup in the host
                  '';
              };
            };
          });
          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 =
        let
          cfg = config.services.rsyncBackup;
        in
          lib.mkIf (builtins.length (builtins.attrNames cfg.profiles) > 0) {
            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
              ssh_key = cfg.ssh_key_private;
              backup_head = ''
                #!${pkgs.stdenv.shell}
                EXCL_FROM=`mktemp`
                FILES_FROM=`mktemp`
                TMP_STDERR=`mktemp`

                on_exit() {
                  if [ -s "$TMP_STDERR" ]; then
                    cat "$TMP_STDERR"
                  fi
                  rm -f $TMP_STDERR $EXCL_FROM $FILES_FROM
                }

                trap "on_exit" EXIT

                exec 2> "$TMP_STDERR"
                exec < /dev/null

                set -e
                '';
              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 \
                      -o UserKnownHostsFile=/dev/null \
                      -o CheckHostIP=no \
                      -p $PORT \
                      -i ${ssh_key} \
                      $DEST ${profile.check_command}; 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 -o UserKnownHostsFile=/dev/null -o CheckHostIP=no -i ${ssh_key} -p $PORT $DEST sh -c "date > .cache/last_backup" || true
                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 --new-compress -XAavbr --fake-super -e "ssh -o UserKnownHostsFile=/dev/null -o CheckHostIP=no -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} ###
              '';
              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 = pkgs.writeScript "backup.sh" (builtins.concatStringsSep "\n" ([
                backup_head
              ] ++ lib.mapAttrsToList backup_profile cfg.profiles));
            in [
              ''
                25 3,15 * * * backup ${backup}
                ''
            ];

            programs.ssh.knownHosts = lib.attrsets.mapAttrs' (name: profile: lib.attrsets.nameValuePair name {
              extraHostNames = [ 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)
                );
            };
          };
      };
  };
}