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