zfs: allow snapshot before activation
This commit is contained in:
parent
5fd0b1dc83
commit
588c8f4e67
5 changed files with 148 additions and 0 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue