diff --git a/hosts/tywin.storage.ts.hillion.co.uk/default.nix b/hosts/tywin.storage.ts.hillion.co.uk/default.nix index 1fc312c..2526aa8 100644 --- a/hosts/tywin.storage.ts.hillion.co.uk/default.nix +++ b/hosts/tywin.storage.ts.hillion.co.uk/default.nix @@ -43,6 +43,17 @@ fileSystems."/mnt/d0".options = [ "x-systemd.mount-timeout=3m" ]; + ## Backups + ### Git + age.secrets."git/git_backups_ecdsa".file = ../../secrets/git/git_backups_ecdsa.age; + age.secrets."git/git_backups_remotes".file = ../../secrets/git/git_backups_remotes.age; + custom.backups.git = { + enable = true; + sshKey = config.age.secrets."git/git_backups_ecdsa".path; + reposFile = config.age.secrets."git/git_backups_remotes".path; + repos = [ "https://gitea.hillion.co.uk/JakeHillion/nixos.git" ]; + }; + ## Resilio custom.resilio.enable = true; @@ -107,6 +118,7 @@ }; pruneOpts = [ + "--keep-last 48" "--keep-within-hourly 7d" "--keep-within-daily 1m" "--keep-within-weekly 6m" diff --git a/modules/backups/default.nix b/modules/backups/default.nix index db56aad..9b90f7e 100644 --- a/modules/backups/default.nix +++ b/modules/backups/default.nix @@ -2,6 +2,7 @@ { imports = [ + ./git.nix ./matrix.nix ]; } diff --git a/modules/backups/git.nix b/modules/backups/git.nix new file mode 100644 index 0000000..5aaa407 --- /dev/null +++ b/modules/backups/git.nix @@ -0,0 +1,102 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.custom.backups.git; +in +{ + options.custom.backups.git = { + enable = lib.mkEnableOption "git"; + + repos = lib.mkOption { + description = "A list of remotes to clone."; + type = with lib.types; listOf str; + default = [ ]; + }; + reposFile = lib.mkOption { + description = "A file containing the remotes to clone, one per line."; + type = with lib.types; nullOr str; + default = null; + }; + sshKey = lib.mkOption { + description = "SSH private key to use when cloning repositories over SSH."; + type = with lib.types; nullOr str; + default = null; + }; + }; + + config = lib.mkIf cfg.enable { + age.secrets."git-backups/restic/128G".file = ../../secrets/restic/128G.age; + + systemd.services.backup-git = { + description = "Git repo backup service."; + + serviceConfig = { + DynamicUser = true; + + CacheDirectory = "backup-git"; + WorkingDirectory = "%C/backup-git"; + + LoadCredential = [ + "restic_password:${config.age.secrets."git-backups/restic/128G".path}" + ] ++ (if cfg.sshKey == null then [ ] else [ "id_ecdsa:${cfg.sshKey}" ]) + ++ (if cfg.reposFile == null then [ ] else [ "repos_file:${cfg.reposFile}" ]); + }; + + environment = { + GIT_SSH_COMMAND = "${pkgs.openssh}/bin/ssh -i %d/id_ecdsa"; + RESTIC_PASSWORD_FILE = "%d/restic_password"; + }; + + script = '' + shopt -s nullglob + + # Read and deduplicate repos + ${if cfg.reposFile == null then "" else "readarray -t raw_repos < $CREDENTIALS_DIRECTORY/repos_file"} + declare -A repos=(${builtins.concatStringsSep " " (builtins.map (x : "[${x}]=1") cfg.repos)}) + for repo in ''${raw_repos[@]}; do repos[$repo]=1; done + + # Clean up existing repos + declare -A dirs + for d in *; do + origin=$(cd $d && ${pkgs.git}/bin/git remote get-url origin) + if ! [ -n "''${repos[$origin]}" ]; then + echo "$origin removed from config, cleaning up..." + rm -rf $d + else + dirs[$origin]=$d + fi + done + + # Update repos + EXIT_CODE=0 + for repo in "''${!repos[@]}"; do + if [ -n "''${dirs[$repo]}" ]; then + if ! (cd ''${dirs[$repo]} && ${pkgs.git}/bin/git remote update); then EXIT_CODE=1; fi + else + if ! (${pkgs.git}/bin/git clone --mirror $repo); then EXIT_CODE=1; fi + fi + done + + # Backup to Restic + ${pkgs.restic}/bin/restic \ + -r rest:http://restic.tywin.storage.ts.hillion.co.uk/128G \ + --cache-dir .restic --exclude .restic \ + backup . + + if test $EXIT_CODE -ne 0; then + echo "Some repositories failed to clone!" + exit $EXIT_CODE + fi + ''; + }; + systemd.timers.backup-git = { + wantedBy = [ "timers.target" ]; + timerConfig = { + Persistent = true; + OnUnitInactiveSec = "15m"; + RandomizedDelaySec = "5m"; + Unit = "backup-git.service"; + }; + }; + }; +} diff --git a/secrets/git/git_backups_ecdsa.age b/secrets/git/git_backups_ecdsa.age new file mode 100644 index 0000000..475663e Binary files /dev/null and b/secrets/git/git_backups_ecdsa.age differ diff --git a/secrets/git/git_backups_remotes.age b/secrets/git/git_backups_remotes.age new file mode 100644 index 0000000..99b1723 Binary files /dev/null and b/secrets/git/git_backups_remotes.age differ diff --git a/secrets/secrets.nix b/secrets/secrets.nix index f0fdf6a..08d9d6c 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -62,11 +62,14 @@ in # Backblaze Secrets "backblaze/vm-strangervm-backups-matrix.age".publicKeys = jake_users ++ [ ts.strangervm.vm ]; - # Restic Secrets + # Backups Secrets "restic/b2-backups-matrix.age".publicKeys = jake_users ++ [ ts.strangervm.vm ]; "restic/128G.age".publicKeys = jake_users ++ [ ts.storage.tywin ]; "restic/1.6T.age".publicKeys = jake_users ++ [ ts.storage.tywin ]; + "git/git_backups_ecdsa.age".publicKeys = jake_users ++ [ ts.storage.tywin ]; + "git/git_backups_remotes.age".publicKeys = jake_users ++ [ ts.storage.tywin ]; + # Spotify Secrets "spotify/11132032266.age".publicKeys = jake_users ++ [ ts.terminals.jakehillion.gendry ];