at 23.11-beta 7.4 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 91 Whether to snapshot root on boot 92 ''; 93 }; 94 95 snapshotInterval = mkOption { 96 type = types.str; 97 default = "hourly"; 98 description = lib.mdDoc '' 99 Snapshot interval. 100 101 The format is described in 102 {manpage}`systemd.time(7)`. 103 ''; 104 }; 105 106 cleanupInterval = mkOption { 107 type = types.str; 108 default = "1d"; 109 description = lib.mdDoc '' 110 Cleanup interval. 111 112 The format is described in 113 {manpage}`systemd.time(7)`. 114 ''; 115 }; 116 117 filters = mkOption { 118 type = types.nullOr types.lines; 119 default = null; 120 description = lib.mdDoc '' 121 Global display difference filter. See man:snapper(8) for more details. 122 ''; 123 }; 124 125 configs = mkOption { 126 default = { }; 127 example = literalExpression '' 128 { 129 home = { 130 SUBVOLUME = "/home"; 131 ALLOW_USERS = [ "alice" ]; 132 TIMELINE_CREATE = true; 133 TIMELINE_CLEANUP = true; 134 }; 135 } 136 ''; 137 138 description = lib.mdDoc '' 139 Subvolume configuration. Any option mentioned in man:snapper-configs(5) 140 is valid here, even if NixOS doesn't document it. 141 ''; 142 143 type = types.attrsOf (types.submodule { 144 freeformType = types.attrsOf (types.oneOf [ (types.listOf safeStr) types.bool safeStr types.number ]); 145 146 options = configOptions; 147 }); 148 }; 149 }; 150 151 config = mkIf (cfg.configs != {}) (let 152 documentation = [ "man:snapper(8)" "man:snapper-configs(5)" ]; 153 in { 154 155 environment = { 156 157 systemPackages = [ pkgs.snapper ]; 158 159 # Note: snapper/config-templates/default is only needed for create-config 160 # which is not the NixOS way to configure. 161 etc = { 162 163 "sysconfig/snapper".text = '' 164 SNAPPER_CONFIGS="${lib.concatStringsSep " " (builtins.attrNames cfg.configs)}" 165 ''; 166 167 } 168 // (mapAttrs' (name: subvolume: nameValuePair "snapper/configs/${name}" ({ 169 text = lib.generators.toKeyValue { inherit mkKeyValue; } (filterAttrs (k: v: v != defaultOf k) subvolume); 170 })) cfg.configs) 171 // (lib.optionalAttrs (cfg.filters != null) { 172 "snapper/filters/default.txt".text = cfg.filters; 173 }); 174 175 }; 176 177 services.dbus.packages = [ pkgs.snapper ]; 178 179 systemd.services.snapperd = { 180 description = "DBus interface for snapper"; 181 inherit documentation; 182 serviceConfig = { 183 Type = "dbus"; 184 BusName = "org.opensuse.Snapper"; 185 ExecStart = "${pkgs.snapper}/bin/snapperd"; 186 CapabilityBoundingSet = "CAP_DAC_OVERRIDE CAP_FOWNER CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_SYS_ADMIN CAP_SYS_MODULE CAP_IPC_LOCK CAP_SYS_NICE"; 187 LockPersonality = true; 188 NoNewPrivileges = false; 189 PrivateNetwork = true; 190 ProtectHostname = true; 191 RestrictAddressFamilies = "AF_UNIX"; 192 RestrictRealtime = true; 193 }; 194 }; 195 196 systemd.services.snapper-timeline = { 197 description = "Timeline of Snapper Snapshots"; 198 inherit documentation; 199 requires = [ "local-fs.target" ]; 200 serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --timeline"; 201 startAt = cfg.snapshotInterval; 202 }; 203 204 systemd.services.snapper-cleanup = { 205 description = "Cleanup of Snapper Snapshots"; 206 inherit documentation; 207 serviceConfig.ExecStart = "${pkgs.snapper}/lib/snapper/systemd-helper --cleanup"; 208 }; 209 210 systemd.timers.snapper-cleanup = { 211 description = "Cleanup of Snapper Snapshots"; 212 inherit documentation; 213 wantedBy = [ "timers.target" ]; 214 requires = [ "local-fs.target" ]; 215 timerConfig.OnBootSec = "10m"; 216 timerConfig.OnUnitActiveSec = cfg.cleanupInterval; 217 }; 218 219 systemd.services.snapper-boot = lib.optionalAttrs cfg.snapshotRootOnBoot { 220 description = "Take snapper snapshot of root on boot"; 221 inherit documentation; 222 serviceConfig.ExecStart = "${pkgs.snapper}/bin/snapper --config root create --cleanup-algorithm number --description boot"; 223 serviceConfig.Type = "oneshot"; 224 requires = [ "local-fs.target" ]; 225 wantedBy = [ "multi-user.target" ]; 226 unitConfig.ConditionPathExists = "/etc/snapper/configs/root"; 227 }; 228 229 assertions = 230 concatMap 231 (name: 232 let 233 sub = cfg.configs.${name}; 234 in 235 [ { assertion = !(sub ? extraConfig); 236 message = '' 237 The option definition `services.snapper.configs.${name}.extraConfig' no longer has any effect; please remove it. 238 The contents of this option should be migrated to attributes on `services.snapper.configs.${name}'. 239 ''; 240 } 241 ] ++ 242 map 243 (attr: { 244 assertion = !(hasAttr attr sub); 245 message = '' 246 The option definition `services.snapper.configs.${name}.${attr}' has been renamed to `services.snapper.configs.${name}.${toUpper attr}'. 247 ''; 248 }) 249 [ "fstype" "subvolume" ] 250 ) 251 (attrNames cfg.configs); 252 }); 253}