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