at 23.05-pre 6.1 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 interval = mkOption { 118 type = types.str; 119 default = "hourly"; 120 example = "daily"; 121 description = lib.mdDoc '' 122 Run sanoid at this interval. The default is to run hourly. 123 124 The format is described in 125 {manpage}`systemd.time(7)`. 126 ''; 127 }; 128 129 datasets = mkOption { 130 type = types.attrsOf (types.submodule ({ config, options, ... }: { 131 freeformType = datasetSettingsType; 132 options = commonOptions // datasetOptions; 133 config.use_template = modules.mkAliasAndWrapDefsWithPriority id (options.useTemplate or { }); 134 config.process_children_only = modules.mkAliasAndWrapDefsWithPriority id (options.processChildrenOnly or { }); 135 })); 136 default = { }; 137 description = lib.mdDoc "Datasets to snapshot."; 138 }; 139 140 templates = mkOption { 141 type = types.attrsOf (types.submodule { 142 freeformType = datasetSettingsType; 143 options = commonOptions; 144 }); 145 default = { }; 146 description = lib.mdDoc "Templates for datasets."; 147 }; 148 149 settings = mkOption { 150 type = types.attrsOf datasetSettingsType; 151 description = lib.mdDoc '' 152 Free-form settings written directly to the config file. See 153 <https://github.com/jimsalterjrs/sanoid/blob/master/sanoid.defaults.conf> 154 for allowed values. 155 ''; 156 }; 157 158 extraArgs = mkOption { 159 type = types.listOf types.str; 160 default = [ ]; 161 example = [ "--verbose" "--readonly" "--debug" ]; 162 description = lib.mdDoc '' 163 Extra arguments to pass to sanoid. See 164 <https://github.com/jimsalterjrs/sanoid/#sanoid-command-line-options> 165 for allowed options. 166 ''; 167 }; 168 }; 169 170 # Implementation 171 172 config = mkIf cfg.enable { 173 services.sanoid.settings = mkMerge [ 174 (mapAttrs' (d: v: nameValuePair ("template_" + d) v) cfg.templates) 175 (mapAttrs (d: v: v) cfg.datasets) 176 ]; 177 178 systemd.services.sanoid = { 179 description = "Sanoid snapshot service"; 180 serviceConfig = { 181 ExecStartPre = (map (buildAllowCommand "allow" [ "snapshot" "mount" "destroy" ]) datasets); 182 ExecStopPost = (map (buildAllowCommand "unallow" [ "snapshot" "mount" "destroy" ]) datasets); 183 ExecStart = lib.escapeShellArgs ([ 184 "${pkgs.sanoid}/bin/sanoid" 185 "--cron" 186 "--configdir" 187 (pkgs.writeTextDir "sanoid.conf" configFile) 188 ] ++ cfg.extraArgs); 189 User = "sanoid"; 190 Group = "sanoid"; 191 DynamicUser = true; 192 RuntimeDirectory = "sanoid"; 193 CacheDirectory = "sanoid"; 194 }; 195 # Prevents missing snapshots during DST changes 196 environment.TZ = "UTC"; 197 after = [ "zfs.target" ]; 198 startAt = cfg.interval; 199 }; 200 }; 201 202 meta.maintainers = with maintainers; [ lopsided98 ]; 203}