at 23.05-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 = lib.mdDoc "Location of the mounted the 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.listOf 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 = mkIf config.autoResize [ "x-nixos.autoresize" ]; 144 formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions); 145 }; 146 147 }; 148 149 # Makes sequence of `specialMount device mountPoint options fsType` commands. 150 # `systemMount` should be defined in the sourcing script. 151 makeSpecialMounts = mounts: 152 pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: '' 153 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}" 154 '') mounts); 155 156 makeFstabEntries = 157 let 158 fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "nfs4" "vboxsf" "glusterfs" "apfs" "9p" "cifs" "prl_fs" "vmhgfs" ]; 159 isBindMount = fs: builtins.elem "bind" fs.options; 160 skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck || isBindMount fs; 161 # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces 162 escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string; 163 in fstabFileSystems: { rootPrefix ? "", excludeChecks ? false, extraOpts ? (fs: []) }: concatMapStrings (fs: 164 (optionalString (isBindMount fs) (escape rootPrefix)) 165 + (if fs.device != null then escape fs.device 166 else if fs.label != null then "/dev/disk/by-label/${escape fs.label}" 167 else throw "No device specified for mount point ${fs.mountPoint}.") 168 + " " + escape (rootPrefix + fs.mountPoint) 169 + " " + fs.fsType 170 + " " + escape (builtins.concatStringsSep "," (fs.options ++ (extraOpts fs))) 171 + " " + (optionalString (!excludeChecks) 172 ("0 " + (if skipCheck fs then "0" else if fs.mountPoint == "/" then "1" else "2"))) 173 + "\n" 174 ) fstabFileSystems; 175 176 initrdFstab = pkgs.writeText "initrd-fstab" (makeFstabEntries (filter utils.fsNeededForBoot fileSystems) { 177 rootPrefix = "/sysroot"; 178 excludeChecks = true; 179 extraOpts = fs: 180 (optional fs.autoResize "x-systemd.growfs") 181 ++ (optional fs.autoFormat "x-systemd.makefs"); 182 }); 183 184in 185 186{ 187 188 ###### interface 189 190 options = { 191 192 fileSystems = mkOption { 193 default = {}; 194 example = literalExpression '' 195 { 196 "/".device = "/dev/hda1"; 197 "/data" = { 198 device = "/dev/hda2"; 199 fsType = "ext3"; 200 options = [ "data=journal" ]; 201 }; 202 "/bigdisk".label = "bigdisk"; 203 } 204 ''; 205 type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]); 206 description = lib.mdDoc '' 207 The file systems to be mounted. It must include an entry for 208 the root directory (`mountPoint = "/"`). Each 209 entry in the list is an attribute set with the following fields: 210 `mountPoint`, `device`, 211 `fsType` (a file system type recognised by 212 {command}`mount`; defaults to 213 `"auto"`), and `options` 214 (the mount options passed to {command}`mount` using the 215 {option}`-o` flag; defaults to `[ "defaults" ]`). 216 217 Instead of specifying `device`, you can also 218 specify a volume label (`label`) for file 219 systems that support it, such as ext2/ext3 (see {command}`mke2fs -L`). 220 ''; 221 }; 222 223 system.fsPackages = mkOption { 224 internal = true; 225 default = [ ]; 226 description = lib.mdDoc "Packages supplying file system mounters and checkers."; 227 }; 228 229 boot.supportedFilesystems = mkOption { 230 default = [ ]; 231 example = [ "btrfs" ]; 232 type = types.listOf types.str; 233 description = lib.mdDoc "Names of supported filesystem types."; 234 }; 235 236 boot.specialFileSystems = mkOption { 237 default = {}; 238 type = types.attrsOf (types.submodule coreFileSystemOpts); 239 internal = true; 240 description = lib.mdDoc '' 241 Special filesystems that are mounted very early during boot. 242 ''; 243 }; 244 245 boot.devSize = mkOption { 246 default = "5%"; 247 example = "32m"; 248 type = types.str; 249 description = lib.mdDoc '' 250 Size limit for the /dev tmpfs. Look at mount(8), tmpfs size option, 251 for the accepted syntax. 252 ''; 253 }; 254 255 boot.devShmSize = mkOption { 256 default = "50%"; 257 example = "256m"; 258 type = types.str; 259 description = lib.mdDoc '' 260 Size limit for the /dev/shm tmpfs. Look at mount(8), tmpfs size option, 261 for the accepted syntax. 262 ''; 263 }; 264 265 boot.runSize = mkOption { 266 default = "25%"; 267 example = "256m"; 268 type = types.str; 269 description = lib.mdDoc '' 270 Size limit for the /run tmpfs. Look at mount(8), tmpfs size option, 271 for the accepted syntax. 272 ''; 273 }; 274 }; 275 276 277 ###### implementation 278 279 config = { 280 281 assertions = let 282 ls = sep: concatMapStringsSep sep (x: x.mountPoint); 283 notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs"); 284 in [ 285 { assertion = ! (fileSystems' ? cycle); 286 message = "The fileSystems option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}"; 287 } 288 { assertion = ! (any notAutoResizable fileSystems); 289 message = let 290 fs = head (filter notAutoResizable fileSystems); 291 in 292 "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${if fs.fsType == "auto" then " fsType has to be explicitly set and" else ""} only the ext filesystems and f2fs support it."; 293 } 294 ]; 295 296 # Export for use in other modules 297 system.build.fileSystems = fileSystems; 298 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result; 299 300 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; 301 302 # Add the mount helpers to the system path so that `mount' can find them. 303 system.fsPackages = [ pkgs.dosfstools ]; 304 305 environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages; 306 307 environment.etc.fstab.text = 308 let 309 swapOptions = sw: concatStringsSep "," ( 310 sw.options 311 ++ optional (sw.priority != null) "pri=${toString sw.priority}" 312 ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}" 313 ); 314 in '' 315 # This is a generated file. Do not edit! 316 # 317 # To make changes, edit the fileSystems and swapDevices NixOS options 318 # in your /etc/nixos/configuration.nix file. 319 # 320 # <file system> <mount point> <type> <options> <dump> <pass> 321 322 # Filesystems. 323 ${makeFstabEntries fileSystems {}} 324 325 # Swap devices. 326 ${flip concatMapStrings config.swapDevices (sw: 327 "${sw.realDevice} none swap ${swapOptions sw}\n" 328 )} 329 ''; 330 331 boot.initrd.systemd.contents."/etc/fstab".source = initrdFstab; 332 333 # Provide a target that pulls in all filesystems. 334 systemd.targets.fs = 335 { description = "All File Systems"; 336 wants = [ "local-fs.target" "remote-fs.target" ]; 337 }; 338 339 systemd.services = 340 341 # Emit systemd services to format requested filesystems. 342 let 343 formatDevice = fs: 344 let 345 mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount"; 346 device' = escapeSystemdPath fs.device; 347 device'' = "${device'}.device"; 348 in nameValuePair "mkfs-${device'}" 349 { description = "Initialisation of Filesystem ${fs.device}"; 350 wantedBy = [ mountPoint' ]; 351 before = [ mountPoint' "systemd-fsck@${device'}.service" ]; 352 requires = [ device'' ]; 353 after = [ device'' ]; 354 path = [ pkgs.util-linux ] ++ config.system.fsPackages; 355 script = 356 '' 357 if ! [ -e "${fs.device}" ]; then exit 1; fi 358 # FIXME: this is scary. The test could be more robust. 359 type=$(blkid -p -s TYPE -o value "${fs.device}" || true) 360 if [ -z "$type" ]; then 361 echo "creating ${fs.fsType} filesystem on ${fs.device}..." 362 mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}" 363 fi 364 ''; 365 unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ]; 366 unitConfig.DefaultDependencies = false; # needed to prevent a cycle 367 serviceConfig.Type = "oneshot"; 368 }; 369 in listToAttrs (map formatDevice (filter (fs: fs.autoFormat && !(utils.fsNeededForBoot fs)) fileSystems)) // { 370 # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore. 371 # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then. 372 "mount-pstore" = { 373 serviceConfig = { 374 Type = "oneshot"; 375 # skip on kernels without the pstore module 376 ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore"; 377 ExecStart = pkgs.writeShellScript "mount-pstore.sh" '' 378 set -eu 379 # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons. 380 ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore 381 # 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. 382 TRIES=15 383 while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do 384 if (( $TRIES )); then 385 sleep 0.1 386 TRIES=$((TRIES-1)) 387 else 388 echo "Persistent Storage backend was not registered in time." >&2 389 break 390 fi 391 done 392 ''; 393 RemainAfterExit = true; 394 }; 395 unitConfig = { 396 ConditionVirtualization = "!container"; 397 DefaultDependencies = false; # needed to prevent a cycle 398 }; 399 before = [ "systemd-pstore.service" ]; 400 wantedBy = [ "systemd-pstore.service" ]; 401 }; 402 }; 403 404 systemd.tmpfiles.rules = [ 405 "d /run/keys 0750 root ${toString config.ids.gids.keys}" 406 "z /run/keys 0750 root ${toString config.ids.gids.keys}" 407 ]; 408 409 # Sync mount options with systemd's src/core/mount-setup.c: mount_table. 410 boot.specialFileSystems = { 411 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; }; 412 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; }; 413 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; }; 414 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; }; 415 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; }; 416 417 # To hold secrets that shouldn't be written to disk 418 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; }; 419 } // optionalAttrs (!config.boot.isContainer) { 420 # systemd-nspawn populates /sys by itself, and remounting it causes all 421 # kinds of weird issues (most noticeably, waiting for host disk device 422 # nodes). 423 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; }; 424 }; 425 426 }; 427 428}