at 21.11-pre 14 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 coreFileSystemOpts = { name, config, ... }: { 28 29 options = { 30 mountPoint = mkOption { 31 example = "/mnt/usb"; 32 type = addCheckDesc "non-empty without trailing slash" types.str 33 (s: isNonEmpty s && (builtins.match ".+/" s) == null); 34 description = "Location of the mounted the file system."; 35 }; 36 37 device = mkOption { 38 default = null; 39 example = "/dev/sda"; 40 type = types.nullOr nonEmptyStr; 41 description = "Location of the device."; 42 }; 43 44 fsType = mkOption { 45 default = "auto"; 46 example = "ext3"; 47 type = nonEmptyStr; 48 description = "Type of the file system."; 49 }; 50 51 options = mkOption { 52 default = [ "defaults" ]; 53 example = [ "data=journal" ]; 54 description = "Options used to mount the file system."; 55 type = types.listOf nonEmptyStr; 56 }; 57 58 }; 59 60 config = { 61 mountPoint = mkDefault name; 62 device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType); 63 }; 64 65 }; 66 67 fileSystemOpts = { config, ... }: { 68 69 options = { 70 71 label = mkOption { 72 default = null; 73 example = "root-partition"; 74 type = types.nullOr nonEmptyStr; 75 description = "Label of the device (if any)."; 76 }; 77 78 autoFormat = mkOption { 79 default = false; 80 type = types.bool; 81 description = '' 82 If the device does not currently contain a filesystem (as 83 determined by <command>blkid</command>, then automatically 84 format it with the filesystem type specified in 85 <option>fsType</option>. Use with caution. 86 ''; 87 }; 88 89 formatOptions = mkOption { 90 default = ""; 91 type = types.str; 92 description = '' 93 If <option>autoFormat</option> option is set specifies 94 extra options passed to mkfs. 95 ''; 96 }; 97 98 autoResize = mkOption { 99 default = false; 100 type = types.bool; 101 description = '' 102 If set, the filesystem is grown to its maximum size before 103 being mounted. (This is typically the size of the containing 104 partition.) This is currently only supported for ext2/3/4 105 filesystems that are mounted during early boot. 106 ''; 107 }; 108 109 noCheck = mkOption { 110 default = false; 111 type = types.bool; 112 description = "Disable running fsck on this filesystem."; 113 }; 114 115 }; 116 117 config = let 118 defaultFormatOptions = 119 # -F needed to allow bare block device without partitions 120 if (builtins.substring 0 3 config.fsType) == "ext" then "-F" 121 # -q needed for non-interactive operations 122 else if config.fsType == "jfs" then "-q" 123 # (same here) 124 else if config.fsType == "reiserfs" then "-q" 125 else null; 126 in { 127 options = mkIf config.autoResize [ "x-nixos.autoresize" ]; 128 formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions); 129 }; 130 131 }; 132 133 # Makes sequence of `specialMount device mountPoint options fsType` commands. 134 # `systemMount` should be defined in the sourcing script. 135 makeSpecialMounts = mounts: 136 pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: '' 137 specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}" 138 '') mounts); 139 140in 141 142{ 143 144 ###### interface 145 146 options = { 147 148 fileSystems = mkOption { 149 default = {}; 150 example = literalExample '' 151 { 152 "/".device = "/dev/hda1"; 153 "/data" = { 154 device = "/dev/hda2"; 155 fsType = "ext3"; 156 options = [ "data=journal" ]; 157 }; 158 "/bigdisk".label = "bigdisk"; 159 } 160 ''; 161 type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]); 162 description = '' 163 The file systems to be mounted. It must include an entry for 164 the root directory (<literal>mountPoint = "/"</literal>). Each 165 entry in the list is an attribute set with the following fields: 166 <literal>mountPoint</literal>, <literal>device</literal>, 167 <literal>fsType</literal> (a file system type recognised by 168 <command>mount</command>; defaults to 169 <literal>"auto"</literal>), and <literal>options</literal> 170 (the mount options passed to <command>mount</command> using the 171 <option>-o</option> flag; defaults to <literal>[ "defaults" ]</literal>). 172 173 Instead of specifying <literal>device</literal>, you can also 174 specify a volume label (<literal>label</literal>) for file 175 systems that support it, such as ext2/ext3 (see <command>mke2fs 176 -L</command>). 177 ''; 178 }; 179 180 system.fsPackages = mkOption { 181 internal = true; 182 default = [ ]; 183 description = "Packages supplying file system mounters and checkers."; 184 }; 185 186 boot.supportedFilesystems = mkOption { 187 default = [ ]; 188 example = [ "btrfs" ]; 189 type = types.listOf types.str; 190 description = "Names of supported filesystem types."; 191 }; 192 193 boot.specialFileSystems = mkOption { 194 default = {}; 195 type = types.attrsOf (types.submodule coreFileSystemOpts); 196 internal = true; 197 description = '' 198 Special filesystems that are mounted very early during boot. 199 ''; 200 }; 201 202 }; 203 204 205 ###### implementation 206 207 config = { 208 209 assertions = let 210 ls = sep: concatMapStringsSep sep (x: x.mountPoint); 211 notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs"); 212 in [ 213 { assertion = ! (fileSystems' ? cycle); 214 message = "The fileSystems option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}"; 215 } 216 { assertion = ! (any notAutoResizable fileSystems); 217 message = let 218 fs = head (filter notAutoResizable fileSystems); 219 in 220 "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."; 221 } 222 ]; 223 224 # Export for use in other modules 225 system.build.fileSystems = fileSystems; 226 system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result; 227 228 boot.supportedFilesystems = map (fs: fs.fsType) fileSystems; 229 230 # Add the mount helpers to the system path so that `mount' can find them. 231 system.fsPackages = [ pkgs.dosfstools ]; 232 233 environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages; 234 235 environment.etc.fstab.text = 236 let 237 fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" ]; 238 skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck; 239 # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces 240 escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string; 241 swapOptions = sw: "defaults" 242 + optionalString (sw.priority != null) ",pri=${toString sw.priority}"; 243 in '' 244 # This is a generated file. Do not edit! 245 # 246 # To make changes, edit the fileSystems and swapDevices NixOS options 247 # in your /etc/nixos/configuration.nix file. 248 249 # Filesystems. 250 ${concatMapStrings (fs: 251 (if fs.device != null then escape fs.device 252 else if fs.label != null then "/dev/disk/by-label/${escape fs.label}" 253 else throw "No device specified for mount point ${fs.mountPoint}.") 254 + " " + escape fs.mountPoint 255 + " " + fs.fsType 256 + " " + builtins.concatStringsSep "," fs.options 257 + " 0" 258 + " " + (if skipCheck fs then "0" else 259 if fs.mountPoint == "/" then "1" else "2") 260 + "\n" 261 ) fileSystems} 262 263 # Swap devices. 264 ${flip concatMapStrings config.swapDevices (sw: 265 "${sw.realDevice} none swap ${swapOptions sw}\n" 266 )} 267 ''; 268 269 # Provide a target that pulls in all filesystems. 270 systemd.targets.fs = 271 { description = "All File Systems"; 272 wants = [ "local-fs.target" "remote-fs.target" ]; 273 }; 274 275 systemd.services = 276 277 # Emit systemd services to format requested filesystems. 278 let 279 formatDevice = fs: 280 let 281 mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount"; 282 device' = escapeSystemdPath fs.device; 283 device'' = "${device'}.device"; 284 in nameValuePair "mkfs-${device'}" 285 { description = "Initialisation of Filesystem ${fs.device}"; 286 wantedBy = [ mountPoint' ]; 287 before = [ mountPoint' "systemd-fsck@${device'}.service" ]; 288 requires = [ device'' ]; 289 after = [ device'' ]; 290 path = [ pkgs.util-linux ] ++ config.system.fsPackages; 291 script = 292 '' 293 if ! [ -e "${fs.device}" ]; then exit 1; fi 294 # FIXME: this is scary. The test could be more robust. 295 type=$(blkid -p -s TYPE -o value "${fs.device}" || true) 296 if [ -z "$type" ]; then 297 echo "creating ${fs.fsType} filesystem on ${fs.device}..." 298 mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}" 299 fi 300 ''; 301 unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ]; 302 unitConfig.DefaultDependencies = false; # needed to prevent a cycle 303 serviceConfig.Type = "oneshot"; 304 }; 305 in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems)) // { 306 # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore. 307 # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then. 308 # Since the pstore filesystem is usually empty right after mounting because the backend isn't registered yet, and a path unit cannot detect files inside of it, the same service waits for that to happen. systemd's restart mechanism can't be used here because the first failure also fails all dependent units. 309 "mount-pstore" = { 310 serviceConfig = { 311 Type = "oneshot"; 312 ExecStart = "${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore"; 313 ExecStartPost = pkgs.writeShellScript "wait-for-pstore.sh" '' 314 set -eu 315 TRIES=0 316 while [ $TRIES -lt 20 ] && [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do 317 sleep 0.1 318 TRIES=$((TRIES+1)) 319 done 320 ''; 321 RemainAfterExit = true; 322 }; 323 unitConfig = { 324 ConditionVirtualization = "!container"; 325 DefaultDependencies = false; # needed to prevent a cycle 326 }; 327 after = [ "modprobe@pstore.service" ]; 328 requires = [ "modprobe@pstore.service" ]; 329 before = [ "systemd-pstore.service" ]; 330 wantedBy = [ "systemd-pstore.service" ]; 331 }; 332 }; 333 334 systemd.tmpfiles.rules = [ 335 "d /run/keys 0750 root ${toString config.ids.gids.keys}" 336 "z /run/keys 0750 root ${toString config.ids.gids.keys}" 337 ]; 338 339 # Sync mount options with systemd's src/core/mount-setup.c: mount_table. 340 boot.specialFileSystems = { 341 "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; }; 342 "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; }; 343 "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; }; 344 "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; }; 345 "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; }; 346 347 # To hold secrets that shouldn't be written to disk 348 "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; }; 349 } // optionalAttrs (!config.boot.isContainer) { 350 # systemd-nspawn populates /sys by itself, and remounting it causes all 351 # kinds of weird issues (most noticeably, waiting for host disk device 352 # nodes). 353 "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; }; 354 }; 355 356 }; 357 358}