diff --git a/nixos/modules/services/backup/btrbk.nix b/nixos/modules/services/backup/btrbk.nix index 0c00b934405..e17761ffc3c 100644 --- a/nixos/modules/services/backup/btrbk.nix +++ b/nixos/modules/services/backup/btrbk.nix @@ -1,18 +1,36 @@ { config, pkgs, lib, ... }: let + inherit (lib) + concatMapStringsSep + concatStringsSep + filterAttrs + flatten + isAttrs + isString + literalExpression + mapAttrs' + mapAttrsToList + mkIf + mkOption + optionalString + partition + typeOf + types + ; + cfg = config.services.btrbk; sshEnabled = cfg.sshAccess != [ ]; serviceEnabled = cfg.instances != { }; attr2Lines = attr: let - pairs = lib.attrsets.mapAttrsToList (name: value: { inherit name value; }) attr; + pairs = mapAttrsToList (name: value: { inherit name value; }) attr; isSubsection = value: - if builtins.isAttrs value then true - else if builtins.isString value then false - else throw "invalid type in btrbk config ${builtins.typeOf value}"; - sortedPairs = lib.lists.partition (x: isSubsection x.value) pairs; + if isAttrs value then true + else if isString value then false + else throw "invalid type in btrbk config ${typeOf value}"; + sortedPairs = partition (x: isSubsection x.value) pairs; in - lib.flatten ( + flatten ( # non subsections go first ( map (pair: [ "${pair.name} ${pair.value}" ]) sortedPairs.wrong @@ -22,7 +40,7 @@ let map ( pair: - lib.mapAttrsToList + mapAttrsToList ( childname: value: [ "${pair.name} ${childname}" ] ++ (map (x: " " + x) (attr2Lines value)) @@ -34,7 +52,7 @@ let ) ; addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings; - mkConfigFile = settings: lib.concatStringsSep "\n" (attr2Lines (addDefaults settings)); + mkConfigFile = settings: concatStringsSep "\n" (attr2Lines (addDefaults settings)); mkTestedConfigFile = name: settings: let configFile = pkgs.writeText "btrbk-${name}.conf" (mkConfigFile settings); @@ -51,37 +69,42 @@ let ''; in { + meta.maintainers = with lib.maintainers; [ oxalica ]; + options = { services.btrbk = { - extraPackages = lib.mkOption { + extraPackages = mkOption { description = "Extra packages for btrbk, like compression utilities for stream_compress"; - type = lib.types.listOf lib.types.package; + type = types.listOf types.package; default = [ ]; - example = lib.literalExpression "[ pkgs.xz ]"; + example = literalExpression "[ pkgs.xz ]"; }; - niceness = lib.mkOption { + niceness = mkOption { description = "Niceness for local instances of btrbk. Also applies to remote ones connecting via ssh when positive."; - type = lib.types.ints.between (-20) 19; + type = types.ints.between (-20) 19; default = 10; }; - ioSchedulingClass = lib.mkOption { + ioSchedulingClass = mkOption { description = "IO scheduling class for btrbk (see ionice(1) for a quick description). Applies to local instances, and remote ones connecting by ssh if set to idle."; - type = lib.types.enum [ "idle" "best-effort" "realtime" ]; + type = types.enum [ "idle" "best-effort" "realtime" ]; default = "best-effort"; }; - instances = lib.mkOption { + instances = mkOption { description = "Set of btrbk instances. The instance named btrbk is the default one."; - type = with lib.types; + type = with types; attrsOf ( submodule { options = { - onCalendar = lib.mkOption { - type = lib.types.str; + onCalendar = mkOption { + type = types.nullOr types.str; default = "daily"; - description = "How often this btrbk instance is started. See systemd.time(7) for more information about the format."; + description = '' + How often this btrbk instance is started. See systemd.time(7) for more information about the format. + Setting it to null disables the timer, thus this instance can only be started manually. + ''; }; - settings = lib.mkOption { - type = let t = lib.types.attrsOf (lib.types.either lib.types.str (t // { description = "instances of this type recursively"; })); in t; + settings = mkOption { + type = let t = types.attrsOf (types.either types.str (t // { description = "instances of this type recursively"; })); in t; default = { }; example = { snapshot_preserve_min = "2d"; @@ -103,16 +126,16 @@ in ); default = { }; }; - sshAccess = lib.mkOption { + sshAccess = mkOption { description = "SSH keys that should be able to make or push snapshots on this system remotely with btrbk"; - type = with lib.types; listOf ( + type = with types; listOf ( submodule { options = { - key = lib.mkOption { + key = mkOption { type = str; description = "SSH public key allowed to login as user btrbk to run remote backups."; }; - roles = lib.mkOption { + roles = mkOption { type = listOf (enum [ "info" "source" "target" "delete" "snapshot" "send" "receive" ]); example = [ "source" "info" "send" ]; description = "What actions can be performed with this SSH key. See ssh_filter_btrbk(1) for details"; @@ -125,7 +148,7 @@ in }; }; - config = lib.mkIf (sshEnabled || serviceEnabled) { + config = mkIf (sshEnabled || serviceEnabled) { environment.systemPackages = [ pkgs.btrbk ] ++ cfg.extraPackages; security.sudo.extraRules = [ { @@ -152,14 +175,14 @@ in ( v: let - options = lib.concatMapStringsSep " " (x: "--" + x) v.roles; + options = concatMapStringsSep " " (x: "--" + x) v.roles; ioniceClass = { "idle" = 3; "best-effort" = 2; "realtime" = 1; }.${cfg.ioSchedulingClass}; in - ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${lib.optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}'' + ''command="${pkgs.util-linux}/bin/ionice -t -c ${toString ioniceClass} ${optionalString (cfg.niceness >= 1) "${pkgs.coreutils}/bin/nice -n ${toString cfg.niceness}"} ${pkgs.btrbk}/share/btrbk/scripts/ssh_filter_btrbk.sh --sudo ${options}" ${v.key}'' ) cfg.sshAccess; }; @@ -169,7 +192,7 @@ in "d /var/lib/btrbk/.ssh 0700 btrbk btrbk" "f /var/lib/btrbk/.ssh/config 0700 btrbk btrbk - StrictHostKeyChecking=accept-new" ]; - environment.etc = lib.mapAttrs' + environment.etc = mapAttrs' ( name: instance: { name = "btrbk/${name}.conf"; @@ -177,7 +200,7 @@ in } ) cfg.instances; - systemd.services = lib.mapAttrs' + systemd.services = mapAttrs' ( name: _: { name = "btrbk-${name}"; @@ -199,7 +222,7 @@ in ) cfg.instances; - systemd.timers = lib.mapAttrs' + systemd.timers = mapAttrs' ( name: instance: { name = "btrbk-${name}"; @@ -214,7 +237,8 @@ in }; } ) - cfg.instances; + (filterAttrs (name: instance: instance.onCalendar != null) + cfg.instances); }; } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e86dda9cb3d..84433806b48 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -62,6 +62,7 @@ in breitbandmessung = handleTest ./breitbandmessung.nix {}; brscan5 = handleTest ./brscan5.nix {}; btrbk = handleTest ./btrbk.nix {}; + btrbk-no-timer = handleTest ./btrbk-no-timer.nix {}; buildbot = handleTest ./buildbot.nix {}; buildkite-agents = handleTest ./buildkite-agents.nix {}; caddy = handleTest ./caddy.nix {}; diff --git a/nixos/tests/btrbk-no-timer.nix b/nixos/tests/btrbk-no-timer.nix new file mode 100644 index 00000000000..4fcab8839c8 --- /dev/null +++ b/nixos/tests/btrbk-no-timer.nix @@ -0,0 +1,37 @@ +import ./make-test-python.nix ({ lib, pkgs, ... }: + { + name = "btrbk-no-timer"; + meta.maintainers = with lib.maintainers; [ oxalica ]; + + nodes.machine = { ... }: { + environment.systemPackages = with pkgs; [ btrfs-progs ]; + services.btrbk.instances.local = { + onCalendar = null; + settings.volume."/mnt" = { + snapshot_dir = "btrbk/local"; + subvolume = "to_backup"; + }; + }; + }; + + testScript = '' + start_all() + + # Create btrfs partition at /mnt + machine.succeed("truncate --size=128M /data_fs") + machine.succeed("mkfs.btrfs /data_fs") + machine.succeed("mkdir /mnt") + machine.succeed("mount /data_fs /mnt") + machine.succeed("btrfs subvolume create /mnt/to_backup") + machine.succeed("mkdir -p /mnt/btrbk/local") + + # The service should not have any triggering timer. + unit = machine.get_unit_info('btrbk-local.service') + assert "TriggeredBy" not in unit + + # Manually starting the service should still work. + machine.succeed("echo foo > /mnt/to_backup/bar") + machine.start_job("btrbk-local.service") + machine.wait_until_succeeds("cat /mnt/btrbk/local/*/bar | grep foo") + ''; + })