zfs: allow snapshot before activation

This commit is contained in:
ibizaman 2026-05-26 10:51:25 +02:00
parent 5fd0b1dc83
commit 588c8f4e67
5 changed files with 148 additions and 0 deletions

View file

@ -29,6 +29,11 @@ Template:
to include the ldap UID instead of the email but I kept the email address. to include the ldap UID instead of the email but I kept the email address.
This means there is no migration to do. This means there is no migration to do.
## New Features
- Add `shb.zfs.snapshotBeforeActivation` option to take a ZFS snapshot of given datasets before activation.
See [the manual](https://shb.skarabox.com/blocks-zfs.html) for more details.
# v0.8.0 # v0.8.0
## Breaking Changes ## Breaking Changes

View file

@ -1928,6 +1928,18 @@
"blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.path": [ "blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.path": [
"blocks-zfs.html#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.path" "blocks-zfs.html#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.path"
], ],
"blocks-zfs-options-shb.zfs.snapshotBeforeActivation.datasets": [
"blocks-zfs.html#blocks-zfs-options-shb.zfs.snapshotBeforeActivation.datasets"
],
"blocks-zfs-options-shb.zfs.snapshotBeforeActivation.enable": [
"blocks-zfs.html#blocks-zfs-options-shb.zfs.snapshotBeforeActivation.enable"
],
"blocks-zfs-options-shb.zfs.snapshotBeforeActivation.recursive": [
"blocks-zfs.html#blocks-zfs-options-shb.zfs.snapshotBeforeActivation.recursive"
],
"blocks-zfs-options-shb.zfs.snapshotBeforeActivation.template": [
"blocks-zfs.html#blocks-zfs-options-shb.zfs.snapshotBeforeActivation.template"
],
"blocks-zfs-usage": [ "blocks-zfs-usage": [
"blocks-zfs.html#blocks-zfs-usage" "blocks-zfs.html#blocks-zfs-usage"
], ],
@ -1937,6 +1949,9 @@
"blocks-zfs-usage-backup-files": [ "blocks-zfs-usage-backup-files": [
"blocks-zfs.html#blocks-zfs-usage-backup-files" "blocks-zfs.html#blocks-zfs-usage-backup-files"
], ],
"blocks-zfs-usage-snapshot-before-activation": [
"blocks-zfs.html#blocks-zfs-usage-snapshot-before-activation"
],
"build-time-validation": [ "build-time-validation": [
"service-implementation-guide.html#build-time-validation" "service-implementation-guide.html#build-time-validation"
], ],

View file

@ -9,6 +9,12 @@
let let
cfg = config.shb.zfs; cfg = config.shb.zfs;
datasets =
if cfg.snapshotBeforeActivation.datasets != null then
cfg.snapshotBeforeActivation.datasets
else
config.boot.zfs.extraPools;
in in
{ {
imports = [ imports = [
@ -145,6 +151,37 @@ in
); );
}; };
snapshotBeforeActivation = {
enable = lib.mkOption {
type = lib.types.bool;
description = "Take a snapshot of all datasets before activation";
default = false;
};
template = lib.mkOption {
type = lib.types.str;
description = "Bash command to generate the snapshot name. $1 is the path to the new generation.";
default = ''pre-$(date --utc '+%y%m%dT%H%M%S')-$(basename "$1")'';
};
datasets = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
description = ''
Defines all datasets to take a snapshot of.
If set to `null`, the default, take the list of datasets from `config.boot.zfs.extraPools`.
The snapshots are not recursive unless specificed in the `recursive` option.
'';
};
recursive = lib.mkOption {
type = lib.types.bool;
description = "If true, take snapshots recurisevly on the given datasets.";
default = false;
};
};
}; };
# The implementation is greatly inspired by https://discourse.nixos.org/t/configure-zfs-filesystems-after-install/48633/3 # The implementation is greatly inspired by https://discourse.nixos.org/t/configure-zfs-filesystems-after-install/48633/3
@ -203,5 +240,17 @@ in
mergeAttrs = lib.foldl lib.mergeAttrs { }; mergeAttrs = lib.foldl lib.mergeAttrs { };
in in
mergeAttrs (lib.mapAttrsToList mkPool cfg.pools); mergeAttrs (lib.mapAttrsToList mkPool cfg.pools);
system.preSwitchChecks."shb-zfs-snapshot" = lib.mkIf cfg.snapshotBeforeActivation.enable (
''
name="${cfg.snapshotBeforeActivation.template}"
''
+ (
let
recursiveFlag = lib.optionalString cfg.snapshotBeforeActivation.recursive "-r";
in
lib.concatMapStringsSep "\n" (ds: "zfs snapshot ${recursiveFlag} ${ds}@\"$name\"") datasets
)
);
}; };
} }

View file

@ -15,6 +15,7 @@ Defined in [`/modules/blocks/zfs.nix`](@REPO@/modules/blocks/zfs.nix):
``` ```
This block creates ZFS datasets, optionally mounts them and sets permissions on the mount point. This block creates ZFS datasets, optionally mounts them and sets permissions on the mount point.
It also enables taking snapshots on activation.
## Features {#blocks-zfs-features} ## Features {#blocks-zfs-features}
@ -22,9 +23,11 @@ This block creates ZFS datasets, optionally mounts them and sets permissions on
- Sets permissions, [owner](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.owner), [group](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.group) and [ACL](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.defaultACLs) on the mount point. - Sets permissions, [owner](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.owner), [group](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.group) and [ACL](#blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.defaultACLs) on the mount point.
- Backup of the files in the dataset [`shb.zfs.<name>.backup`][backup] through the [backup contract](./contracts-backup.html). - Backup of the files in the dataset [`shb.zfs.<name>.backup`][backup] through the [backup contract](./contracts-backup.html).
- Backup of the dataset itself [`shb.zfs.<name>.datasetbackup`][datasetbackup] through the [dataset backup contract](./contracts-datasetbackup.html). - Backup of the dataset itself [`shb.zfs.<name>.datasetbackup`][datasetbackup] through the [dataset backup contract](./contracts-datasetbackup.html).
- Take a snapshot [before activating or deploying][activationsnapshot] allowing a safe rollback.
[backup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.backup [backup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.backup
[datasetbackup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.datasetbackup [datasetbackup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.datasetbackup
[activationsnapshot]: #blocks-zfs-options-shb.zfs.snapshotBeforeActivation.enable
## Usage {#blocks-zfs-usage} ## Usage {#blocks-zfs-usage}
@ -93,6 +96,39 @@ For example, with the restic module as the file backup contract provider:
See the [Restic block](blocks-restic.html) for more examples. See the [Restic block](blocks-restic.html) for more examples.
### Snapshot before activation {#blocks-zfs-usage-snapshot-before-activation}
To take a snapshot of all datasets before activating a new configuration:
```nix
{
shb.zfs.snapshotBeforeActivation.enable = true;
}
```
This does not take a recursive snapshot by default, to do it recursively, do:
```nix
{
shb.zfs.snapshotBeforeActivation = {
enable = true;
recursive = true;
};
}
```
We did not specify datasets manually, which means the datasets list comes from `boot.zfs.extraPools`.
To specify the datasets manually, you can use:
```nix
{
shb.zfs.snapshotBeforeActivation = {
enable = true;
datasets = [ "root" "root/var" ];
};
}
```
## Options Reference {#blocks-zfs-options} ## Options Reference {#blocks-zfs-options}
```{=include=} options ```{=include=} options

View file

@ -63,10 +63,27 @@
group = "syncthing"; group = "syncthing";
defaultACLs = "g:syncthing:rwX"; defaultACLs = "g:syncthing:rwX";
}; };
shb.zfs.snapshotBeforeActivation = {
enable = true;
recursive = true;
};
specialisation = {
test.configuration = { };
test2.configuration = {
# A plus sign is not allowed as the snapshot name so it will make it fail.
shb.zfs.snapshotBeforeActivation.template = "+";
};
};
}; };
testScript = testScript =
{ nodes, ... }: { nodes, ... }:
let
specialisations = "${nodes.machine.system.build.toplevel}/specialisation";
switch = name: "${specialisations}/${name}/bin/switch-to-configuration test";
in
'' ''
import difflib import difflib
@ -98,12 +115,38 @@
diff = difflib.context_diff(expect, out) diff = difflib.context_diff(expect, out)
raise Exception(f"Unexpected mounts:\n{"\n".join(diff)}") raise Exception(f"Unexpected mounts:\n{"\n".join(diff)}")
def assert_count_snapshots(name, count):
out = machine.succeed("zfs list -Ht snapshot root/one")
print(out)
if count != len(out.splitlines()):
raise Exception(f"Expected {count} dataset")
machine.start(allow_reboot=True) machine.start(allow_reboot=True)
machine.wait_for_unit("multi-user.target") machine.wait_for_unit("multi-user.target")
assert_facl() assert_facl()
assert_mounts() assert_mounts()
with subtest("no snapshot yet"):
print(machine.succeed("zpool list"))
print(machine.succeed("zfs list"))
assert_count_snapshots("root/one", 0)
assert_count_snapshots("root/two", 0)
assert_count_snapshots("root/none", 0)
assert_count_snapshots("data/two", 0)
machine.succeed("${switch "test"}")
with subtest("snapshot created"):
print(machine.succeed("zpool list"))
print(machine.succeed("zfs list"))
assert_count_snapshots("root/one", 1)
assert_count_snapshots("root/two", 1)
assert_count_snapshots("root/none", 1)
assert_count_snapshots("data/two", 1)
machine.fail("${switch "test2"}")
# TODO: make this work. zpool import does not work after reboot. # TODO: make this work. zpool import does not work after reboot.
# machine.crash() # machine.crash()
# machine.wait_for_unit("multi-user.target") # machine.wait_for_unit("multi-user.target")