From 527fb19cb773ea3b93bb95c49b2a5edebbd0f2b4 Mon Sep 17 00:00:00 2001
From: Florian Sesser <florian@private.storage>
Date: Tue, 3 May 2022 17:04:37 +0000
Subject: [PATCH] Backup: Start at a different time of day per machine

---
 morph/lib/borgbackup.nix |  11 +-
 nixos/lib/ip-util.nix    | 219 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 229 insertions(+), 1 deletion(-)
 create mode 100644 nixos/lib/ip-util.nix

diff --git a/morph/lib/borgbackup.nix b/morph/lib/borgbackup.nix
index 30e26cf9..72861dca 100644
--- a/morph/lib/borgbackup.nix
+++ b/morph/lib/borgbackup.nix
@@ -8,6 +8,12 @@ let
   cfg = config.services.private-storage.borgbackup;
   inherit (config.grid) publicKeyPath privateKeyPath;
 
+  # Get a per-host number of hours to start the backup at a
+  # time that should be "night" in most of the USA:
+  ip-util = import ../../nixos/lib/ip-util.nix;
+  backupDelayHours = with builtins; bitAnd (ip-util.fromHexString
+    (hashString "md5" config.networking.hostName)) 15;
+
 in {
   options.services.private-storage.borgbackup = {
     enable = lib.mkEnableOption "Borgbackup daily backup job";
@@ -50,7 +56,10 @@ in {
           BORG_RSH = "ssh -i /run/keys/borgbackup/ssh-key";
         };
         compression = "none";
-        startAt = "*-*-* 05:00:00 UTC";
+
+        # Start the backup at a different time per machine,
+        # and not at the full hour, but somewhat later
+        startAt = "*-*-* " + toString backupDelayHours + ":22:11 UTC";
       };
     };
   };
diff --git a/nixos/lib/ip-util.nix b/nixos/lib/ip-util.nix
new file mode 100644
index 00000000..9d9c4789
--- /dev/null
+++ b/nixos/lib/ip-util.nix
@@ -0,0 +1,219 @@
+# Thank you: https://gist.github.com/petabyteboy/558ffddb9aeb24e1eab2d5d6d021b5d7
+
+with import <nixpkgs/lib>;
+
+rec {
+  # FIXME: add case for negative numbers
+  pow = base: exponent: if exponent == 0 then 1 else fold (
+    x: y: y * base
+  ) base (
+    range 2 exponent
+  );
+
+  fromHexString = hex: foldl (
+    x: y: 16 * x + (
+      (
+        listToAttrs (
+          map (
+            x: nameValuePair (
+              toString x
+            ) x
+          ) (
+            range 0 9
+          )
+        ) // {
+          "a" = 10;
+          "b" = 11;
+          "c" = 12;
+          "d" = 13;
+          "e" = 14;
+          "f" = 15;
+        }
+      ).${y}
+    )
+  ) 0 (
+    stringToCharacters (
+      removePrefix "0x" (
+        hex
+      )
+    )
+  );
+
+  ipv4 = rec {
+
+    decode = address: foldl (
+      x: y: 256 * x + y
+    ) 0 (
+      map toInt (
+        splitString "." address
+      )
+    );
+
+    encode = num: concatStringsSep "." (
+      map (
+        x: toString (mod (num / x) 256)
+      ) (
+        reverseList (
+          genList (
+            x: pow 2 (x * 8)
+          ) 4
+        )
+      )
+    );
+
+    netmask = prefixLength: (
+      foldl (
+        x: y: 2 * x + 1
+      ) 0 (
+        range 1 prefixLength
+      )
+    ) * (
+      pow 2 (
+        32 - prefixLength
+      )
+    );
+
+    reverseZone = net: (
+      concatStringsSep "." (
+        reverseList (
+          splitString "." net
+        )
+      )
+    ) + ".in-addr.arpa";
+
+    eachAddress = net: prefixLength: genList (
+      x: decode (
+        x + (
+          decode net
+        )
+      )
+    ) (
+      pow 2 (
+        32 - prefixLength
+      )
+    );
+
+    networkOf = address: prefixLength: encode (
+      bitAnd (
+        decode address
+      ) (
+        netmask prefixLength
+      )
+    );
+
+    isInNetwork = net: address: networkOf address == net;
+
+    /* nixos-specific stuff */
+
+    findOwnAddress = config: net: head (
+      filter (
+        isInNetwork net
+      ) (
+        configuredAddresses config
+      )
+    );
+
+    configuredAddresses = config: concatLists (
+      mapAttrsToList (
+        name: iface: iface.ipv4.addresses
+      ) config.networking.interfaces
+    );
+
+  };
+
+  ipv6 = rec {
+
+    expand = address: (
+      replaceStrings ["::"] [(
+        concatStringsSep "0" (
+          genList (x: ":") (
+            9 - (count (x: x == ":") (stringToCharacters address))
+          )
+        )
+      )] address
+    ) + (
+      if hasSuffix "::" address then
+        "0"
+      else
+        ""
+    );
+
+    decode = address: map fromHexString (
+      splitString ":" (
+        expand address
+      )
+    );
+
+    encode = address: toLower (
+      concatStringsSep ":" (
+        map toHexString address
+      )
+    );
+
+    netmask = prefixLength: map (
+      x: if prefixLength > x + 16 then
+        (pow 2 16) - 1
+      else if prefixLength < x then
+        0
+      else
+        (
+          foldl (
+            x: y: 2 * x + 1
+          ) 0 (
+            range 1 (prefixLength - x)
+          )
+        ) * (
+          pow 2 (
+            16 - (prefixLength - x)
+          )
+        )
+    ) (
+      genList (
+        x: x * 16
+      ) 8
+    );
+
+    reverseZone = net: (
+      concatStringsSep "." (
+        concatLists (
+          reverseList (
+            map (
+              x: stringToCharacters (fixedWidthString 4 "0" x)
+            ) (
+              splitString ":" (
+                expand net
+              )
+            )
+          )
+        )
+      )
+    ) + ".ip6.arpa";
+
+    networkOf = address: prefixLength: encode (
+      zipListsWith bitAnd (
+        decode address
+      ) (
+        netmask prefixLength
+      )
+    );
+
+    isInNetwork = net: address: networkOf address == (expand net);
+
+    /* nixos-specific stuff */
+
+    findOwnAddress = config: net: head (
+      filter (
+        isInNetwork net
+      ) (
+        configuredAddresses config
+      )
+    );
+
+    configuredAddresses = config: concatLists (
+      mapAttrsToList (
+        name: iface: iface.ipv6.addresses
+      ) config.networking.interfaces
+    );
+
+  };
+}
-- 
GitLab