at 23.11-beta 15 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 stratis.poolUuid = lib.mkOption { 40 type = types.uniq (types.nullOr types.str); 41 description = lib.mdDoc '' 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 = lib.mdDoc "Location of the device."; 53 }; 54 55 fsType = mkOption { 56 default = "auto"; 57 example = "ext3"; 58 type = nonEmptyStr; 59 description = lib.mdDoc "Type of the file system."; 60 }; 61 62 options = mkOption { 63 default = [ "defaults" ]; 64 example = [ "data=journal" ]; 65 description = lib.mdDoc "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 = lib.mdDoc '' 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 = lib.mdDoc "Label of the device (if any)."; 101 }; 102 103 autoFormat = mkOption { 104 default = false; 105 type = types.bool; 106 description = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc "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 = lib.mdDoc '' 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 = lib.mdDoc "Packages supplying file system mounters and checkers."; 246 }; 247 248 boot.supportedFilesystems = mkOption { 249 default = [ ]; 250 example = [ "btrfs" ]; 251 type = types.listOf types.str; 252 description = lib.mdDoc "Names of supported filesystem types."; 253 }; 254 255 boot.specialFileSystems = mkOption { 256 default = {}; 257 type = types.attrsOf (types.submodule coreFileSystemOpts); 258 internal = true; 259 description = lib.mdDoc '' 260 Special filesystems that are mounted very early during boot. 261 ''; 262 }; 263 264 boot.devSize = mkOption { 265 default = "5%"; 266 example = "32m"; 267 type = types.str; 268 description = lib.mdDoc '' 269 Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option, 270 for the accepted syntax. 271 ''; 272 }; 273 274 boot.devShmSize = mkOption { 275 default = "50%"; 276 example = "256m"; 277 type = types.str; 278 description = lib.mdDoc '' 279 Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option, 280 for the accepted syntax. 281 ''; 282 }; 283 284 boot.runSize = mkOption { 285 default = "25%"; 286 example = "256m"; 287 type = types.str; 288 description = lib.mdDoc '' 289 Size limit for the /run tmpfs. Look at mount(8), tmpfs size option, 290 for the accepted syntax. 291 ''; 292 }; 293 }; 294 295 296 ###### implementation 297 298 config = { 299 300 assertions = let 301 ls = sep: concatMapStringsSep sep (x: x.mountPoint); 302 resizableFSes = [ 303 "ext3" 304 "ext4" 305 "btrfs" 306 "xfs" 307 ]; 308 notAutoResizable = fs: fs.autoResize && !(builtins.elem fs.fsType resizableFSes); 309 in [ 310 { assertion = ! (fileSystems' ? cycle); 311 message = "The fileSystems option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}"; 312 } 313 { assertion = ! (any notAutoResizable fileSystems); 314 message = let 315 fs = head (filter notAutoResizable fileSystems); 316 in '' 317 Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = "${fs.fsType}"' 318 ${optionalString (fs.fsType == "auto") "fsType has to be explicitly set and"} 319 only the following support it: ${lib.concatStringsSep ", " resizableFSes}. 320 ''; 321 } 322 { 323 assertion = ! (any (fs: fs.formatOptions != null) fileSystems); 324 message = let 325 fs = head (filter (fs: fs.formatOptions != null) fileSystems); 326 in '' 327 'fileSystems.<name>.formatOptions' has been removed, since 328 systemd-makefs does not support any way to provide formatting 329 options. 330 ''; 331 } 332 ]; 333 334 # Export for use in other modules 335 system.build.fileSystems = fileSystems; 336 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result; 337 338 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; 339 340 # Add the mount helpers to the system path so that `mount' can find them. 341 system.fsPackages = [ pkgs.dosfstools ]; 342 343 environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages; 344 345 environment.etc.fstab.text = 346 let 347 swapOptions = sw: concatStringsSep "," ( 348 sw.options 349 ++ optional (sw.priority != null) "pri=${toString sw.priority}" 350 ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}" 351 ); 352 in '' 353 # This is a generated file. Do not edit! 354 # 355 # To make changes, edit the fileSystems and swapDevices NixOS options 356 # in your /etc/nixos/configuration.nix file. 357 # 358 # <file system> <mount point> <type> <options> <dump> <pass> 359 360 # Filesystems. 361 ${makeFstabEntries fileSystems {}} 362 363 # Swap devices. 364 ${flip concatMapStrings config.swapDevices (sw: 365 "${sw.realDevice} none swap ${swapOptions sw}\n" 366 )} 367 ''; 368 369 boot.initrd.systemd.storePaths = [initrdFstab]; 370 boot.initrd.systemd.managerEnvironment.SYSTEMD_SYSROOT_FSTAB = initrdFstab; 371 boot.initrd.systemd.services.initrd-parse-etc.environment.SYSTEMD_SYSROOT_FSTAB = initrdFstab; 372 373 # Provide a target that pulls in all filesystems. 374 systemd.targets.fs = 375 { description = "All File Systems"; 376 wants = [ "local-fs.target" "remote-fs.target" ]; 377 }; 378 379 systemd.services = { 380 # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore. 381 # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then. 382 "mount-pstore" = { 383 serviceConfig = { 384 Type = "oneshot"; 385 # skip on kernels without the pstore module 386 ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore"; 387 ExecStart = pkgs.writeShellScript "mount-pstore.sh" '' 388 set -eu 389 # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons. 390 ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore 391 # 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. 392 TRIES=15 393 while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do 394 if (( $TRIES )); then 395 sleep 0.1 396 TRIES=$((TRIES-1)) 397 else 398 echo "Persistent Storage backend was not registered in time." >&2 399 break 400 fi 401 done 402 ''; 403 RemainAfterExit = true; 404 }; 405 unitConfig = { 406 ConditionVirtualization = "!container"; 407 DefaultDependencies = false; # needed to prevent a cycle 408 }; 409 before = [ "systemd-pstore.service" ]; 410 wantedBy = [ "systemd-pstore.service" ]; 411 }; 412 }; 413 414 systemd.tmpfiles.rules = [ 415 "d /run/keys 0750 root ${toString config.ids.gids.keys}" 416 "z /run/keys 0750 root ${toString config.ids.gids.keys}" 417 ]; 418 419 # Sync mount options with systemd's src/core/mount-setup.c: mount_table. 420 boot.specialFileSystems = { 421 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; }; 422 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; }; 423 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; }; 424 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; }; 425 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; }; 426 427 # To hold secrets that shouldn't be written to disk 428 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; }; 429 } // optionalAttrs (!config.boot.isContainer) { 430 # systemd-nspawn populates /sys by itself, and remounting it causes all 431 # kinds of weird issues (most noticeably, waiting for host disk device 432 # nodes). 433 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; }; 434 }; 435 436 }; 437 438}