treefmt / nix fmt (pull_request) Successful in 6s
pytest / pytest (pull_request) Successful in 37s
test ebook search / test-ebook-search (pull_request) Successful in 44s
build_systems / build-brain (pull_request) Successful in 57s
build_systems / build-bob (pull_request) Successful in 58s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m11s
build_systems / build-jeeves (pull_request) Successful in 3m1s
Add a zfs_manager module that reconciles the live datasets on jeeves against a nix declaration, and generate the snapshot retention config from that same declaration so the two can no longer drift apart. systems/jeeves/datasets.nix declares every dataset on the media, storage and scratch pools, nested the way zfs nests them and flattened into pool/parent/child names. Values were transcribed from the live pools rather than from scripts/zfs.sh, which had gone stale: acltype reads back as posix, and media/secure/important, scratch/kestra and storage/nomad were never recorded. root_pool datasets are declared for retention only, their properties stay unmanaged for now. python.tools.zfs_manager creates missing datasets and corrects drifted properties, and never destroys anything. Undeclared properties are judged by the zfs source field, so inherited and default values stay quiet while locally set ones warn. Size values are normalised to bytes so that 16K and 16384 do not re-issue zfs set on every run. vars.nix now derives its paths from the declared mountpoints instead of repeating them, dropping three zfs_* keys that nothing referenced. Replaces systems/jeeves/snapshot_config.toml, which listed a dataset that does not exist and omitted thirteen that do.
169 lines
5.3 KiB
Nix
169 lines
5.3 KiB
Nix
{
|
|
pkgs,
|
|
lib,
|
|
config,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.zfs_manager;
|
|
|
|
snapshotOptions = {
|
|
options = {
|
|
"15_min" = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 0;
|
|
description = "How many 15 minute snapshots to keep.";
|
|
};
|
|
hourly = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 0;
|
|
description = "How many hourly snapshots to keep.";
|
|
};
|
|
daily = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 0;
|
|
description = "How many daily snapshots to keep.";
|
|
};
|
|
monthly = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 0;
|
|
description = "How many monthly snapshots to keep.";
|
|
};
|
|
};
|
|
};
|
|
|
|
datasetOptions = {
|
|
options = {
|
|
manageProperties = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = ''
|
|
Whether zfs_manager owns this dataset's properties. When false the
|
|
dataset only contributes its snapshot retention, which is how
|
|
root_pool datasets are declared.
|
|
'';
|
|
};
|
|
properties = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.str;
|
|
default = { };
|
|
description = ''
|
|
The zfs properties this dataset should have. Values are compared
|
|
against the live dataset and corrected when they differ.
|
|
'';
|
|
};
|
|
snapshots = lib.mkOption {
|
|
type = lib.types.submodule snapshotOptions;
|
|
default = { };
|
|
description = "Snapshot retention for this dataset.";
|
|
};
|
|
};
|
|
};
|
|
|
|
# snapshot_manager.py only ever walks datasets below a pool root, so pool
|
|
# roots are left out of the retention table. It also indexes the table
|
|
# directly, which is why every entry carries all four keys.
|
|
snapshotTable = lib.mapAttrs (_: dataset: dataset.snapshots) (
|
|
lib.filterAttrs (name: _: lib.hasInfix "/" name) cfg.datasets
|
|
);
|
|
|
|
snapshotConfig = (pkgs.formats.toml { }).generate "snapshot_config.toml" (
|
|
snapshotTable // { default = cfg.defaultSnapshots; }
|
|
);
|
|
|
|
# Every declared dataset is emitted, including the ones whose properties are
|
|
# not managed, so the tool can tell "deliberately hands off" apart from
|
|
# "nobody has written this down yet".
|
|
datasetConfig = (pkgs.formats.json { }).generate "zfs_datasets.json" {
|
|
datasets = lib.mapAttrs (_: dataset: {
|
|
inherit (dataset) manageProperties properties;
|
|
}) cfg.datasets;
|
|
};
|
|
in
|
|
{
|
|
options = {
|
|
services.zfs_manager = {
|
|
enable = lib.mkEnableOption "declarative ZFS dataset management";
|
|
datasets = lib.mkOption {
|
|
type = lib.types.attrsOf (lib.types.submodule datasetOptions);
|
|
default = { };
|
|
example = lib.literalExpression ''
|
|
{
|
|
"media/temp".properties = {
|
|
sync = "disabled";
|
|
redundant_metadata = "none";
|
|
};
|
|
}
|
|
'';
|
|
description = ''
|
|
The datasets to manage, keyed by full dataset name. Missing datasets
|
|
are created and drifted properties are corrected. Nothing is ever
|
|
destroyed, and datasets that are not declared are left alone.
|
|
|
|
A name without a "/" is a pool root filesystem. Its properties are
|
|
managed but it is never created, pool creation stays manual.
|
|
'';
|
|
};
|
|
defaultSnapshots = lib.mkOption {
|
|
type = lib.types.submodule snapshotOptions;
|
|
default = { };
|
|
description = ''
|
|
Retention for any dataset that is not declared above, emitted as the
|
|
"default" table of the snapshot config.
|
|
'';
|
|
};
|
|
dryRun = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Log every change that would be made without touching zfs. Use this to
|
|
validate a new or heavily edited declaration before applying it.
|
|
'';
|
|
};
|
|
PYTHONPATH = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = ''
|
|
the PYTHONPATH to use for the zfs_manager service.
|
|
'';
|
|
};
|
|
EnvironmentFile = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.coercedTo lib.types.path toString lib.types.str);
|
|
default = null;
|
|
description = ''
|
|
Single environment file for the service (e.g. /etc/zfs-manager/env).
|
|
Use a leading "-" to ignore if missing (systemd feature).
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
services.snapshot_manager.path = snapshotConfig;
|
|
|
|
systemd = {
|
|
services.zfs_manager = {
|
|
description = "ZFS Dataset Manager";
|
|
requires = [ "zfs-import.target" ];
|
|
after = [
|
|
"zfs-import.target"
|
|
"zfs-mount.service"
|
|
];
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = [ pkgs.zfs ];
|
|
# Re-run on nixos-rebuild switch whenever the declaration changes.
|
|
restartTriggers = [ datasetConfig ];
|
|
environment = {
|
|
PYTHONPATH = cfg.PYTHONPATH;
|
|
};
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
ExecStart = "${pkgs.my_python}/bin/python -m python.tools.zfs_manager ${lib.escapeShellArg datasetConfig}${lib.optionalString cfg.dryRun " --dry-run"}";
|
|
}
|
|
// lib.optionalAttrs (cfg.EnvironmentFile != null) {
|
|
EnvironmentFile = cfg.EnvironmentFile;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
}
|