at master 7.9 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.sanoid; 9 10 datasetSettingsType = 11 with lib.types; 12 (attrsOf ( 13 nullOr (oneOf [ 14 str 15 int 16 bool 17 (listOf str) 18 ]) 19 )) 20 // { 21 description = "dataset/template options"; 22 }; 23 24 commonOptions = { 25 hourly = lib.mkOption { 26 description = "Number of hourly snapshots."; 27 type = with lib.types; nullOr ints.unsigned; 28 default = null; 29 }; 30 31 daily = lib.mkOption { 32 description = "Number of daily snapshots."; 33 type = with lib.types; nullOr ints.unsigned; 34 default = null; 35 }; 36 37 monthly = lib.mkOption { 38 description = "Number of monthly snapshots."; 39 type = with lib.types; nullOr ints.unsigned; 40 default = null; 41 }; 42 43 yearly = lib.mkOption { 44 description = "Number of yearly snapshots."; 45 type = with lib.types; nullOr ints.unsigned; 46 default = null; 47 }; 48 49 autoprune = lib.mkOption { 50 description = "Whether to automatically prune old snapshots."; 51 type = with lib.types; nullOr bool; 52 default = null; 53 }; 54 55 autosnap = lib.mkOption { 56 description = "Whether to automatically take snapshots."; 57 type = with lib.types; nullOr bool; 58 default = null; 59 }; 60 61 pre_snapshot_script = lib.mkOption { 62 description = "Script to run before taking snapshot."; 63 type = with lib.types; nullOr str; 64 default = null; 65 }; 66 67 post_snapshot_script = lib.mkOption { 68 description = "Script to run after taking snapshot."; 69 type = with lib.types; nullOr str; 70 default = null; 71 }; 72 73 pruning_script = lib.mkOption { 74 description = "Script to run after pruning snapshot."; 75 type = with lib.types; nullOr str; 76 default = null; 77 }; 78 79 no_inconsistent_snapshot = lib.mkOption { 80 description = "Whether to take a snapshot if the pre script fails"; 81 type = with lib.types; nullOr bool; 82 default = null; 83 }; 84 85 force_post_snapshot_script = lib.mkOption { 86 description = "Whether to run the post script if the pre script fails"; 87 type = with lib.types; nullOr bool; 88 default = null; 89 }; 90 91 script_timeout = lib.mkOption { 92 description = "Time limit for pre/post/pruning script execution time (<=0 for infinite)."; 93 type = with lib.types; nullOr int; 94 default = null; 95 }; 96 }; 97 98 datasetOptions = rec { 99 use_template = lib.mkOption { 100 description = "Names of the templates to use for this dataset."; 101 type = lib.types.listOf ( 102 lib.types.str 103 // { 104 check = (lib.types.enum (lib.attrNames cfg.templates)).check; 105 description = "configured template name"; 106 } 107 ); 108 default = [ ]; 109 }; 110 useTemplate = use_template; 111 112 recursive = lib.mkOption { 113 description = '' 114 Whether to recursively snapshot dataset children. 115 You can also set this to `"zfs"` to handle datasets 116 recursively in an atomic way without the possibility to 117 override settings for child datasets. 118 ''; 119 type = 120 with lib.types; 121 oneOf [ 122 bool 123 (enum [ "zfs" ]) 124 ]; 125 default = false; 126 }; 127 128 process_children_only = lib.mkOption { 129 description = "Whether to only snapshot child datasets if recursing."; 130 type = lib.types.bool; 131 default = false; 132 }; 133 processChildrenOnly = process_children_only; 134 }; 135 136 # Extract unique dataset names 137 datasets = lib.unique (lib.attrNames cfg.datasets); 138 139 # Function to build "zfs allow" and "zfs unallow" commands for the 140 # filesystems we've delegated permissions to. 141 buildAllowCommand = 142 zfsAction: permissions: dataset: 143 lib.escapeShellArgs [ 144 # Here we explicitly use the booted system to guarantee the stable API needed by ZFS 145 "-+/run/booted-system/sw/bin/zfs" 146 zfsAction 147 "sanoid" 148 (lib.concatStringsSep "," permissions) 149 dataset 150 ]; 151 152 configFile = 153 let 154 mkValueString = 155 v: if lib.isList v then lib.concatStringsSep "," v else lib.generators.mkValueStringDefault { } v; 156 157 mkKeyValue = 158 k: v: 159 if v == null then 160 "" 161 else if k == "processChildrenOnly" then 162 "" 163 else if k == "useTemplate" then 164 "" 165 else 166 lib.generators.mkKeyValueDefault { inherit mkValueString; } "=" k v; 167 in 168 lib.generators.toINI { inherit mkKeyValue; } cfg.settings; 169 170in 171{ 172 173 # Interface 174 175 options.services.sanoid = { 176 enable = lib.mkEnableOption "Sanoid ZFS snapshotting service"; 177 178 package = lib.mkPackageOption pkgs "sanoid" { }; 179 180 interval = lib.mkOption { 181 type = lib.types.str; 182 default = "hourly"; 183 example = "daily"; 184 description = '' 185 Run sanoid at this interval. The default is to run hourly. 186 187 The format is described in 188 {manpage}`systemd.time(7)`. 189 ''; 190 }; 191 192 datasets = lib.mkOption { 193 type = lib.types.attrsOf ( 194 lib.types.submodule ( 195 { config, options, ... }: 196 { 197 freeformType = datasetSettingsType; 198 options = commonOptions // datasetOptions; 199 config.use_template = lib.modules.mkAliasAndWrapDefsWithPriority lib.id ( 200 options.useTemplate or { } 201 ); 202 config.process_children_only = lib.modules.mkAliasAndWrapDefsWithPriority lib.id ( 203 options.processChildrenOnly or { } 204 ); 205 } 206 ) 207 ); 208 default = { }; 209 description = "Datasets to snapshot."; 210 }; 211 212 templates = lib.mkOption { 213 type = lib.types.attrsOf ( 214 lib.types.submodule { 215 freeformType = datasetSettingsType; 216 options = commonOptions; 217 } 218 ); 219 default = { }; 220 description = "Templates for datasets."; 221 }; 222 223 settings = lib.mkOption { 224 type = lib.types.attrsOf datasetSettingsType; 225 description = '' 226 Free-form settings written directly to the config file. See 227 <https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf> 228 for allowed values. 229 ''; 230 }; 231 232 extraArgs = lib.mkOption { 233 type = lib.types.listOf lib.types.str; 234 default = [ ]; 235 example = [ 236 "--verbose" 237 "--readonly" 238 "--debug" 239 ]; 240 description = '' 241 Extra arguments to pass to sanoid. See 242 <https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options> 243 for allowed options. 244 ''; 245 }; 246 }; 247 248 # Implementation 249 250 config = lib.mkIf cfg.enable { 251 services.sanoid.settings = lib.mkMerge [ 252 (lib.mapAttrs' (d: v: lib.nameValuePair ("template_" + d) v) cfg.templates) 253 (lib.mapAttrs (d: v: v) cfg.datasets) 254 ]; 255 256 systemd.services.sanoid = { 257 description = "Sanoid snapshot service"; 258 serviceConfig = { 259 ExecStartPre = ( 260 map (buildAllowCommand "allow" [ 261 "snapshot" 262 "mount" 263 "destroy" 264 ]) datasets 265 ); 266 ExecStopPost = ( 267 map (buildAllowCommand "unallow" [ 268 "snapshot" 269 "mount" 270 "destroy" 271 ]) datasets 272 ); 273 ExecStart = lib.escapeShellArgs ( 274 [ 275 "${cfg.package}/bin/sanoid" 276 "--cron" 277 "--configdir" 278 (pkgs.writeTextDir "sanoid.conf" configFile) 279 ] 280 ++ cfg.extraArgs 281 ); 282 User = "sanoid"; 283 Group = "sanoid"; 284 DynamicUser = true; 285 RuntimeDirectory = "sanoid"; 286 CacheDirectory = "sanoid"; 287 }; 288 # Prevents missing snapshots during DST changes 289 environment.TZ = "UTC"; 290 after = [ "zfs.target" ]; 291 startAt = cfg.interval; 292 }; 293 }; 294 295 meta.maintainers = with lib.maintainers; [ lopsided98 ]; 296}