zfs: allow snapshot before activation

This commit is contained in:
ibizaman 2026-05-26 10:51:25 +02:00 committed by Pierre Penninckx
parent 5fd0b1dc83
commit 941f2257d0
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.
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
## Breaking Changes

View file

@ -1928,6 +1928,18 @@
"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.html#blocks-zfs-usage"
],
@ -1937,6 +1949,9 @@
"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": [
"service-implementation-guide.html#build-time-validation"
],

View file

@ -9,6 +9,12 @@
let
cfg = config.shb.zfs;
datasets =
if cfg.snapshotBeforeActivation.datasets != null then
cfg.snapshotBeforeActivation.datasets
else
config.boot.zfs.extraPools;
in
{
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
@ -203,5 +240,17 @@ in
mergeAttrs = lib.foldl lib.mergeAttrs { };
in
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.
It also enables taking snapshots on activation.
## 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.
- 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).
- Take a snapshot [before activating or deploying][activationsnapshot] allowing a safe rollback.
[backup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.backup
[datasetbackup]: #blocks-zfs-options-shb.zfs.pools._name_.datasets._name_.datasetbackup
[activationsnapshot]: #blocks-zfs-options-shb.zfs.snapshotBeforeActivation.enable
## 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.
### 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}
```{=include=} options

View file

@ -63,10 +63,27 @@
group = "syncthing";
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 =
{ nodes, ... }:
let
specialisations = "${nodes.machine.system.build.toplevel}/specialisation";
switch = name: "${specialisations}/${name}/bin/switch-to-configuration test";
in
''
import difflib
@ -98,12 +115,38 @@
diff = difflib.context_diff(expect, out)
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.wait_for_unit("multi-user.target")
assert_facl()
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.
# machine.crash()
# machine.wait_for_unit("multi-user.target")