at 24.11-pre 7.8 kB view raw
1{ config, pkgs, lib, ... }: 2 3with lib; 4 5let 6 cfg = config.services.snapper; 7 8 mkValue = v: 9 if isList v then "\"${concatMapStringsSep " " (escape [ "\\" " " ]) v}\"" 10 else if v == true then "yes" 11 else if v == false then "no" 12 else if isString v then "\"${v}\"" 13 else builtins.toJSON v; 14 15 mkKeyValue = k: v: "${k}=${mkValue v}"; 16 17 # "it's recommended to always specify the filesystem type" -- man snapper-configs 18 defaultOf = k: if k == "FSTYPE" then null else configOptions.${k}.default or null; 19 20 safeStr = types.strMatching "[^\n\"]*" // { 21 description = "string without line breaks or quotes"; 22 descriptionClass = "conjunction"; 23 }; 24 25 configOptions = { 26 SUBVOLUME = mkOption { 27 type = types.path; 28 description = '' 29 Path of the subvolume or mount point. 30 This path is a subvolume and has to contain a subvolume named 31 .snapshots. 32 See also man:snapper(8) section PERMISSIONS. 33 ''; 34 }; 35 36 FSTYPE = mkOption { 37 type = types.enum [ "btrfs" ]; 38 default = "btrfs"; 39 description = '' 40 Filesystem type. Only btrfs is stable and tested. 41 ''; 42 }; 43 44 ALLOW_GROUPS = mkOption { 45 type = types.listOf safeStr; 46 default = []; 47 description = '' 48 List of groups allowed to operate with the config. 49 50 Also see the PERMISSIONS section in man:snapper(8). 51 ''; 52 }; 53 54 ALLOW_USERS = mkOption { 55 type = types.listOf safeStr; 56 default = []; 57 example = [ "alice" ]; 58 description = '' 59 List of users allowed to operate with the config. "root" is always 60 implicitly included. 61 62 Also see the PERMISSIONS section in man:snapper(8). 63 ''; 64 }; 65 66 TIMELINE_CLEANUP = mkOption { 67 type = types.bool; 68 default = false; 69 description = '' 70 Defines whether the timeline cleanup algorithm should be run for the config. 71 ''; 72 }; 73 74 TIMELINE_CREATE = mkOption { 75 type = types.bool; 76 default = false; 77 description = '' 78 Defines whether hourly snapshots should be created. 79 ''; 80 }; 81 }; 82in 83 84{ 85 options.services.snapper = { 86 87 snapshotRootOnBoot = mkOption { 88 type = types.bool; 89 default = false; 90 description = '' 91 Whether to snapshot root on boot 92 ''; 93 }; 94 95 snapshotInterval = mkOption { 96 type = types.str; 97 default = "hourly"; 98 description = '' 99 Snapshot interval. 100 101 The format is described in 102 {manpage}`systemd.time(7)`. 103 ''; 104 }; 105 106 persistentTimer = mkOption { 107 default = false; 108 type = types.bool; 109 example = true; 110 description = '' 111 Set the `persistentTimer` option for the 112 {manpage}`systemd.timer(5)` 113 which triggers the snapshot immediately if the last trigger 114 was missed (e.g. if the system was powered down). 115 ''; 116 }; 117 118 cleanupInterval = mkOption { 119 type = types.str; 120 default = "1d"; 121 description = '' 122 Cleanup interval. 123 124 The format is described in 125 {manpage}`systemd.time(7)`. 126 ''; 127 }; 128 129 filters = mkOption { 130 type = types.nullOr types.lines; 131 default = null; 132 description = '' 133 Global display difference filter. See man:snapper(8) for more details. 134 ''; 135 }; 136 137 configs = mkOption { 138 default = { }; 139 example = literalExpression '' 140 { 141 home = { 142 SUBVOLUME = "/home"; 143 ALLOW_USERS = [ "alice" ]; 144 TIMELINE_CREATE = true; 145 TIMELINE_CLEANUP = true; 146 }; 147 } 148 ''; 149 150 description = '' 151 Subvolume configuration. Any option mentioned in man:snapper-configs(5) 152 is valid here, even if NixOS doesn't document it. 153 ''; 154 155 type = types.attrsOf (types.submodule { 156 freeformType = types.attrsOf (types.oneOf [ (types.listOf safeStr) types.bool safeStr types.number ]); 157 158 options = configOptions; 159 }); 160 }; 161 }; 162 163 config = mkIf (cfg.configs != {}) (let 164 documentation = [ "man:snapper(8)" "man:snapper-configs(5)" ]; 165 in { 166 167 environment = { 168 169 systemPackages = [ pkgs.snapper ]; 170 171 # Note: snapper/config-templates/default is only needed for create-config 172 # which is not the NixOS way to configure. 173 etc = { 174 175 "sysconfig/snapper".text = '' 176 SNAPPER_CONFIGS="${lib.concatStringsSep " " (builtins.attrNames cfg.configs)}" 177 ''; 178 179 } 180 // (mapAttrs' (name: subvolume: nameValuePair "snapper/configs/${name}" ({ 181 text = lib.generators.toKeyValue { inherit mkKeyValue; } (filterAttrs (k: v: v != defaultOf k) subvolume); 182 })) cfg.configs) 183 // (lib.optionalAttrs (cfg.filters != null) { 184 "snapper/filters/default.txt".text = cfg.filters; 185 }); 186 187 }; 188 189 services.dbus.packages = [ pkgs.snapper ]; 190 191 systemd.services.snapperd = { 192 description = "DBus interface for snapper"; 193 inherit documentation; 194 serviceConfig = { 195 Type = "dbus"; 196 BusName = "org.opensuse.Snapper"; 197 ExecStart = "${pkgs.snapper}/bin/snapperd"; 198 CapabilityBoundingSet = "CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SYS_ADMIN CAP_SYS_MODULE CAP_IPC_LOCK CAP_SYS_NICE"; 199 LockPersonality = true; 200 NoNewPrivileges = false; 201 PrivateNetwork = true; 202 ProtectHostname = true; 203 RestrictAddressFamilies = "AF_UNIX"; 204 RestrictRealtime = true; 205 }; 206 }; 207 208 systemd.services.snapper-timeline = { 209 description = "Timeline of Snapper Snapshots"; 210 inherit documentation; 211 requires = [ "local-fs.target" ]; 212 serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --timeline"; 213 }; 214 215 systemd.timers.snapper-timeline = { 216 wantedBy = [ "timers.target" ]; 217 timerConfig = { 218 Persistent = cfg.persistentTimer; 219 OnCalendar = cfg.snapshotInterval; 220 }; 221 }; 222 223 systemd.services.snapper-cleanup = { 224 description = "Cleanup of Snapper Snapshots"; 225 inherit documentation; 226 serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --cleanup"; 227 }; 228 229 systemd.timers.snapper-cleanup = { 230 description = "Cleanup of Snapper Snapshots"; 231 inherit documentation; 232 wantedBy = [ "timers.target" ]; 233 requires = [ "local-fs.target" ]; 234 timerConfig.OnBootSec = "10m"; 235 timerConfig.OnUnitActiveSec = cfg.cleanupInterval; 236 }; 237 238 systemd.services.snapper-boot = lib.optionalAttrs cfg.snapshotRootOnBoot { 239 description = "Take snapper snapshot of root on boot"; 240 inherit documentation; 241 serviceConfig.ExecStart = "${pkgs.snapper}/bin/snapper --config root create --cleanup-algorithm number --description boot"; 242 serviceConfig.Type = "oneshot"; 243 requires = [ "local-fs.target" ]; 244 wantedBy = [ "multi-user.target" ]; 245 unitConfig.ConditionPathExists = "/etc/snapper/configs/root"; 246 }; 247 248 assertions = 249 concatMap 250 (name: 251 let 252 sub = cfg.configs.${name}; 253 in 254 [ { assertion = !(sub ? extraConfig); 255 message = '' 256 The option definition `services.snapper.configs.${name}.extraConfig' no longer has any effect; please remove it. 257 The contents of this option should be migrated to attributes on `services.snapper.configs.${name}'. 258 ''; 259 } 260 ] ++ 261 map 262 (attr: { 263 assertion = !(hasAttr attr sub); 264 message = '' 265 The option definition `services.snapper.configs.${name}.${attr}' has been renamed to `services.snapper.configs.${name}.${toUpper attr}'. 266 ''; 267 }) 268 [ "fstype" "subvolume" ] 269 ) 270 (attrNames cfg.configs); 271 }); 272}