From 25a49899af60a4c9b163c6ef47a2a6920f5f1109 Mon Sep 17 00:00:00 2001 From: Jake Hillion Date: Sun, 2 Jul 2023 22:43:53 +0100 Subject: [PATCH] tywin: enable git backups --- .../default.nix | 12 +++ modules/backups/default.nix | 1 + modules/backups/git.nix | 102 ++++++++++++++++++ secrets/git/git_backups_ecdsa.age | Bin 0 -> 1651 bytes secrets/git/git_backups_remotes.age | Bin 0 -> 1638 bytes secrets/secrets.nix | 5 +- 6 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 modules/backups/git.nix create mode 100644 secrets/git/git_backups_ecdsa.age create mode 100644 secrets/git/git_backups_remotes.age 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 0000000000000000000000000000000000000000..475663e09480a08f385db408e71a0a951dfedbd1 GIT binary patch literal 1651 zcmYk({nOKg0l@Le81CZEZGbplrge`TNY|%LleEbp?n&DuZPJ&tY15_yMw_N-+N5cc zz9famjj8u|jv)x=<}t03i?UggP6L@59CZ4{WRSpxtfiqO z+ezwRg5%0)H5U=R{vt+_h%eS@6I3XOITXSq$hZ)|?Lx-auGl~aA%&n1$D(o#x0ymc zYO)X(CN#6F6}Y(sIe5DMmFa0WqS1J)+z28I_>wF6I5CKPDnf1W$)yU)Yz# z&3J&W!={y|wKz@c-Humo07;pN3m{_l@{EGXC>l^Hr=KdrO$CjSG{bdx&>bmC5aOT# zu|UP~wiB%ex|CgR_Y!g#1#Ft@SHiv)u7f4bi$e|5LSj-a?XA-+ttv?tSNTk!<%Lwy zEjMjnR!YcN4v%zNDZ541{6L$o2Sp!~CSwT;q(ZnZX-=KWD>PHIy>_Uj`7>EQO(opn zv>+LlkgCyELMWPkw1wtPFAZd~yb)&-MJQ^PxDqGGp){X2@|squ8H|g_ zg~d=eLxf5ZmStVBwk7#ZZ4l)l+1GO0z!foe&HO3{*^bwf_AoX85M-eZyg zA_E?e$JI~*4cZ%xyQ5v2Om*SDUlzJO;_K%TsjjemnI}5=RL+Y)BCzI1k*l9o;miInd^yTCB z@>Rpn{b2LosCBN}(7pMGcW?sMDzvUcVG z`ZMXCf!|;IWvaWyl{(rBrYfcL4{Nl(b z*F0b}xM_8)u=0+1FuFH$dIJVYNyQp@sW`KMJG3(89vc0;eyevze`b0J-+t)!%GGcD zH3r<(J#+c|{BJ3qZ+|ece%HhXm-4+Oi^aFM=5Fmj`_ozHM_-y3xcjT02MRB*9Ark= z``#HD+A%7w>U(Z@W^LW_?&*mq2mY||G_-ecaPgIMJI0oHW{L;rZag(vpMMwP9$fTR z`JTNeZ`*$E$Q0=*`T{-@wxoXqB1G64p zaCu$p{m(x+wcz5-O~bE>8xM@%y=3mxtG6DjzxKtN;TL1m&poU(zp literal 0 HcmV?d00001 diff --git a/secrets/git/git_backups_remotes.age b/secrets/git/git_backups_remotes.age new file mode 100644 index 0000000000000000000000000000000000000000..99b1723459c46502f19d8df438d9d5717455dfa8 GIT binary patch literal 1638 zcmYk(`*YKV0l@LJM;SF7tQ&BFjo~yr+IdEP$hNdKC9?dK5?g-Al3d!<56KVNk}X-5 z?CW5KqoX|T+R%eiQpl)*TzkE=>jHy9;UFa~(3JqAdr%6!;{q%sYa2#kJjU&Acl!sv zKYTxl3~OYClvFEKa-y(AlrliotTR5Xm*uQck`sEbMtHE0u2k|_F>Ai;M^g%kgwtg~ z#Z-*IilrD>@*`3dEQD9RfQ0%T_})Kdusyk^+RP&z!r=QPHx;w&f|1ThU} zJ2+g;!M-BvEh0=9btUbQj%<~#YatUAjMGZE3b{>?3CMzlYTm8W07ca zXdztjSzQAlb#pCcl69s^!U}T$UI2Zr zavt=R1l)?s9exrI6!R&?>$D?THUrc!b2Vf(vu4nu@hBES-1Rc*j61A4TM6JrS3&d! zi4tA3MVN@}$szxL8;6UOS=C`sCF$%CoJqFmmvcTvA{?YCmdwaJ6xV8qA(&y^HHV5b zR@S8^>;*bft})(dHAWdC6=ylGOSv(aNOxr7F0}(mCqwCcg{kK)xmqM_%6ou_l6X)Q zBaoT1lNenhY!pdJbt0}(QL8Cj24`QNmohRz$ku1$1bO3?r6c)(iA=Ht209($Rs$|&0 zF(m;Eah+-fgZyMi!OUuA03#3*WCU0#=rl3}+ZCHq$flrphP80G7WIezcbiRHY&J8j z7pN-SiRjrJp;F;=G3;>%%6Kd)g!nY*M`#K4airB!&@{4;r5rfn&FJuS7Qu@qmx97j zN6-am#+4`M2V43s&nR`@9+|v{nuK*KW1CtAuOE2+__yc3 zNDt71;J-%c!98Tvy|_2^=&2pG)kBe2 zKW;;2d8WQPh68!=Q)jLZx6VJ)wyrJm+VcLw*_G^(<^Qg|@%0q&$i#x% zm-p{I_Wm*Vwb73r`|Rx_UC)SX&v4%j-&u7s+q@JWyZRQeMf~yjlebqr^p{K3S&LgI zZCRo&y4BE~4wM=!h7LXZ?9>J|ukD5Uj?pHJy_KOj1(dvnI z_~`cE4J>I)+OQ{m>uzcN1=CY~-ur)-e!1h$q&Gjjx_IHp`|_*tYx=KW%IyC6%@d=R z#p@@3Vwo;slV`*4d^JN_mA&@z>`UFJ&J&-X-CXMHIf%0HN1p4lrsBJ%H)eaLE}izp zqR(2#_ibuveT{wMC++6V3pVtD?dstRdFSdwQ)1`7nX+$WCV{&qurNFA;!ii%XI>!> zTn=nKd*z{%%Ll2U=2r%i$K&(%9NdTtk00dxO~l1d`Wke%^Me6o{`Rroz8E^Nf+Knk z?Haq#b)#+g^!N`J&fdMHU+Mj{-`3cF=k*1-!RiOCL3K>p6ymi*mg{}JgkCnEsTf6L^vj5UF rdehGJ;+NNd1vFL10sQ;st|55k8ntcLTH8?1ALg!u{5Mu!Ub*o