at master 14 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 utils, 6 ... 7}: 8 9let 10 cfg = config.boot.bcachefs; 11 cfgScrub = config.services.bcachefs.autoScrub; 12 13 bootFs = lib.filterAttrs ( 14 n: fs: (fs.fsType == "bcachefs") && (utils.fsNeededForBoot fs) 15 ) config.fileSystems; 16 17 commonFunctions = '' 18 prompt() { 19 local name="$1" 20 printf "enter passphrase for $name: " 21 } 22 23 tryUnlock() { 24 local name="$1" 25 local path="$2" 26 local success=false 27 local target 28 local uuid=$(echo -n $path | sed -e 's,UUID=\(.*\),\1,g') 29 30 printf "waiting for device to appear $path" 31 for try in $(seq 10); do 32 if [ -e $path ]; then 33 target=$(readlink -f $path) 34 success=true 35 break 36 else 37 target=$(blkid --uuid $uuid) 38 if [ $? == 0 ]; then 39 success=true 40 break 41 fi 42 fi 43 echo -n "." 44 sleep 1 45 done 46 printf "\n" 47 if [ $success == true ]; then 48 path=$target 49 fi 50 51 if bcachefs unlock -c $path > /dev/null 2> /dev/null; then # test for encryption 52 prompt $name 53 until bcachefs unlock $path 2> /dev/null; do # repeat until successfully unlocked 54 printf "unlocking failed!\n" 55 prompt $name 56 done 57 printf "unlocking successful.\n" 58 else 59 echo "Cannot unlock device $uuid with path $path" >&2 60 fi 61 } 62 ''; 63 64 # we need only unlock one device manually, and cannot pass multiple at once 65 # remove this adaptation when bcachefs implements mounting by filesystem uuid 66 # also, implement automatic waiting for the constituent devices when that happens 67 # bcachefs does not support mounting devices with colons in the path, ergo we don't (see #49671) 68 firstDevice = fs: lib.head (lib.splitString ":" fs.device); 69 70 useClevis = 71 fs: 72 config.boot.initrd.clevis.enable 73 && (lib.hasAttr (firstDevice fs) config.boot.initrd.clevis.devices); 74 75 openCommand = 76 name: fs: 77 if useClevis fs then 78 '' 79 if clevis decrypt < /etc/clevis/${firstDevice fs}.jwe | bcachefs unlock ${firstDevice fs} 80 then 81 printf "unlocked ${name} using clevis\n" 82 else 83 printf "falling back to interactive unlocking...\n" 84 tryUnlock ${name} ${firstDevice fs} 85 fi 86 '' 87 else 88 '' 89 tryUnlock ${name} ${firstDevice fs} 90 ''; 91 92 mkUnits = 93 prefix: name: fs: 94 let 95 parseTags = 96 device: 97 if lib.hasPrefix "LABEL=" device then 98 "/dev/disk/by-label/" + lib.removePrefix "LABEL=" device 99 else if lib.hasPrefix "UUID=" device then 100 "/dev/disk/by-uuid/" + lib.removePrefix "UUID=" device 101 else if lib.hasPrefix "PARTLABEL=" device then 102 "/dev/disk/by-partlabel/" + lib.removePrefix "PARTLABEL=" device 103 else if lib.hasPrefix "PARTUUID=" device then 104 "/dev/disk/by-partuuid/" + lib.removePrefix "PARTUUID=" device 105 else if lib.hasPrefix "ID=" device then 106 "/dev/disk/by-id/" + lib.removePrefix "ID=" device 107 else 108 device; 109 device = parseTags (firstDevice fs); 110 mkDeviceUnit = device: "${utils.escapeSystemdPath device}.device"; 111 mkMountUnit = path: "${utils.escapeSystemdPath (lib.removeSuffix "/" path)}.mount"; 112 deviceUnit = mkDeviceUnit device; 113 mountUnit = mkMountUnit (prefix + fs.mountPoint); 114 extractProperty = 115 prop: options: (map (lib.removePrefix prop) (builtins.filter (lib.hasPrefix prop) options)); 116 normalizeUnits = 117 unit: 118 if lib.hasPrefix "/dev/" unit then 119 mkDeviceUnit unit 120 else if lib.hasPrefix "/" unit then 121 mkMountUnit unit 122 else 123 unit; 124 requiredUnits = map normalizeUnits (extractProperty "x-systemd.requires=" fs.options); 125 wantedUnits = map normalizeUnits (extractProperty "x-systemd.wants=" fs.options); 126 requiredMounts = extractProperty "x-systemd.requires-mounts-for=" fs.options; 127 wantedMounts = extractProperty "x-systemd.wants-mounts-for=" fs.options; 128 in 129 { 130 name = "unlock-bcachefs-${utils.escapeSystemdPath fs.mountPoint}"; 131 value = { 132 description = "Unlock bcachefs for ${fs.mountPoint}"; 133 requiredBy = [ mountUnit ]; 134 after = [ deviceUnit ] ++ requiredUnits ++ wantedUnits; 135 before = [ 136 mountUnit 137 "shutdown.target" 138 ]; 139 bindsTo = [ deviceUnit ]; 140 requires = requiredUnits; 141 wants = wantedUnits; 142 unitConfig = { 143 RequiresMountsFor = requiredMounts; 144 WantsMountsFor = wantedMounts; 145 }; 146 conflicts = [ "shutdown.target" ]; 147 unitConfig.DefaultDependencies = false; 148 serviceConfig = { 149 Type = "oneshot"; 150 ExecCondition = "${cfg.package}/bin/bcachefs unlock -c \"${device}\""; 151 Restart = "on-failure"; 152 RestartMode = "direct"; 153 # Ideally, this service would lock the key on stop. 154 # As is, RemainAfterExit doesn't accomplish anything. 155 RemainAfterExit = true; 156 }; 157 script = 158 let 159 unlock = ''${cfg.package}/bin/bcachefs unlock "${device}"''; 160 unlockInteractively = ''${config.boot.initrd.systemd.package}/bin/systemd-ask-password --timeout=0 "enter passphrase for ${name}" | exec ${unlock}''; 161 in 162 if useClevis fs then 163 '' 164 if ${config.boot.initrd.clevis.package}/bin/clevis decrypt < "/etc/clevis/${device}.jwe" | ${unlock} 165 then 166 printf "unlocked ${name} using clevis\n" 167 else 168 printf "falling back to interactive unlocking...\n" 169 ${unlockInteractively} 170 fi 171 '' 172 else 173 '' 174 ${unlockInteractively} 175 ''; 176 }; 177 }; 178in 179 180{ 181 options.boot.bcachefs = { 182 package = lib.mkPackageOption pkgs "bcachefs-tools" { 183 extraDescription = '' 184 This package should also provide a passthru 'kernelModule' 185 attribute to build the out-of-tree kernel module. 186 ''; 187 }; 188 189 modulePackage = lib.mkOption { 190 type = lib.types.package; 191 # See NOTE in linux-kernels.nix 192 default = config.boot.kernelPackages.callPackage cfg.package.kernelModule { }; 193 internal = true; 194 }; 195 }; 196 197 options.services.bcachefs.autoScrub = { 198 enable = lib.mkEnableOption "regular bcachefs scrub"; 199 200 fileSystems = lib.mkOption { 201 type = lib.types.listOf lib.types.path; 202 example = [ "/" ]; 203 description = '' 204 List of paths to bcachefs filesystems to regularly call {command}`bcachefs scrub` on. 205 Defaults to all mount points with bcachefs filesystems. 206 ''; 207 }; 208 209 interval = lib.mkOption { 210 default = "monthly"; 211 type = lib.types.str; 212 example = "weekly"; 213 description = '' 214 Systemd calendar expression for when to scrub bcachefs filesystems. 215 The recommended period is a month but could be less. 216 See 217 {manpage}`systemd.time(7)` 218 for more information on the syntax. 219 ''; 220 }; 221 }; 222 223 config = lib.mkIf (config.boot.supportedFilesystems.bcachefs or false) ( 224 lib.mkMerge [ 225 { 226 assertions = [ 227 { 228 assertion = 229 let 230 kernel = config.boot.kernelPackages.kernel; 231 in 232 ( 233 kernel.kernelAtLeast "6.7" 234 || (lib.elem (kernel.structuredExtraConfig.BCACHEFS_FS or null) [ 235 lib.kernel.module 236 lib.kernel.yes 237 (lib.kernel.option lib.kernel.yes) 238 ]) 239 ); 240 241 message = "Linux 6.7-rc1 at minimum or a custom linux kernel with bcachefs support is required"; 242 } 243 ]; 244 245 warnings = lib.mkIf cfg.modulePackage.meta.broken [ 246 '' 247 Using unmaintained in-tree bcachefs kernel module. This 248 will be removed in 26.05. Please use a kernel supported 249 by the out-of-tree module package. 250 '' 251 ]; 252 253 # Bcachefs upstream recommends using the latest kernel 254 boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest; 255 256 # needed for systemd-remount-fs 257 system.fsPackages = [ cfg.package ]; 258 services.udev.packages = [ cfg.package ]; 259 260 boot.extraModulePackages = lib.optionals (!cfg.modulePackage.meta.broken) [ 261 cfg.modulePackage 262 ]; 263 264 systemd = { 265 packages = [ cfg.package ]; 266 services = lib.mapAttrs' (mkUnits "") ( 267 lib.filterAttrs (n: fs: (fs.fsType == "bcachefs") && (!utils.fsNeededForBoot fs)) config.fileSystems 268 ); 269 }; 270 } 271 272 (lib.mkIf ((config.boot.initrd.supportedFilesystems.bcachefs or false) || (bootFs != { })) { 273 boot.initrd.availableKernelModules = [ 274 "bcachefs" 275 "sha256" 276 ] 277 ++ lib.optionals (config.boot.kernelPackages.kernel.kernelOlder "6.15") [ 278 # chacha20 and poly1305 are required only for decryption attempts 279 # kernel 6.15 uses kernel api libraries for poly1305/chacha20: 4bf4b5046de0ef7f9dc50f3a9ef8a6dcda178a6d 280 # kernel 6.16 removes poly1305: ceef731b0e22df80a13d67773ae9afd55a971f9e 281 "poly1305" 282 "chacha20" 283 ]; 284 boot.initrd.systemd.extraBin = { 285 # do we need this? boot/systemd.nix:566 & boot/systemd/initrd.nix:357 286 "bcachefs" = "${cfg.package}/bin/bcachefs"; 287 "mount.bcachefs" = "${cfg.package}/bin/mount.bcachefs"; 288 }; 289 boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) '' 290 copy_bin_and_libs ${cfg.package}/bin/bcachefs 291 copy_bin_and_libs ${cfg.package}/bin/mount.bcachefs 292 ''; 293 boot.initrd.extraUtilsCommandsTest = lib.mkIf (!config.boot.initrd.systemd.enable) '' 294 $out/bin/bcachefs version 295 ''; 296 297 boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) ( 298 commonFunctions + lib.concatStrings (lib.mapAttrsToList openCommand bootFs) 299 ); 300 301 boot.initrd.systemd.services = lib.mapAttrs' (mkUnits "/sysroot") bootFs; 302 }) 303 304 (lib.mkIf (cfgScrub.enable) { 305 assertions = [ 306 { 307 assertion = lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.14"; 308 message = "Bcachefs scrubbing is supported from kernel version 6.14 or later."; 309 } 310 { 311 assertion = cfgScrub.enable -> (cfgScrub.fileSystems != [ ]); 312 message = '' 313 If 'services.bcachefs.autoScrub' is enabled, you need to have at least one 314 bcachefs file system mounted via 'fileSystems' or specify a list manually 315 in 'services.bcachefs.autoScrub.fileSystems'. 316 ''; 317 } 318 ]; 319 320 # This will remove duplicated units from either having a filesystem mounted multiple 321 # time, or additionally mounted subvolumes, as well as having a filesystem span 322 # multiple devices (provided the same device is used to mount said filesystem). 323 services.bcachefs.autoScrub.fileSystems = 324 let 325 isDeviceInList = list: device: builtins.filter (e: e.device == device) list != [ ]; 326 327 uniqueDeviceList = lib.foldl' ( 328 acc: e: if isDeviceInList acc e.device then acc else acc ++ [ e ] 329 ) [ ]; 330 in 331 lib.mkDefault ( 332 map (e: e.mountPoint) ( 333 uniqueDeviceList ( 334 lib.mapAttrsToList (name: fs: { 335 mountPoint = fs.mountPoint; 336 device = fs.device; 337 }) (lib.filterAttrs (name: fs: fs.fsType == "bcachefs") config.fileSystems) 338 ) 339 ) 340 ); 341 342 systemd.timers = 343 let 344 scrubTimer = 345 fs: 346 let 347 fs' = if fs == "/" then "root" else utils.escapeSystemdPath fs; 348 in 349 lib.nameValuePair "bcachefs-scrub-${fs'}" { 350 description = "regular bcachefs scrub timer on ${fs}"; 351 352 wantedBy = [ "timers.target" ]; 353 timerConfig = { 354 OnCalendar = cfgScrub.interval; 355 AccuracySec = "1d"; 356 Persistent = true; 357 }; 358 }; 359 in 360 lib.listToAttrs (map scrubTimer cfgScrub.fileSystems); 361 362 systemd.services = 363 let 364 scrubService = 365 fs: 366 let 367 fs' = if fs == "/" then "root" else utils.escapeSystemdPath fs; 368 in 369 lib.nameValuePair "bcachefs-scrub-${fs'}" { 370 description = "bcachefs scrub on ${fs}"; 371 # scrub prevents suspend2ram or proper shutdown 372 conflicts = [ 373 "shutdown.target" 374 "sleep.target" 375 ]; 376 before = [ 377 "shutdown.target" 378 "sleep.target" 379 ]; 380 381 script = "${lib.getExe cfg.package} data scrub ${fs}"; 382 383 serviceConfig = { 384 Type = "oneshot"; 385 Nice = 19; 386 IOSchedulingClass = "idle"; 387 }; 388 }; 389 in 390 lib.listToAttrs (map scrubService cfgScrub.fileSystems); 391 }) 392 ] 393 ); 394 395 meta = { 396 inherit (pkgs.bcachefs-tools.meta) maintainers; 397 }; 398}