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