aboutsummaryrefslogblamecommitdiff
path: root/systems/eldiron/duply_backup.nix
blob: 51433029feaab0e97668e218570890bda7868ebe (plain) (tree)
1
2
3
4
5
                                 

   
                            
                            



















































































                                                                                                                                                              
                               



                                                                 

                               
                      
 







                                                                                             
     


             
                                                





                                         
                                                  

                                                     













                                                

                                  
                                 



                            

                                 





                                                  



                                       






                                                  






                                            







                                                                             




          
                                                        


                                                                   
                                                                          



                                                                                            
                               
                                              
          
                                                            
                               



                                                                                                                                                            
          




                                                        
                               
















                                                                                                               
                       
          





















                                                                         
 















                                                                                                          

                     


                                                            


      

    
{ lib, pkgs, config, name, ... }:

let
  cfg = config.myEnv.backup;
  varDir = "/var/lib/duply";
  default_action = "pre_bkp_purge_purgeFull_purgeIncr";
  duply_backup_full_with_ignored = pkgs.writeScriptBin "duply_full_with_ignored" ''
    #!${pkgs.stdenv.shell}

    export DUPLY_FULL_BACKUP_WITH_IGNORED=yes
    if [ -z "$1" -o "$1" = "-h" -o "$1" = "--help" ]; then
      echo "duply_full_with_ignored /path/to/profile"
      echo "Does a full backup including directories with .duplicity-ignore"
      exit 1
    fi
    ${pkgs.duply}/bin/duply "$1" pre_full --force
  '';
  duply_backup = pkgs.writeScriptBin "duply_backup" ''
    #!${pkgs.stdenv.shell}

    declare -a profiles
    profiles=()
    ${builtins.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList (k: v: map (remote: [
      ''profiles+=("${remote}_${k}")''
    ]) v.remotes) config.services.duplyBackup.profiles))}

    if [ -f "${varDir}/last_backup_profile" ]; then
      last_backup=$(cat ${varDir}/last_backup_profile)
      for i in "''${!profiles[@]}"; do
        if [[ "''${profiles[$i]}" = "$last_backup" ]]; then
          break
        fi
      done
      ((i+=1))
      profiles=("''${profiles[@]:$i}" "''${profiles[@]:0:$i}")
    fi

    # timeout in minutes
    timeout="''${1:-180}"
    timeout_timestamp=$(date +%s -d "$timeout minutes")
    for profile in "''${profiles[@]}"; do
      if [ $(date +%s -d "now") -ge "$timeout_timestamp" ]; then
        break
      fi

      touch "${varDir}/$profile.log"
      ${pkgs.duply}/bin/duply ${config.secrets.location}/backup/$profile/ ${default_action} --force >> ${varDir}/$profile.log
      [[ $? = 0 ]] || echo -e "Error when doing backup for $profile, see above or logs in ${varDir}/$profile.log\n---------------------------------------" >&2
      echo "$profile" > ${varDir}/last_backup_profile
    done
  '';

  check_backups = pkgs.writeScriptBin "duply_list_not_backuped" ''
    #!${pkgs.stdenv.shell}

    do_check() {
      local dir="$1" path ignored_path
      find "$dir" -mindepth 1 -maxdepth 1 | while IFS= read -r path; do
        if ${pkgs.gnugrep}/bin/grep -qFx "$path" ${config.secrets.fullPaths."backup/backuped_list"}; then
          continue
        elif ${pkgs.gnugrep}/bin/grep -q "^$path/" ${config.secrets.fullPaths."backup/backuped_list"}; then
          do_check "$path"
        else
          while IFS= read -r ignored_path; do
            if [[ "$path" =~ ^$ignored_path$ ]]; then
              continue 2
            fi
          done < ${config.secrets.fullPaths."backup/ignored_list"}
          printf '%s\n' "$path"
        fi
      done
    }

    do_check /var/lib
  '';
  duplyProfile = profile: remote: bucket: let
    remote' = cfg.remotes.${remote};
  in ''
    if [ -z "$DUPLY_FULL_BACKUP_WITH_IGNORED" ]; then
      GPG_PW="${cfg.password}"
    fi
    TARGET="${remote'.remote bucket}"
    ${lib.optionalString (remote'.remote_type == "s3") ''
    export AWS_ACCESS_KEY_ID="${remote'.s3AccessKeyId}"
    export AWS_SECRET_ACCESS_KEY="${remote'.s3SecretAccessKey}"
    ''}
    ${lib.optionalString (remote'.remote_type == "rsync") ''
    DUPL_PARAMS="$DUPL_PARAMS --ssh-options=-oIdentityFile='${config.secrets.fullPaths."backup/identity"}' "
    ''}
    SOURCE="${profile.rootDir}"
    if [ -z "$DUPLY_FULL_BACKUP_WITH_IGNORED" ]; then
      FILENAME=".duplicity-ignore"
      DUPL_PARAMS="$DUPL_PARAMS --exclude-if-present '$FILENAME'"
    fi
    VERBOSITY=4
    ARCH_DIR="${varDir}/caches"
    DUPL_PYTHON_BIN=""

    # Do a full backup after 6 month
    MAX_FULLBKP_AGE=6M
    DUPL_PARAMS="$DUPL_PARAMS --allow-source-mismatch --full-if-older-than $MAX_FULLBKP_AGE "
    # Backups older than 1months are deleted
    MAX_AGE=1M
    # Keep 1 full backup
    MAX_FULL_BACKUPS=1
    MAX_FULLS_WITH_INCRS=1
  '';
in
{
  options = {
    services.duplyBackup.enable = lib.mkOption {
      type = lib.types.bool;
      default = false;
      description = ''
        Whether to enable remote backups.
      '';
    };
    services.duplyBackup.profiles = lib.mkOption {
      type = lib.types.attrsOf (lib.types.submodule {
        options = {
          hash = lib.mkOption {
            type = lib.types.bool;
            default = true;
            description = ''
              Hash bucket and directory names
            '';
          };
          excludeRootDir = lib.mkOption {
            type = lib.types.bool;
            default = true;
            description = ''
              Exclude root dir in exclusion file
            '';
          };
          rootDir = lib.mkOption {
            type = lib.types.path;
            default = "/var/lib";
            description = ''
              Path to backup
              '';
          };
          bucket = lib.mkOption {
            type = lib.types.str;
            description = ''
              Bucket to use
              '';
          };
          remotes = lib.mkOption {
            type = lib.types.listOf lib.types.str;
            description = ''
              Remotes to use for backup
              '';
          };
          includedPaths = lib.mkOption {
            type = lib.types.listOf lib.types.str;
            default = [];
            description = ''
              Included paths (subdirs of rootDir)
            '';
          };
          excludeFile = lib.mkOption {
            type = lib.types.lines;
            default = "";
            description = ''
              Content to put in exclude file
              '';
          };
          ignoredPaths = lib.mkOption {
            type = lib.types.listOf lib.types.str;
            default = [];
            description = ''
              List of paths to ignore when checking non-backed-up directories
              Can use (POSIX extended) regex
            '';
          };
        };
      });
    };
  };

  config = lib.mkIf config.services.duplyBackup.enable {
    system.activationScripts.backup = ''
      install -m 0700 -o root -g root -d ${varDir} ${varDir}/caches
      '';
    secrets.keys = lib.listToAttrs (lib.flatten (lib.mapAttrsToList (k: v:
      let
        bucket = if v.hash or true then builtins.hashString "sha256" v.bucket else v.bucket;
      in map (remote: [
        (lib.nameValuePair "backup/${remote}_${k}/conf" {
          permissions = "0400";
          text = duplyProfile v remote bucket;
        })
        (lib.nameValuePair "backup/${remote}_${k}/exclude" {
          permissions = "0400";
          text = v.excludeFile + (builtins.concatStringsSep "\n" (map (p: "+ ${v.rootDir}/${p}") v.includedPaths)) + (lib.optionalString v.excludeRootDir ''

            - **
          '');
        })
        (lib.nameValuePair "backup/${remote}_${k}/pre" {
          keyDependencies = [
            pkgs.bash
            pkgs.rsync
          ];
          permissions = "0500";
          text = let
            remote' = cfg.remotes.${remote};
          in ''
            #!${pkgs.stdenv.shell}

            ${lib.optionalString (remote'.remote_type == "rsync") ''
              # Recreate directory structure before synchronizing
              mkdir -p ${varDir}/rsync_remotes/${remote}/${bucket}
              ${pkgs.rsync}/bin/rsync -av -e \
                "ssh -p ${remote'.sshRsyncPort} -oIdentityFile=${config.secrets.fullPaths."backup/identity"}" \
                "${varDir}/rsync_remotes/${remote}/" \
                ${remote'.sshRsyncHost}:
            ''}
          '';
        })
        (lib.nameValuePair "backup/${remote}_${k}" {
          permissions = "0700";
          isDir = true;
        })
    ]) v.remotes) config.services.duplyBackup.profiles)) // {
      "backup/identity" = {
        permissions = "0400";
        text = "{{ .ssl_keys.duply_backup }}";
      };
      "backup/ignored_list" = {
        permissions = "0400";
        text = let
            ignored = map
              (v: map (p: "${v.rootDir}/${p}") v.ignoredPaths)
              (builtins.attrValues config.services.duplyBackup.profiles);
          in builtins.concatStringsSep "\n" (lib.flatten ignored);
      };
      "backup/backuped_list" = {
        permissions = "0400";
        text = let
            included = map
              (v: map (p: "${v.rootDir}/${p}") v.includedPaths)
              (builtins.attrValues config.services.duplyBackup.profiles);
          in builtins.concatStringsSep "\n" (lib.flatten included);
      };
    };

    programs.ssh.knownHostsFiles = [
      (pkgs.writeText
        "duply_backup_known_hosts"
        (builtins.concatStringsSep
          "\n"
          (builtins.filter
            (v: v != null)
            (builtins.map
              (v: v.sshKnownHosts)
              (builtins.attrValues cfg.remotes)
            )
          )
        )
      )
    ];
    environment.systemPackages = [ pkgs.duply check_backups duply_backup_full_with_ignored duply_backup ];
    services.cron = {
      enable = true;
      systemCronJobs = [
        "0 0 * * * root ${duply_backup}/bin/duply_backup 90"
      ];

    };

  };
}