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