nixos/btrbk: fix ordering of subsections and refactor

This commit is contained in:
oxalica 2022-10-18 23:50:44 +08:00
parent 1e684b371c
commit 50eb816d29
3 changed files with 105 additions and 51 deletions

View file

@ -1,72 +1,74 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
let let
inherit (lib) inherit (lib)
concatLists
concatMap
concatMapStringsSep concatMapStringsSep
concatStringsSep concatStringsSep
filterAttrs filterAttrs
flatten
isAttrs isAttrs
isString
literalExpression literalExpression
mapAttrs' mapAttrs'
mapAttrsToList mapAttrsToList
mkIf mkIf
mkOption mkOption
optionalString optionalString
partition sort
typeOf
types types
; ;
# The priority of an option or section.
# The configurations format are order-sensitive. Pairs are added as children of
# the last sections if possible, otherwise, they start a new section.
# We sort them in topological order:
# 1. Leaf pairs.
# 2. Sections that may contain (1).
# 3. Sections that may contain (1) or (2).
# 4. Etc.
prioOf = { name, value }:
if !isAttrs value then 0 # Leaf options.
else {
target = 1; # Contains: options.
subvolume = 2; # Contains: options, target.
volume = 3; # Contains: options, target, subvolume.
}.${name} or (throw "Unknow section '${name}'");
genConfig' = set: concatStringsSep "\n" (genConfig set);
genConfig = set:
let
pairs = mapAttrsToList (name: value: { inherit name value; }) set;
sortedPairs = sort (a: b: prioOf a < prioOf b) pairs;
in
concatMap genPair sortedPairs;
genSection = sec: secName: value:
[ "${sec} ${secName}" ] ++ map (x: " " + x) (genConfig value);
genPair = { name, value }:
if !isAttrs value
then [ "${name} ${value}" ]
else concatLists (mapAttrsToList (genSection name) value);
addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
mkConfigFile = name: settings: pkgs.writeTextFile {
name = "btrbk-${name}.conf";
text = genConfig' (addDefaults settings);
checkPhase = ''
set +e
${pkgs.btrbk}/bin/btrbk -c $out dryrun
# According to btrbk(1), exit status 2 means parse error
# for CLI options or the config file.
if [[ $? == 2 ]]; then
echo "Btrbk configuration is invalid:"
cat $out
exit 1
fi
set -e
'';
};
cfg = config.services.btrbk; cfg = config.services.btrbk;
sshEnabled = cfg.sshAccess != [ ]; sshEnabled = cfg.sshAccess != [ ];
serviceEnabled = cfg.instances != { }; serviceEnabled = cfg.instances != { };
attr2Lines = attr:
let
pairs = mapAttrsToList (name: value: { inherit name value; }) attr;
isSubsection = value:
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
flatten (
# non subsections go first
(
map (pair: [ "${pair.name} ${pair.value}" ]) sortedPairs.wrong
)
++ # subsections go last
(
map
(
pair:
mapAttrsToList
(
childname: value:
[ "${pair.name} ${childname}" ] ++ (map (x: " " + x) (attr2Lines value))
)
pair.value
)
sortedPairs.right
)
)
;
addDefaults = settings: { backend = "btrfs-progs-sudo"; } // settings;
mkConfigFile = settings: concatStringsSep "\n" (attr2Lines (addDefaults settings));
mkTestedConfigFile = name: settings:
let
configFile = pkgs.writeText "btrbk-${name}.conf" (mkConfigFile settings);
in
pkgs.runCommand "btrbk-${name}-tested.conf" { } ''
mkdir foo
cp ${configFile} $out
if (set +o pipefail; ${pkgs.btrbk}/bin/btrbk -c $out ls foo 2>&1 | grep $out);
then
echo btrbk configuration is invalid
cat $out
exit 1
fi;
'';
in in
{ {
meta.maintainers = with lib.maintainers; [ oxalica ]; meta.maintainers = with lib.maintainers; [ oxalica ];
@ -196,7 +198,7 @@ in
( (
name: instance: { name: instance: {
name = "btrbk/${name}.conf"; name = "btrbk/${name}.conf";
value.source = mkTestedConfigFile name instance.settings; value.source = mkConfigFile name instance.settings;
} }
) )
cfg.instances; cfg.instances;

View file

@ -102,6 +102,7 @@ in {
brscan5 = handleTest ./brscan5.nix {}; brscan5 = handleTest ./brscan5.nix {};
btrbk = handleTest ./btrbk.nix {}; btrbk = handleTest ./btrbk.nix {};
btrbk-no-timer = handleTest ./btrbk-no-timer.nix {}; btrbk-no-timer = handleTest ./btrbk-no-timer.nix {};
btrbk-section-order = handleTest ./btrbk-section-order.nix {};
buildbot = handleTest ./buildbot.nix {}; buildbot = handleTest ./buildbot.nix {};
buildkite-agents = handleTest ./buildkite-agents.nix {}; buildkite-agents = handleTest ./buildkite-agents.nix {};
caddy = handleTest ./caddy.nix {}; caddy = handleTest ./caddy.nix {};

View file

@ -0,0 +1,51 @@
# This tests validates the order of generated sections that may contain
# other sections.
# When a `volume` section has both `subvolume` and `target` children,
# `target` must go before `subvolume`. Otherwise, `target` will become
# a child of the last `subvolume` instead of `volume`, due to the
# order-sensitive config format.
#
# Issue: https://github.com/NixOS/nixpkgs/issues/195660
import ./make-test-python.nix ({ lib, pkgs, ... }: {
name = "btrbk-section-order";
meta.maintainers = with lib.maintainers; [ oxalica ];
nodes.machine = { ... }: {
services.btrbk.instances.local = {
onCalendar = null;
settings = {
timestamp_format = "long";
target."ssh://global-target/".ssh_user = "root";
volume."/btrfs" = {
snapshot_dir = "/volume-snapshots";
target."ssh://volume-target/".ssh_user = "root";
subvolume."@subvolume" = {
snapshot_dir = "/subvolume-snapshots";
target."ssh://subvolume-target/".ssh_user = "root";
};
};
};
};
};
testScript = ''
machine.wait_for_unit("basic.target")
got = machine.succeed("cat /etc/btrbk/local.conf")
expect = """
backend btrfs-progs-sudo
timestamp_format long
target ssh://global-target/
ssh_user root
volume /btrfs
snapshot_dir /volume-snapshots
target ssh://volume-target/
ssh_user root
subvolume @subvolume
snapshot_dir /subvolume-snapshots
target ssh://subvolume-target/
ssh_user root
""".strip()
print(got)
assert got == expect
'';
})