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