summary refs log tree commit diff
path: root/nixos/modules/tasks/filesystems.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/tasks/filesystems.nix')
-rw-r--r--nixos/modules/tasks/filesystems.nix385
1 files changed, 385 insertions, 0 deletions
diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix
new file mode 100644
index 00000000000..f3da6771197
--- /dev/null
+++ b/nixos/modules/tasks/filesystems.nix
@@ -0,0 +1,385 @@
+{ config, lib, pkgs, utils, ... }:
+
+with lib;
+with utils;
+
+let
+
+  addCheckDesc = desc: elemType: check: types.addCheck elemType check
+    // { description = "${elemType.description} (with check: ${desc})"; };
+
+  isNonEmpty = s: (builtins.match "[ \t\n]*" s) == null;
+  nonEmptyStr = addCheckDesc "non-empty" types.str isNonEmpty;
+
+  fileSystems' = toposort fsBefore (attrValues config.fileSystems);
+
+  fileSystems = if fileSystems' ? result
+                then # use topologically sorted fileSystems everywhere
+                     fileSystems'.result
+                else # the assertion below will catch this,
+                     # but we fall back to the original order
+                     # anyway so that other modules could check
+                     # their assertions too
+                     (attrValues config.fileSystems);
+
+  specialFSTypes = [ "proc" "sysfs" "tmpfs" "ramfs" "devtmpfs" "devpts" ];
+
+  nonEmptyWithoutTrailingSlash = addCheckDesc "non-empty without trailing slash" types.str
+    (s: isNonEmpty s && (builtins.match ".+/" s) == null);
+
+  coreFileSystemOpts = { name, config, ... }: {
+
+    options = {
+      mountPoint = mkOption {
+        example = "/mnt/usb";
+        type = nonEmptyWithoutTrailingSlash;
+        description = "Location of the mounted the file system.";
+      };
+
+      device = mkOption {
+        default = null;
+        example = "/dev/sda";
+        type = types.nullOr nonEmptyStr;
+        description = "Location of the device.";
+      };
+
+      fsType = mkOption {
+        default = "auto";
+        example = "ext3";
+        type = nonEmptyStr;
+        description = "Type of the file system.";
+      };
+
+      options = mkOption {
+        default = [ "defaults" ];
+        example = [ "data=journal" ];
+        description = "Options used to mount the file system.";
+        type = types.listOf nonEmptyStr;
+      };
+
+      depends = mkOption {
+        default = [ ];
+        example = [ "/persist" ];
+        type = types.listOf nonEmptyWithoutTrailingSlash;
+        description = ''
+          List of paths that should be mounted before this one. This filesystem's
+          <option>device</option> and <option>mountPoint</option> are always
+          checked and do not need to be included explicitly. If a path is added
+          to this list, any other filesystem whose mount point is a parent of
+          the path will be mounted before this filesystem. The paths do not need
+          to actually be the <option>mountPoint</option> of some other filesystem.
+        '';
+      };
+
+    };
+
+    config = {
+      mountPoint = mkDefault name;
+      device = mkIf (elem config.fsType specialFSTypes) (mkDefault config.fsType);
+    };
+
+  };
+
+  fileSystemOpts = { config, ... }: {
+
+    options = {
+
+      label = mkOption {
+        default = null;
+        example = "root-partition";
+        type = types.nullOr nonEmptyStr;
+        description = "Label of the device (if any).";
+      };
+
+      autoFormat = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If the device does not currently contain a filesystem (as
+          determined by <command>blkid</command>, then automatically
+          format it with the filesystem type specified in
+          <option>fsType</option>.  Use with caution.
+        '';
+      };
+
+      formatOptions = mkOption {
+        default = "";
+        type = types.str;
+        description = ''
+          If <option>autoFormat</option> option is set specifies
+          extra options passed to mkfs.
+        '';
+      };
+
+      autoResize = mkOption {
+        default = false;
+        type = types.bool;
+        description = ''
+          If set, the filesystem is grown to its maximum size before
+          being mounted. (This is typically the size of the containing
+          partition.) This is currently only supported for ext2/3/4
+          filesystems that are mounted during early boot.
+        '';
+      };
+
+      noCheck = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Disable running fsck on this filesystem.";
+      };
+
+    };
+
+    config = let
+      defaultFormatOptions =
+        # -F needed to allow bare block device without partitions
+        if (builtins.substring 0 3 config.fsType) == "ext" then "-F"
+        # -q needed for non-interactive operations
+        else if config.fsType == "jfs" then "-q"
+        # (same here)
+        else if config.fsType == "reiserfs" then "-q"
+        else null;
+    in {
+      options = mkIf config.autoResize [ "x-nixos.autoresize" ];
+      formatOptions = mkIf (defaultFormatOptions != null) (mkDefault defaultFormatOptions);
+    };
+
+  };
+
+  # Makes sequence of `specialMount device mountPoint options fsType` commands.
+  # `systemMount` should be defined in the sourcing script.
+  makeSpecialMounts = mounts:
+    pkgs.writeText "mounts.sh" (concatMapStringsSep "\n" (mount: ''
+      specialMount "${mount.device}" "${mount.mountPoint}" "${concatStringsSep "," mount.options}" "${mount.fsType}"
+    '') mounts);
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    fileSystems = mkOption {
+      default = {};
+      example = literalExpression ''
+        {
+          "/".device = "/dev/hda1";
+          "/data" = {
+            device = "/dev/hda2";
+            fsType = "ext3";
+            options = [ "data=journal" ];
+          };
+          "/bigdisk".label = "bigdisk";
+        }
+      '';
+      type = types.attrsOf (types.submodule [coreFileSystemOpts fileSystemOpts]);
+      description = ''
+        The file systems to be mounted.  It must include an entry for
+        the root directory (<literal>mountPoint = "/"</literal>).  Each
+        entry in the list is an attribute set with the following fields:
+        <literal>mountPoint</literal>, <literal>device</literal>,
+        <literal>fsType</literal> (a file system type recognised by
+        <command>mount</command>; defaults to
+        <literal>"auto"</literal>), and <literal>options</literal>
+        (the mount options passed to <command>mount</command> using the
+        <option>-o</option> flag; defaults to <literal>[ "defaults" ]</literal>).
+
+        Instead of specifying <literal>device</literal>, you can also
+        specify a volume label (<literal>label</literal>) for file
+        systems that support it, such as ext2/ext3 (see <command>mke2fs
+        -L</command>).
+      '';
+    };
+
+    system.fsPackages = mkOption {
+      internal = true;
+      default = [ ];
+      description = "Packages supplying file system mounters and checkers.";
+    };
+
+    boot.supportedFilesystems = mkOption {
+      default = [ ];
+      example = [ "btrfs" ];
+      type = types.listOf types.str;
+      description = "Names of supported filesystem types.";
+    };
+
+    boot.specialFileSystems = mkOption {
+      default = {};
+      type = types.attrsOf (types.submodule coreFileSystemOpts);
+      internal = true;
+      description = ''
+        Special filesystems that are mounted very early during boot.
+      '';
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = {
+
+    assertions = let
+      ls = sep: concatMapStringsSep sep (x: x.mountPoint);
+      notAutoResizable = fs: fs.autoResize && !(hasPrefix "ext" fs.fsType || fs.fsType == "f2fs");
+    in [
+      { assertion = ! (fileSystems' ? cycle);
+        message = "The ‘fileSystems’ option can't be topologically sorted: mountpoint dependency path ${ls " -> " fileSystems'.cycle} loops to ${ls ", " fileSystems'.loops}";
+      }
+      { assertion = ! (any notAutoResizable fileSystems);
+        message = let
+          fs = head (filter notAutoResizable fileSystems);
+        in
+          "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.";
+      }
+    ];
+
+    # Export for use in other modules
+    system.build.fileSystems = fileSystems;
+    system.build.earlyMountScript = makeSpecialMounts (toposort fsBefore (attrValues config.boot.specialFileSystems)).result;
+
+    boot.supportedFilesystems = map (fs: fs.fsType) fileSystems;
+
+    # Add the mount helpers to the system path so that `mount' can find them.
+    system.fsPackages = [ pkgs.dosfstools ];
+
+    environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages;
+
+    environment.etc.fstab.text =
+      let
+        fsToSkipCheck = [ "none" "bindfs" "btrfs" "zfs" "tmpfs" "nfs" "vboxsf" "glusterfs" "apfs" ];
+        skipCheck = fs: fs.noCheck || fs.device == "none" || builtins.elem fs.fsType fsToSkipCheck;
+        # https://wiki.archlinux.org/index.php/fstab#Filepath_spaces
+        escape = string: builtins.replaceStrings [ " " "\t" ] [ "\\040" "\\011" ] string;
+        swapOptions = sw: concatStringsSep "," (
+          sw.options
+          ++ optional (sw.priority != null) "pri=${toString sw.priority}"
+          ++ optional (sw.discardPolicy != null) "discard${optionalString (sw.discardPolicy != "both") "=${toString sw.discardPolicy}"}"
+        );
+      in ''
+        # This is a generated file.  Do not edit!
+        #
+        # To make changes, edit the fileSystems and swapDevices NixOS options
+        # in your /etc/nixos/configuration.nix file.
+        #
+        # <file system> <mount point>   <type>  <options>       <dump>  <pass>
+
+        # Filesystems.
+        ${concatMapStrings (fs:
+            (if fs.device != null then escape fs.device
+             else if fs.label != null then "/dev/disk/by-label/${escape fs.label}"
+             else throw "No device specified for mount point ‘${fs.mountPoint}’.")
+            + " " + escape fs.mountPoint
+            + " " + fs.fsType
+            + " " + builtins.concatStringsSep "," fs.options
+            + " 0"
+            + " " + (if skipCheck fs then "0" else
+                     if fs.mountPoint == "/" then "1" else "2")
+            + "\n"
+        ) fileSystems}
+
+        # Swap devices.
+        ${flip concatMapStrings config.swapDevices (sw:
+            "${sw.realDevice} none swap ${swapOptions sw}\n"
+        )}
+      '';
+
+    # Provide a target that pulls in all filesystems.
+    systemd.targets.fs =
+      { description = "All File Systems";
+        wants = [ "local-fs.target" "remote-fs.target" ];
+      };
+
+    systemd.services =
+
+    # Emit systemd services to format requested filesystems.
+      let
+        formatDevice = fs:
+          let
+            mountPoint' = "${escapeSystemdPath fs.mountPoint}.mount";
+            device'  = escapeSystemdPath fs.device;
+            device'' = "${device'}.device";
+          in nameValuePair "mkfs-${device'}"
+          { description = "Initialisation of Filesystem ${fs.device}";
+            wantedBy = [ mountPoint' ];
+            before = [ mountPoint' "systemd-fsck@${device'}.service" ];
+            requires = [ device'' ];
+            after = [ device'' ];
+            path = [ pkgs.util-linux ] ++ config.system.fsPackages;
+            script =
+              ''
+                if ! [ -e "${fs.device}" ]; then exit 1; fi
+                # FIXME: this is scary.  The test could be more robust.
+                type=$(blkid -p -s TYPE -o value "${fs.device}" || true)
+                if [ -z "$type" ]; then
+                  echo "creating ${fs.fsType} filesystem on ${fs.device}..."
+                  mkfs.${fs.fsType} ${fs.formatOptions} "${fs.device}"
+                fi
+              '';
+            unitConfig.RequiresMountsFor = [ "${dirOf fs.device}" ];
+            unitConfig.DefaultDependencies = false; # needed to prevent a cycle
+            serviceConfig.Type = "oneshot";
+          };
+      in listToAttrs (map formatDevice (filter (fs: fs.autoFormat) fileSystems)) // {
+    # Mount /sys/fs/pstore for evacuating panic logs and crashdumps from persistent storage onto the disk using systemd-pstore.
+    # This cannot be done with the other special filesystems because the pstore module (which creates the mount point) is not loaded then.
+        "mount-pstore" = {
+          serviceConfig = {
+            Type = "oneshot";
+            # skip on kernels without the pstore module
+            ExecCondition = "${pkgs.kmod}/bin/modprobe -b pstore";
+            ExecStart = pkgs.writeShellScript "mount-pstore.sh" ''
+              set -eu
+              # if the pstore module is builtin it will have mounted the persistent store automatically. it may also be already mounted for other reasons.
+              ${pkgs.util-linux}/bin/mountpoint -q /sys/fs/pstore || ${pkgs.util-linux}/bin/mount -t pstore -o nosuid,noexec,nodev pstore /sys/fs/pstore
+              # 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.
+              TRIES=15
+              while [ "$(cat /sys/module/pstore/parameters/backend)" = "(null)" ]; do
+                if (( $TRIES )); then
+                  sleep 0.1
+                  TRIES=$((TRIES-1))
+                else
+                  echo "Persistent Storage backend was not registered in time." >&2
+                  break
+                fi
+              done
+            '';
+            RemainAfterExit = true;
+          };
+          unitConfig = {
+            ConditionVirtualization = "!container";
+            DefaultDependencies = false; # needed to prevent a cycle
+          };
+          before = [ "systemd-pstore.service" ];
+          wantedBy = [ "systemd-pstore.service" ];
+        };
+      };
+
+    systemd.tmpfiles.rules = [
+      "d /run/keys 0750 root ${toString config.ids.gids.keys}"
+      "z /run/keys 0750 root ${toString config.ids.gids.keys}"
+    ];
+
+    # Sync mount options with systemd's src/core/mount-setup.c: mount_table.
+    boot.specialFileSystems = {
+      "/proc" = { fsType = "proc"; options = [ "nosuid" "noexec" "nodev" ]; };
+      "/run" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=755" "size=${config.boot.runSize}" ]; };
+      "/dev" = { fsType = "devtmpfs"; options = [ "nosuid" "strictatime" "mode=755" "size=${config.boot.devSize}" ]; };
+      "/dev/shm" = { fsType = "tmpfs"; options = [ "nosuid" "nodev" "strictatime" "mode=1777" "size=${config.boot.devShmSize}" ]; };
+      "/dev/pts" = { fsType = "devpts"; options = [ "nosuid" "noexec" "mode=620" "ptmxmode=0666" "gid=${toString config.ids.gids.tty}" ]; };
+
+      # To hold secrets that shouldn't be written to disk
+      "/run/keys" = { fsType = "ramfs"; options = [ "nosuid" "nodev" "mode=750" ]; };
+    } // optionalAttrs (!config.boot.isContainer) {
+      # systemd-nspawn populates /sys by itself, and remounting it causes all
+      # kinds of weird issues (most noticeably, waiting for host disk device
+      # nodes).
+      "/sys" = { fsType = "sysfs"; options = [ "nosuid" "noexec" "nodev" ]; };
+    };
+
+  };
+
+}