at 24.11-pre 16 kB view raw
1{ config, lib, pkgs, utils, ... }: 2 3with lib; 4with utils; 5 6let 7 8 addCheckDesc = desc: elemType: check: types.addCheck elemType check 9 // { description = "${elemType.description} (with check: ${desc})"; }; 10 11 isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null; 12 nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty; 13 14 fileSystems' = toposort fsBefore (attrValues config.fileSystems); 15 16 fileSystems = if fileSystems' ? result 17 then # use topologically sorted fileSystems everywhere 18 fileSystems'.result 19 else # the assertion below will catch this, 20 # but we fall back to the original order 21 # anyway so that other modules could check 22 # their assertions too 23 (attrValues config.fileSystems); 24 25 specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ]; 26 27 nonEmptyWithoutTrailingSlash = addCheckDesc "non-empty without trailing slash" types.str 28 (s: isNonEmpty s && (builtins.match ".+/" s) == null); 29 30 coreFileSystemOpts = { name, config, ... }: { 31 32 options = { 33 mountPoint = mkOption { 34 example = "/mnt/usb"; 35 type = nonEmptyWithoutTrailingSlash; 36 description = "Location of the mounted file system."; 37 }; 38 39 stratis.poolUuid = lib.mkOption { 40 type = types.uniq (types.nullOr types.str); 41 description = '' 42 UUID of the stratis pool that the fs is located in 43 ''; 44 example = "04c68063-90a5-4235-b9dd-6180098a20d9"; 45 default = null; 46 }; 47 48 device = mkOption { 49 default = null; 50 example = "/dev/sda"; 51 type = types.nullOr nonEmptyStr; 52 description = "Location of the device."; 53 }; 54 55 fsType = mkOption { 56 default = "auto"; 57 example = "ext3"; 58 type = nonEmptyStr; 59 description = "Type of the file system."; 60 }; 61 62 options = mkOption { 63 default = [ "defaults" ]; 64 example = [ "data=journal" ]; 65 description = "Options used to mount the file system."; 66 type = types.nonEmptyListOf nonEmptyStr; 67 }; 68 69 depends = mkOption { 70 default = [ ]; 71 example = [ "/persist" ]; 72 type = types.listOf nonEmptyWithoutTrailingSlash; 73 description = '' 74 List of paths that should be mounted before this one. This filesystem's 75 {option}`device` and {option}`mountPoint` are always 76 checked and do not need to be included explicitly. If a path is added 77 to this list, any other filesystem whose mount point is a parent of 78 the path will be mounted before this filesystem. The paths do not need 79 to actually be the {option}`mountPoint` of some other filesystem. 80 ''; 81 }; 82 83 }; 84 85 config = { 86 mountPoint = mkDefault name; 87 device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType); 88 }; 89 90 }; 91 92 fileSystemOpts = { config, ... }: { 93 94 options = { 95 96 label = mkOption { 97 default = null; 98 example = "root-partition"; 99 type = types.nullOr nonEmptyStr; 100 description = "Label of the device (if any)."; 101 }; 102 103 autoFormat = mkOption { 104 default = false; 105 type = types.bool; 106 description = '' 107 If the device does not currently contain a filesystem (as 108 determined by {command}`blkid`), then automatically 109 format it with the filesystem type specified in 110 {option}`fsType`. Use with caution. 111 ''; 112 }; 113 114 formatOptions = mkOption { 115 visible = false; 116 type = types.unspecified; 117 default = null; 118 }; 119 120 autoResize = mkOption { 121 default = false; 122 type = types.bool; 123 description = '' 124 If set, the filesystem is grown to its maximum size before 125 being mounted. (This is typically the size of the containing 126 partition.) This is currently only supported for ext2/3/4 127 filesystems that are mounted during early boot. 128 ''; 129 }; 130 131 noCheck = mkOption { 132 default = false; 133 type = types.bool; 134 description = "Disable running fsck on this filesystem."; 135 }; 136 137 }; 138 139 config.options = mkMerge [ 140 (mkIf config.autoResize [ "x-systemd.growfs" ]) 141 (mkIf config.autoFormat [ "x-systemd.makefs" ]) 142 (mkIf (utils.fsNeededForBoot config) [ "x-initrd.mount" ]) 143 ]; 144 145 }; 146 147 # Makes sequence of `specialMount device mountPoint options fsType` commands. 148 # `systemMount` should be defined in the sourcing script. 149 makeSpecialMounts = mounts: 150 pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: '' 151 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}" 152 '') mounts); 153 154 makeFstabEntries = 155 let 156 fsToSkipCheck = [ 157 "none" 158 "auto" 159 "overlay" 160 "iso9660" 161 "bindfs" 162 "udf" 163 "btrfs" 164 "zfs" 165 "tmpfs" 166 "bcachefs" 167 "nfs" 168 "nfs4" 169 "nilfs2" 170 "vboxsf" 171 "squashfs" 172 "glusterfs" 173 "apfs" 174 "9p" 175 "cifs" 176 "prl_fs" 177 "vmhgfs" 178 ] ++ lib.optionals (!config.boot.initrd.checkJournalingFS) [ 179 "ext3" 180 "ext4" 181 "reiserfs" 182 "xfs" 183 "jfs" 184 "f2fs" 185 ]; 186 isBindMount = fs: builtins.elem "bind" fs.options; 187 skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs; 188 # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces 189 escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string; 190 in fstabFileSystems: { }: concatMapStrings (fs: 191 (if fs.device != null then escape fs.device 192 else if fs.label != null then "/dev/disk/by-label/${escape fs.label}" 193 else throw "No device specified for mount point ${fs.mountPoint}.") 194 + " " + escape fs.mountPoint 195 + " " + fs.fsType 196 + " " + escape (builtins.concatStringsSep "," fs.options) 197 + " 0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2") 198 + "\n" 199 ) fstabFileSystems; 200 201 initrdFstab = pkgs.writeText "initrd-fstab" (makeFstabEntries (filter utils.fsNeededForBoot fileSystems) { }); 202 203in 204 205{ 206 207 ###### interface 208 209 options = { 210 211 fileSystems = mkOption { 212 default = {}; 213 example = literalExpression '' 214 { 215 "/".device = "/dev/hda1"; 216 "/data" = { 217 device = "/dev/hda2"; 218 fsType = "ext3"; 219 options = [ "data=journal" ]; 220 }; 221 "/bigdisk".label = "bigdisk"; 222 } 223 ''; 224 type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]); 225 description = '' 226 The file systems to be mounted. It must include an entry for 227 the root directory (`mountPoint = "/"`). Each 228 entry in the list is an attribute set with the following fields: 229 `mountPoint`, `device`, 230 `fsType` (a file system type recognised by 231 {command}`mount`; defaults to 232 `"auto"`), and `options` 233 (the mount options passed to {command}`mount` using the 234 {option}`-o` flag; defaults to `[ "defaults" ]`). 235 236 Instead of specifying `device`, you can also 237 specify a volume label (`label`) for file 238 systems that support it, such as ext2/ext3 (see {command}`mke2fs -L`). 239 ''; 240 }; 241 242 system.fsPackages = mkOption { 243 internal = true; 244 default = [ ]; 245 description = "Packages supplying file system mounters and checkers."; 246 }; 247 248 boot.supportedFilesystems = mkOption { 249 default = { }; 250 example = lib.literalExpression '' 251 { 252 btrfs = true; 253 zfs = lib.mkForce false; 254 } 255 ''; 256 type = types.coercedTo 257 (types.listOf types.str) 258 (enabled: lib.listToAttrs (map (fs: lib.nameValuePair fs true) enabled)) 259 (types.attrsOf types.bool); 260 description = '' 261 Names of supported filesystem types, or an attribute set of file system types 262 and their state. The set form may be used together with `lib.mkForce` to 263 explicitly disable support for specific filesystems, e.g. to disable ZFS 264 with an unsupported kernel. 265 ''; 266 }; 267 268 boot.specialFileSystems = mkOption { 269 default = {}; 270 type = types.attrsOf (types.submodule coreFileSystemOpts); 271 internal = true; 272 description = '' 273 Special filesystems that are mounted very early during boot. 274 ''; 275 }; 276 277 boot.devSize = mkOption { 278 default = "5%"; 279 example = "32m"; 280 type = types.str; 281 description = '' 282 Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option, 283 for the accepted syntax. 284 ''; 285 }; 286 287 boot.devShmSize = mkOption { 288 default = "50%"; 289 example = "256m"; 290 type = types.str; 291 description = '' 292 Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option, 293 for the accepted syntax. 294 ''; 295 }; 296 297 boot.runSize = mkOption { 298 default = "25%"; 299 example = "256m"; 300 type = types.str; 301 description = '' 302 Size limit for the /run tmpfs. Look at mount(8), tmpfs size option, 303 for the accepted syntax. 304 ''; 305 }; 306 }; 307 308 309 ###### implementation 310 311 config = { 312 313 assertions = let 314 ls = sep: concatMapStringsSep sep (x: x.mountPoint); 315 resizableFSes = [ 316 "ext3" 317 "ext4" 318 "btrfs" 319 "xfs" 320 ]; 321 notAutoResizable = fs: fs.autoResize && !(builtins.elem fs.fsType resizableFSes); 322 in [ 323 { assertion = ! (fileSystems' ? cycle); 324 message = "The fileSystems option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}"; 325 } 326 { assertion = ! (any notAutoResizable fileSystems); 327 message = let 328 fs = head (filter notAutoResizable fileSystems); 329 in '' 330 Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = "${fs.fsType}"' 331 ${optionalString (fs.fsType == "auto") "fsType has to be explicitly set and"} 332 only the following support it: ${lib.concatStringsSep ", " resizableFSes}. 333 ''; 334 } 335 { 336 assertion = ! (any (fs: fs.formatOptions != null) fileSystems); 337 message = let 338 fs = head (filter (fs: fs.formatOptions != null) fileSystems); 339 in '' 340 'fileSystems.<name>.formatOptions' has been removed, since 341 systemd-makefs does not support any way to provide formatting 342 options. 343 ''; 344 } 345 ]; 346 347 # Export for use in other modules 348 system.build.fileSystems = fileSystems; 349 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result; 350 351 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; 352 353 # Add the mount helpers to the system path so that `mount' can find them. 354 system.fsPackages = [ pkgs.dosfstools ]; 355 356 environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages; 357 358 environment.etc.fstab.text = 359 let 360 swapOptions = sw: concatStringsSep "," ( 361 sw.options 362 ++ optional (sw.priority != null) "pri=${toString sw.priority}" 363 ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}" 364 ); 365 in '' 366 # This is a generated file. Do not edit! 367 # 368 # To make changes, edit the fileSystems and swapDevices NixOS options 369 # in your /etc/nixos/configuration.nix file. 370 # 371 # <file system> <mount point> <type> <options> <dump> <pass> 372 373 # Filesystems. 374 ${makeFstabEntries fileSystems {}} 375 376 # Swap devices. 377 ${flip concatMapStrings config.swapDevices (sw: 378 "${sw.realDevice} none swap ${swapOptions sw}\n" 379 )} 380 ''; 381 382 boot.initrd.systemd.storePaths = [initrdFstab]; 383 boot.initrd.systemd.managerEnvironment.SYSTEMD_SYSROOT_FSTAB = initrdFstab; 384 boot.initrd.systemd.services.initrd-parse-etc.environment.SYSTEMD_SYSROOT_FSTAB = initrdFstab; 385 386 # Provide a target that pulls in all filesystems. 387 systemd.targets.fs = 388 { description = "All File Systems"; 389 wants = [ "local-fs.target" "remote-fs.target" ]; 390 }; 391 392 systemd.services = { 393 # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore. 394 # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then. 395 "mount-pstore" = { 396 serviceConfig = { 397 Type = "oneshot"; 398 # skip on kernels without the pstore module 399 ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore"; 400 ExecStart = pkgs.writeShellScript "mount-pstore.sh" '' 401 set -eu 402 # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons. 403 ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore 404 # wait up to 1.5 seconds for the backend to be registered and the files to appear. a systemd path unit cannot detect this happening; and succeeding after a restart would not start dependent units. 405 TRIES=15 406 while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do 407 if (( $TRIES )); then 408 sleep 0.1 409 TRIES=$((TRIES-1)) 410 else 411 echo "Persistent Storage backend was not registered in time." >&2 412 break 413 fi 414 done 415 ''; 416 RemainAfterExit = true; 417 }; 418 unitConfig = { 419 ConditionVirtualization = "!container"; 420 DefaultDependencies = false; # needed to prevent a cycle 421 }; 422 before = [ "systemd-pstore.service" "shutdown.target" ]; 423 conflicts = [ "shutdown.target" ]; 424 wantedBy = [ "systemd-pstore.service" ]; 425 }; 426 }; 427 428 systemd.tmpfiles.rules = [ 429 "d /run/keys 0750 root ${toString config.ids.gids.keys}" 430 "z /run/keys 0750 root ${toString config.ids.gids.keys}" 431 ]; 432 433 # Sync mount options with systemd's src/core/mount-setup.c: mount_table. 434 boot.specialFileSystems = { 435 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; }; 436 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; }; 437 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; }; 438 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; }; 439 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; }; 440 441 # To hold secrets that shouldn't be written to disk 442 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; }; 443 } // optionalAttrs (!config.boot.isContainer) { 444 # systemd-nspawn populates /sys by itself, and remounting it causes all 445 # kinds of weird issues (most noticeably, waiting for host disk device 446 # nodes). 447 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; }; 448 }; 449 450 }; 451 452}