aboutsummaryrefslogblamecommitdiff
path: root/modules/backups.nix
blob: 0724053a29819fd43bccf89a31482398a7b8b2e7 (plain) (tree)



















































































































































































































                                                                                                
# Some examples for how to use this module
#
# Host with media files - backup /media only locally
# my.modules.backups = {
#   enable = true;
#   passwordFile = config.age.secrets.restic_password.path
#   local.paths = [ "/media" "/home" "/var/lib/important" ];
#   remote.paths = [ "/home" "/var/lib/important" ];  # Excludes /media
# };
#
# Another example - different exclusions for local vs remote
# my.modules.backups = {
#   enable = true;
#   passwordFile = config.age.secrets.restic_password.path
#   local.paths = [ "/home" "/var/cache/downloads" ];
#   local.exclude = [ "*.tmp" ];
#   remote.paths = [ "/home" ];  # Skip cache directory for remote
#   remote.exclude = [ "*.tmp" "*.log" ];  # More aggressive exclusions for remote
# };
{
  pkgs,
  config,
  lib,
  ...
}:
let
  cfg = config.my.modules.backups;

  # Helper scripts for easy backup access
  restic-local = pkgs.writeShellScriptBin "restic-local" ''
    export RESTIC_REPOSITORY="${cfg.localBasePath}/${config.networking.hostName}"
    export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
    exec ${pkgs.restic}/bin/restic "$@"
  '';

  restic-remote = pkgs.writeShellScriptBin "restic-remote" ''
    export RESTIC_REPOSITORY="${cfg.remoteBaseRepository}:/${config.networking.hostName}/"
    export RESTIC_PASSWORD_FILE="${cfg.passwordFile}"
    ${lib.optionalString (cfg.remote.environmentFile != null) ''
      source ${cfg.remote.environmentFile}
    ''}
    exec ${pkgs.restic}/bin/restic "$@"
  '';

  # Common backup options shared between local and remote
  backupOptions = {
    paths = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      default = [ ];
      description = "Paths to backup";
      example = [
        "/home"
        "/var/lib/important-data"
      ];
    };

    exclude = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      default = [ ];
      description = "Paths to exclude from backup";
      example = [
        "*.tmp"
        "/var/cache"
      ];
    };

    extraBackupArgs = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      default = [
        "--exclude-caches"
        "--compression=max"
      ];
      description = "Additional arguments to pass to restic backup";
    };

    pruneOpts = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      default = [
        "--keep-daily 7"
        "--keep-weekly 4"
        "--keep-monthly 3"
      ];
      description = "Pruning options for old backups";
    };

    timerConfig = lib.mkOption {
      type = lib.types.attrs;
      default = {
        OnCalendar = "daily";
        RandomizedDelaySec = "5m";
      };
      description = "Systemd timer configuration";
    };
  };
in
{
  options.my.modules.backups = {
    enable = lib.mkEnableOption "backups";

    passwordFile = lib.mkOption {
      type = lib.types.str;
      default = config.age.secrets.restic_password.path;
      description = "Path to file containing restic repository password";
      example = "/run/secrets/restic-password";
    };

    localBasePath = lib.mkOption {
      type = lib.types.str;
      default = "/data/backups";
      description = "Base path for local backup repositories";
      example = "/mnt/backup-drive/backups";
    };

    remoteBaseRepository = lib.mkOption {
      type = lib.types.str;
      default = "gs:fcuny-infra-backups";
      description = "Base repository URL for remote backups";
      example = "s3:my-backup-bucket";
    };

    local = backupOptions;

    remote = backupOptions // {
      timerConfig = lib.mkOption {
        type = lib.types.attrs;
        default = {
          OnCalendar = "daily";
          # No randomized delay for remote to avoid overlap with local
        };
        description = "Systemd timer configuration for remote backups";
      };

      googleProjectId = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = "fcuny-infra";
        description = "Google Cloud project ID for GCS backups";
        example = "my-backup-project";
      };

      googleCredentialsFile = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = config.age.secrets.restic_gcs_credentials.path;
        description = "Path to Google Cloud service account credentials file";
        example = "/run/secrets/gcs-credentials";
      };

      environmentFile = lib.mkOption {
        type = lib.types.nullOr lib.types.path;
        default =
          if cfg.remote.googleProjectId != null && cfg.remote.googleCredentialsFile != null then
            pkgs.writeText "restic-gcs-env" ''
              GOOGLE_PROJECT_ID=${cfg.remote.googleProjectId}
              GOOGLE_APPLICATION_CREDENTIALS=${cfg.remote.googleCredentialsFile}
            ''
          else
            null;
        description = "Environment file for remote backup authentication";
      };
    };

    helpers = lib.mkOption {
      type = lib.types.bool;
      default = true;
      description = "Install helper scripts (restic-local, restic-remote)";
    };
  };

  config = lib.mkIf cfg.enable {
    environment.systemPackages =
      [
        pkgs.restic
      ]
      ++ lib.optionals cfg.helpers [
        restic-local
        restic-remote
      ];

    services.restic.backups = lib.mkMerge [
      # Local backup configuration - only if paths are specified
      (lib.mkIf (cfg.local.paths != [ ]) {
        local = {
          initialize = true;
          repository = "${cfg.localBasePath}/${config.networking.hostName}";
          passwordFile = cfg.passwordFile;
          paths = cfg.local.paths;
          exclude = cfg.local.exclude;
          extraBackupArgs = cfg.local.extraBackupArgs;
          timerConfig = cfg.local.timerConfig;
          pruneOpts = cfg.local.pruneOpts;
        };
      })

      # Remote backup configuration - only if paths are specified
      (lib.mkIf (cfg.remote.paths != [ ]) {
        remote =
          {
            initialize = true;
            repository = "${cfg.remoteBaseRepository}:/${config.networking.hostName}/";
            passwordFile = cfg.passwordFile;
            paths = cfg.remote.paths;
            exclude = cfg.remote.exclude;
            extraBackupArgs = cfg.remote.extraBackupArgs;
            timerConfig = cfg.remote.timerConfig;
            pruneOpts = cfg.remote.pruneOpts;
          }
          // lib.optionalAttrs (cfg.remote.environmentFile != null) {
            environmentFile = toString cfg.remote.environmentFile;
          };
      })
    ];
  };
}