diff options
| author | Franck Cuny <franck@fcuny.net> | 2025-07-25 08:46:32 -0700 |
|---|---|---|
| committer | Franck Cuny <franck@fcuny.net> | 2025-07-25 08:47:10 -0700 |
| commit | db119ccc29ab169a7f35138d5e5d3ba251748ad5 (patch) | |
| tree | c3ac1c0f26dfa1fed38447a27aa2bf4706d3fc84 | |
| parent | add a script to build the VM (diff) | |
| download | infra-db119ccc29ab169a7f35138d5e5d3ba251748ad5.tar.gz | |
add a module for backups
Enable the module on the VM, and backup the git repositories both to the
NAS and to a GCS bucket.
| -rw-r--r-- | machines/nixos/x86_64-linux/vm-synology.nix | 9 | ||||
| -rw-r--r-- | modules/backups.nix | 212 | ||||
| -rw-r--r-- | modules/default.nix | 1 | ||||
| -rw-r--r-- | profiles/git-server.nix | 15 |
4 files changed, 237 insertions, 0 deletions
diff --git a/machines/nixos/x86_64-linux/vm-synology.nix b/machines/nixos/x86_64-linux/vm-synology.nix index 0dfbc14..4b499f2 100644 --- a/machines/nixos/x86_64-linux/vm-synology.nix +++ b/machines/nixos/x86_64-linux/vm-synology.nix @@ -71,5 +71,14 @@ }; }; + my.modules.backups = { + enable = true; + passwordFile = config.age.secrets.restic_password.path; + remote = { + googleProjectId = "fcuny-infra"; + googleCredentialsFile = config.age.secrets.restic_gcs_credentials.path; + }; + }; + system.stateVersion = "23.11"; # Did you read the comment? } diff --git a/modules/backups.nix b/modules/backups.nix new file mode 100644 index 0000000..0724053 --- /dev/null +++ b/modules/backups.nix @@ -0,0 +1,212 @@ +# 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; + }; + }) + ]; + }; +} diff --git a/modules/default.nix b/modules/default.nix index 441a9b8..b8e8d0b 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -4,5 +4,6 @@ ./home.nix ./host-config.nix ./nas-client.nix + ./backups.nix ]; } diff --git a/profiles/git-server.nix b/profiles/git-server.nix index 27eebc7..6f523a8 100644 --- a/profiles/git-server.nix +++ b/profiles/git-server.nix @@ -22,4 +22,19 @@ defaultBranch = main ''}" ]; + + my.modules.backups = { + local.paths = [ "/var/lib/gitolite" ]; + local.exclude = [ + "/var/lib/gitolite/.bash_history" + "/var/lib/gitolite/.ssh" + "/var/lib/gitolite/.viminfo" + ]; + remote.paths = [ "/var/lib/gitolite" ]; + remote.exclude = [ + "/var/lib/gitolite/.bash_history" + "/var/lib/gitolite/.ssh" + "/var/lib/gitolite/.viminfo" + ]; + }; } |
