at 23.11-pre 6.2 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.sanoid; 7 8 datasetSettingsType = with types; 9 (attrsOf (nullOr (oneOf [ str int bool (listOf str) ]))) // { 10 description = "dataset/template options"; 11 }; 12 13 commonOptions = { 14 hourly = mkOption { 15 description = lib.mdDoc "Number of hourly snapshots."; 16 type = with types; nullOr ints.unsigned; 17 default = null; 18 }; 19 20 daily = mkOption { 21 description = lib.mdDoc "Number of daily snapshots."; 22 type = with types; nullOr ints.unsigned; 23 default = null; 24 }; 25 26 monthly = mkOption { 27 description = lib.mdDoc "Number of monthly snapshots."; 28 type = with types; nullOr ints.unsigned; 29 default = null; 30 }; 31 32 yearly = mkOption { 33 description = lib.mdDoc "Number of yearly snapshots."; 34 type = with types; nullOr ints.unsigned; 35 default = null; 36 }; 37 38 autoprune = mkOption { 39 description = lib.mdDoc "Whether to automatically prune old snapshots."; 40 type = with types; nullOr bool; 41 default = null; 42 }; 43 44 autosnap = mkOption { 45 description = lib.mdDoc "Whether to automatically take snapshots."; 46 type = with types; nullOr bool; 47 default = null; 48 }; 49 }; 50 51 datasetOptions = rec { 52 use_template = mkOption { 53 description = lib.mdDoc "Names of the templates to use for this dataset."; 54 type = types.listOf (types.str // { 55 check = (types.enum (attrNames cfg.templates)).check; 56 description = "configured template name"; 57 }); 58 default = [ ]; 59 }; 60 useTemplate = use_template; 61 62 recursive = mkOption { 63 description = lib.mdDoc '' 64 Whether to recursively snapshot dataset children. 65 You can also set this to `"zfs"` to handle datasets 66 recursively in an atomic way without the possibility to 67 override settings for child datasets. 68 ''; 69 type = with types; oneOf [ bool (enum [ "zfs" ]) ]; 70 default = false; 71 }; 72 73 process_children_only = mkOption { 74 description = lib.mdDoc "Whether to only snapshot child datasets if recursing."; 75 type = types.bool; 76 default = false; 77 }; 78 processChildrenOnly = process_children_only; 79 }; 80 81 # Extract unique dataset names 82 datasets = unique (attrNames cfg.datasets); 83 84 # Function to build "zfs allow" and "zfs unallow" commands for the 85 # filesystems we've delegated permissions to. 86 buildAllowCommand = zfsAction: permissions: dataset: lib.escapeShellArgs [ 87 # Here we explicitly use the booted system to guarantee the stable API needed by ZFS 88 "-+/run/booted-system/sw/bin/zfs" 89 zfsAction 90 "sanoid" 91 (concatStringsSep "," permissions) 92 dataset 93 ]; 94 95 configFile = 96 let 97 mkValueString = v: 98 if builtins.isList v then concatStringsSep "," v 99 else generators.mkValueStringDefault { } v; 100 101 mkKeyValue = k: v: 102 if v == null then "" 103 else if k == "processChildrenOnly" then "" 104 else if k == "useTemplate" then "" 105 else generators.mkKeyValueDefault { inherit mkValueString; } "=" k v; 106 in 107 generators.toINI { inherit mkKeyValue; } cfg.settings; 108 109in 110{ 111 112 # Interface 113 114 options.services.sanoid = { 115 enable = mkEnableOption (lib.mdDoc "Sanoid ZFS snapshotting service"); 116 117 package = lib.mkPackageOptionMD pkgs "sanoid" {}; 118 119 interval = mkOption { 120 type = types.str; 121 default = "hourly"; 122 example = "daily"; 123 description = lib.mdDoc '' 124 Run sanoid at this interval. The default is to run hourly. 125 126 The format is described in 127 {manpage}`systemd.time(7)`. 128 ''; 129 }; 130 131 datasets = mkOption { 132 type = types.attrsOf (types.submodule ({ config, options, ... }: { 133 freeformType = datasetSettingsType; 134 options = commonOptions // datasetOptions; 135 config.use_template = modules.mkAliasAndWrapDefsWithPriority id (options.useTemplate or { }); 136 config.process_children_only = modules.mkAliasAndWrapDefsWithPriority id (options.processChildrenOnly or { }); 137 })); 138 default = { }; 139 description = lib.mdDoc "Datasets to snapshot."; 140 }; 141 142 templates = mkOption { 143 type = types.attrsOf (types.submodule { 144 freeformType = datasetSettingsType; 145 options = commonOptions; 146 }); 147 default = { }; 148 description = lib.mdDoc "Templates for datasets."; 149 }; 150 151 settings = mkOption { 152 type = types.attrsOf datasetSettingsType; 153 description = lib.mdDoc '' 154 Free-form settings written directly to the config file. See 155 <https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf> 156 for allowed values. 157 ''; 158 }; 159 160 extraArgs = mkOption { 161 type = types.listOf types.str; 162 default = [ ]; 163 example = [ "--verbose" "--readonly" "--debug" ]; 164 description = lib.mdDoc '' 165 Extra arguments to pass to sanoid. See 166 <https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options> 167 for allowed options. 168 ''; 169 }; 170 }; 171 172 # Implementation 173 174 config = mkIf cfg.enable { 175 services.sanoid.settings = mkMerge [ 176 (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates) 177 (mapAttrs (d: v: v) cfg.datasets) 178 ]; 179 180 systemd.services.sanoid = { 181 description = "Sanoid snapshot service"; 182 serviceConfig = { 183 ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets); 184 ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets); 185 ExecStart = lib.escapeShellArgs ([ 186 "${cfg.package}/bin/sanoid" 187 "--cron" 188 "--configdir" 189 (pkgs.writeTextDir "sanoid.conf" configFile) 190 ] ++ cfg.extraArgs); 191 User = "sanoid"; 192 Group = "sanoid"; 193 DynamicUser = true; 194 RuntimeDirectory = "sanoid"; 195 CacheDirectory = "sanoid"; 196 }; 197 # Prevents missing snapshots during DST changes 198 environment.TZ = "UTC"; 199 after = [ "zfs.target" ]; 200 startAt = cfg.interval; 201 }; 202 }; 203 204 meta.maintainers = with maintainers; [ lopsided98 ]; 205}