From f6e600c20d6a97ebeda23fa2bb5621646222b2b0 Mon Sep 17 00:00:00 2001 From: Gregor Kleen Date: Sat, 2 Jan 2021 20:53:17 +0100 Subject: sif: import config --- modules/borgbackup/btrfs-snapshots.nix | 52 +++++++++ modules/borgbackup/default.nix | 199 +++++++++++++++++++++++++++++++++ modules/borgbackup/lvm-snapshots.nix | 133 ++++++++++++++++++++++ 3 files changed, 384 insertions(+) create mode 100644 modules/borgbackup/btrfs-snapshots.nix create mode 100644 modules/borgbackup/default.nix create mode 100644 modules/borgbackup/lvm-snapshots.nix (limited to 'modules/borgbackup') diff --git a/modules/borgbackup/btrfs-snapshots.nix b/modules/borgbackup/btrfs-snapshots.nix new file mode 100644 index 00000000..96d2b2ba --- /dev/null +++ b/modules/borgbackup/btrfs-snapshots.nix @@ -0,0 +1,52 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.btrfs-snapshots; + + snapshotMount = str: "${str}${cfg.mountSuffix}"; +in { + options = { + + services.btrfs-snapshots = { + enable = mkEnableOption "a systemd unit for btrfs snapshots"; + + mountSuffix = mkOption { + type = types.str; + default = ".snapshot"; + }; + + readOnly = mkOption { + type = types.bool; + default = true; + }; + + persist = mkOption { + type = types.bool; + default = false; + }; + }; + + }; + + + config = mkIf cfg.enable { + systemd.services."btrfs-snapshot@" = { + enable = true; + + unitConfig = { + StopWhenUnneeded = !cfg.persist; + }; + + serviceConfig = with pkgs; { + Type = "oneshot"; + ExecStartPre = "-${btrfs-progs}/bin/btrfs subvolume delete -c ${snapshotMount "%f"}"; + ExecStart = "${btrfs-progs}/bin/btrfs subvolume snapshot ${optionalString cfg.readOnly "-r"} %f ${snapshotMount "%f"}"; + RemainAfterExit = true; + ExecStop = "${btrfs-progs}/bin/btrfs subvolume delete -c ${snapshotMount "%f"}"; + }; + }; + + }; +} diff --git a/modules/borgbackup/default.nix b/modules/borgbackup/default.nix new file mode 100644 index 00000000..47f8e06d --- /dev/null +++ b/modules/borgbackup/default.nix @@ -0,0 +1,199 @@ +{ config, lib, utils, pkgs, ... }: + +with utils; +with lib; + +let + cfg = config.services.borgbackup; + + lvmPath = { + options = { + LV = mkOption { + type = types.str; + }; + VG = mkOption { + type = types.str; + }; + }; + }; + + pathType = if cfg.snapshots == "lvm" then types.submodule lvmPath else types.path; + + systemdPath = path: escapeSystemdPath (if cfg.snapshots == "lvm" then "${path.VG}-${path.LV}" else path); + + withSuffix = path: path + (if cfg.snapshots == "btrfs" then config.services.btrfs-snapshots.mountSuffix else config.services.lvm-snapshots.mountSuffix); + + mountPoint = if cfg.snapshots == "lvm" then config.services.lvm-snapshots.mountPoint else ""; + + targetOptions = { + options = { + repo = mkOption { + type = types.str; + }; + + paths = mkOption { + type = types.listOf pathType; + default = []; + }; + + prune = mkOption { + type = types.attrsOf (types.listOf types.str); + default = {}; + }; + + interval = mkOption { + type = types.str; + default = "6h"; + }; + + jitter = mkOption { + type = with types; nullOr str; + default = "6h"; + }; + + lock = mkOption { + type = types.nullOr types.str; + default = "backup"; + }; + + network = mkOption { + type = types.bool; + default = true; + }; + + lockWait = mkOption { + type = types.int; + default = 600; + }; + }; + }; +in { + disabledModules = [ "services/backup/borgbackup.nix" ]; + + options = { + services.borgbackup = { + snapshots = mkOption { + type = types.nullOr (types.enum ["btrfs" "lvm"]); + default = null; + }; + + targets = mkOption { + type = types.attrsOf (types.submodule targetOptions); + default = {}; + }; + + prefix = mkOption { + type = types.str; + }; + }; + }; + + imports = + [ ./lvm-snapshots.nix + ./btrfs-snapshots.nix + ]; + + config = mkIf (any (t: t.paths != []) (attrValues cfg.targets)) { + services.btrfs-snapshots.enable = mkIf (cfg.snapshots == "btrfs") true; + + services.lvm-snapshots.snapshots = mkIf (cfg.snapshots == "lvm") (listToAttrs (map (path: nameValuePair (path.VG + "-" + path.LV) { + inherit (path) LV VG; + mountName = withSuffix (path.VG + "-" + path.LV); + }) (unique (flatten (mapAttrsToList (target: tCfg: tCfg.paths) cfg.targets))))); + + systemd.targets."timers-borg" = { + wantedBy = [ "timers.target" ]; + }; + + systemd.slices."system-borgbackup" = {}; + + systemd.timers = (listToAttrs (map ({ target, path, tCfg }: nameValuePair "borgbackup-${target}@${systemdPath path}" { + requiredBy = [ "timers-borg.target" ]; + + timerConfig = { + Persistent = false; + OnBootSec = tCfg.interval; + OnUnitActiveSec = tCfg.interval; + RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter; + }; + }) (flatten (mapAttrsToList (target: tCfg: map (path: { inherit target path tCfg; }) tCfg.paths) cfg.targets)))) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" { + enable = tCfg.prune != {}; + + requiredBy = [ "timers-borg.target" ]; + + timerConfig = { + Persistent = false; + OnBootSec = tCfg.interval; + OnUnitActiveSec = tCfg.interval; + RandomizedDelaySec = mkIf (tCfg.jitter != null) tCfg.jitter; + }; + }) cfg.targets); + + systemd.services = (mapAttrs' (target: tCfg: nameValuePair "borgbackup-${target}@" (let + deps = flatten [ + (optional (cfg.snapshots == "btrfs") "btrfs-snapshot@%i.service") + (optional tCfg.network "network-online.target") + ]; + in { + bindsTo = deps; + after = deps; + + path = with pkgs; [borgbackup] ++ optional (tCfg.lock != null) utillinux; + + script = let + borgCmd = '' + borg create \ + --lock-wait ${toString tCfg.lockWait} \ + --stats \ + --list \ + --filter 'AME' \ + --exclude-caches \ + --keep-exclude-tags \ + --patterns-from .backup-${target} \ + --one-file-system \ + --compression auto,lzma \ + ${tCfg.repo}::${cfg.prefix}$1-{utcnow} + ''; + in if tCfg.lock == null then borgCmd else "flock -xo /var/lock/${tCfg.lock} ${borgCmd}"; + scriptArgs = if cfg.snapshots == "lvm" then "%I" else "%i"; + + unitConfig = { + AssertPathIsDirectory = mkIf (tCfg.lock != null) "/var/lock"; + DefaultDependencies = false; + RequiresMountsFor = mkIf (cfg.snapshots == "lvm") [ "${mountPoint}/${withSuffix "%I"}" ]; + }; + + serviceConfig = { + Type = "oneshot"; + WorkingDirectory = if (cfg.snapshots == null) then "%I" else (if (cfg.snapshots == "lvm") then "${mountPoint}/${withSuffix "%I"}" else "${withSuffix "%f"}"); + Nice = 15; + IOSchedulingClass = 2; + IOSchedulingPriority = 7; + SuccessExitStatus = [1 2]; + Slice = "system-borgbackup.slice"; + }; + })) cfg.targets) // (mapAttrs' (target: tCfg: nameValuePair "borgbackup-prune-${target}" { + enable = tCfg.prune != {}; + + bindsTo = ["network-online.target"]; + after = ["network-online.target"]; + + path = with pkgs; [borgbackup]; + + script = concatStringsSep "\n" (mapAttrsToList (path: args: '' + borg prune \ + --lock-wait ${toString tCfg.lockWait} \ + --list \ + --stats \ + --prefix ${escapeShellArg "${cfg.prefix}${path}"} \ + ${escapeShellArgs args} \ + ${tCfg.repo} + '') tCfg.prune); + + serviceConfig = { + Type = "oneshot"; + Slice = "system-borgbackup.slice"; + }; + }) cfg.targets); + }; +} diff --git a/modules/borgbackup/lvm-snapshots.nix b/modules/borgbackup/lvm-snapshots.nix new file mode 100644 index 00000000..9b2a6562 --- /dev/null +++ b/modules/borgbackup/lvm-snapshots.nix @@ -0,0 +1,133 @@ +{ config, lib, utils, pkgs, ... }: + +with utils; +with lib; + +let + cfg = config.services.lvm-snapshots; + + snapshotMount = name: "${cfg.mountPoint}/${if isNull cfg.snapshots."${name}".mountName then name else cfg.snapshots."${name}".mountName}"; + snapshotName = name: "${name}-${cfg.mountSuffix}"; + + snapshotConfig = { + options = { + LV = mkOption { + type = types.str; + }; + + VG = mkOption { + type = types.str; + }; + + mountName = mkOption { + type = types.nullOr types.str; + default = null; + }; + + cowSize = mkOption { + type = types.str; + default = "-l20%ORIGIN"; + }; + + readOnly = mkOption { + type = types.bool; + default = true; + }; + + persist = mkOption { + type = types.bool; + default = false; + }; + }; + }; +in { + options = { + + services.lvm-snapshots = { + snapshots = mkOption { + type = types.attrsOf (types.submodule snapshotConfig); + default = {}; + }; + + mountPoint = mkOption { + type = types.path; + default = "/mnt"; + }; + + mountSuffix = mkOption { + type = types.str; + default = "-snapshot"; + }; + }; + }; + + + config = mkIf (cfg != {}) { + + boot.kernelModules = [ "dm_snapshot" ]; + + # system.activationScripts = mapAttrs' (name: scfg: nameValuePair ("lvm-mountpoint" + name) '' + # mkdir -p ${snapshotMount name} + # '') cfg.snapshots; + + systemd.services = mapAttrs' (name: scfg: nameValuePair ("lvm-snapshot@" + escapeSystemdPath name) { + enable = true; + + description = "LVM-snapshot of ${scfg.VG}/${scfg.LV}"; + + bindsTo = ["${escapeSystemdPath "/dev/${scfg.VG}/${scfg.LV}"}.device"]; + after = ["${escapeSystemdPath "/dev/${scfg.VG}/${scfg.LV}"}.device"]; + + unitConfig = { + StopWhenUnneeded = !scfg.persist; + AssertPathIsDirectory = "/var/lock"; + }; + + path = with pkgs; [ devicemapper utillinux ]; + + script = '' + ( + flock -xn -E 4 9 + if [[ "$?" -ne 0 ]]; then + exit $? + fi + + lvcreate -s ${scfg.cowSize} --name ${snapshotName name} ${scfg.VG}/${scfg.LV} + + sleep infinity & + ) 9>/var/lock/lvm-snapshot.${scfg.VG} + ''; + + preStart = '' + lvremove -f ${scfg.VG}/${snapshotName name} + ''; + + preStop = '' + lvremove -f ${scfg.VG}/${snapshotName name} + ''; + + serviceConfig = with pkgs; { + Type = "forking"; + RestartForceExitStatus = [ "4" ]; + RestartSec = "5min"; + }; + }) cfg.snapshots; + + systemd.mounts = mapAttrsToList (name: scfg: { + enable = true; + + unitConfig = { + # AssertPathIsDirectory = snapshotMount name; + StopWhenUnneeded = !scfg.persist; + }; + + bindsTo = [ ("lvm-snapshot@" + escapeSystemdPath name + ".service") ]; + after = [ ("lvm-snapshot@" + escapeSystemdPath name + ".service") ]; + + options = concatStringsSep "," ([ "noauto" ] ++ optional scfg.readOnly "ro"); + + where = snapshotMount name; + what = "/dev/" + scfg.VG + "/" + snapshotName name; + }) cfg.snapshots; + }; +} -- cgit v1.2.3