about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorLinus Heckemann <git@sphalerite.org>2024-02-09 08:22:09 +0100
committerGitHub <noreply@github.com>2024-02-09 08:22:09 +0100
commit4916e228123744637bf89906a210b86a37251226 (patch)
tree4bb7d7e03642037329f483e4bf42bad1955f9c84 /nixos
parent251c69b73037f0a1b961c6216cb45872615f0a2a (diff)
parentbbce103ca8d450a9278dcf494657e03558c6d8ab (diff)
Merge pull request #286176 from nikstur/writable-overlays
nixos/filesystems: init overlayfs
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/configuration/file-systems.chapter.md1
-rw-r--r--nixos/doc/manual/configuration/overlayfs.section.md27
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md5
-rw-r--r--nixos/modules/module-list.nix1
-rw-r--r--nixos/modules/tasks/filesystems/overlayfs.nix144
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/filesystems-overlayfs.nix89
7 files changed, 268 insertions, 0 deletions
diff --git a/nixos/doc/manual/configuration/file-systems.chapter.md b/nixos/doc/manual/configuration/file-systems.chapter.md
index aca978be064d3..3dfdd20ac33ec 100644
--- a/nixos/doc/manual/configuration/file-systems.chapter.md
+++ b/nixos/doc/manual/configuration/file-systems.chapter.md
@@ -39,4 +39,5 @@ and non-critical by adding `options = [ "nofail" ];`.
 ```{=include=} sections
 luks-file-systems.section.md
 sshfs-file-systems.section.md
+overlayfs.section.md
 ```
diff --git a/nixos/doc/manual/configuration/overlayfs.section.md b/nixos/doc/manual/configuration/overlayfs.section.md
new file mode 100644
index 0000000000000..592fb7c2e6f79
--- /dev/null
+++ b/nixos/doc/manual/configuration/overlayfs.section.md
@@ -0,0 +1,27 @@
+# Overlayfs {#sec-overlayfs}
+
+NixOS offers a convenient abstraction to create both read-only as well writable
+overlays.
+
+```nix
+fileSystems = {
+  "/writable-overlay" = {
+    overlay = {
+      lowerdir = [ writableOverlayLowerdir ];
+      upperdir = "/.rw-writable-overlay/upper";
+      workdir = "/.rw-writable-overlay/work";
+    };
+    # Mount the writable overlay in the initrd.
+    neededForBoot = true;
+  };
+  "/readonly-overlay".overlay.lowerdir = [
+    writableOverlayLowerdir
+    writableOverlayLowerdir2
+  ];
+};
+```
+
+If `upperdir` and `workdir` are not null, they will be created before the
+overlay is mounted.
+
+To mount an overlay as read-only, you need to provide at least two `lowerdir`s.
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index bad1fd449bbb9..d3efc1789cdc4 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -272,6 +272,11 @@ The pre-existing [services.ankisyncd](#opt-services.ankisyncd.enable) has been m
 - The option [`services.nextcloud.config.dbport`] of the Nextcloud module was removed to match upstream.
   The port can be specified in [`services.nextcloud.config.dbhost`](#opt-services.nextcloud.config.dbhost).
 
+- A new abstraction to create both read-only as well as writable overlay file
+  systems was added. Available via
+  [fileSystems.overlay](#opt-fileSystems._name_.overlay.lowerdir). See also the
+  [NixOS docs](#sec-overlayfs).
+
 - `stdenv`: The `--replace` flag in `substitute`, `substituteInPlace`, `substituteAll`, `substituteAllStream`, and `substituteStream` is now deprecated if favor of the new `--replace-fail`, `--replace-warn` and `--replace-quiet`. The deprecated `--replace` equates to `--replace-warn`.
 
 - New options were added to the dnsdist module to enable and configure a DNSCrypt endpoint (see `services.dnsdist.dnscrypt.enable`, etc.).
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 1384c7c6dfa22..72b6d42591da5 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -1527,6 +1527,7 @@
   ./tasks/filesystems/jfs.nix
   ./tasks/filesystems/nfs.nix
   ./tasks/filesystems/ntfs.nix
+  ./tasks/filesystems/overlayfs.nix
   ./tasks/filesystems/reiserfs.nix
   ./tasks/filesystems/sshfs.nix
   ./tasks/filesystems/squashfs.nix
diff --git a/nixos/modules/tasks/filesystems/overlayfs.nix b/nixos/modules/tasks/filesystems/overlayfs.nix
new file mode 100644
index 0000000000000..e71ef9ba62e9c
--- /dev/null
+++ b/nixos/modules/tasks/filesystems/overlayfs.nix
@@ -0,0 +1,144 @@
+{ config, lib, pkgs, utils, ... }:
+
+let
+  # The scripted initrd contains some magic to add the prefix to the
+  # paths just in time, so we don't add it here.
+  sysrootPrefix = fs:
+    if config.boot.initrd.systemd.enable && (utils.fsNeededForBoot fs) then
+      "/sysroot"
+    else
+      "";
+
+  # Returns a service that creates the required directories before the mount is
+  # created.
+  preMountService = _name: fs:
+    let
+      prefix = sysrootPrefix fs;
+
+      escapedMountpoint = utils.escapeSystemdPath (prefix + fs.mountPoint);
+      mountUnit = "${escapedMountpoint}.mount";
+
+      upperdir = prefix + fs.overlay.upperdir;
+      workdir = prefix + fs.overlay.workdir;
+    in
+    lib.mkIf (fs.overlay.upperdir != null)
+      {
+        "rw-${escapedMountpoint}" = {
+          requiredBy = [ mountUnit ];
+          before = [ mountUnit ];
+          unitConfig = {
+            DefaultDependencies = false;
+            RequiresMountsFor = "${upperdir} ${workdir}";
+          };
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${pkgs.coreutils}/bin/mkdir -p -m 0755 ${upperdir} ${workdir}";
+          };
+        };
+      };
+
+  overlayOpts = { config, ... }: {
+
+    options.overlay = {
+
+      lowerdir = lib.mkOption {
+        type = with lib.types; nullOr (nonEmptyListOf (either str pathInStore));
+        default = null;
+        description = lib.mdDoc ''
+          The list of path(s) to the lowerdir(s).
+
+          To create a writable overlay, you MUST provide an upperdir and a
+          workdir.
+
+          You can create a read-only overlay when you provide multiple (at
+          least 2!) lowerdirs and neither an upperdir nor a workdir.
+        '';
+      };
+
+      upperdir = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = lib.mdDoc ''
+          The path to the upperdir.
+
+          If this is null, a read-only overlay is created using the lowerdir.
+
+          If you set this to some value you MUST also set `workdir`.
+        '';
+      };
+
+      workdir = lib.mkOption {
+        type = lib.types.nullOr lib.types.str;
+        default = null;
+        description = lib.mdDoc ''
+          The path to the workdir.
+
+          This MUST be set if you set `upperdir`.
+        '';
+      };
+
+    };
+
+    config = lib.mkIf (config.overlay.lowerdir != null) {
+      fsType = "overlay";
+      device = lib.mkDefault "overlay";
+
+      options =
+        let
+          prefix = sysrootPrefix config;
+
+          lowerdir = map (s: prefix + s) config.overlay.lowerdir;
+          upperdir = prefix + config.overlay.upperdir;
+          workdir = prefix + config.overlay.workdir;
+        in
+        [
+          "lowerdir=${lib.concatStringsSep ":" lowerdir}"
+        ] ++ lib.optionals (config.overlay.upperdir != null) [
+          "upperdir=${upperdir}"
+          "workdir=${workdir}"
+        ] ++ (map (s: "x-systemd.requires-mounts-for=${s}") lowerdir);
+    };
+
+  };
+in
+
+{
+
+  options = {
+
+    # Merge the overlay options into the fileSystems option.
+    fileSystems = lib.mkOption {
+      type = lib.types.attrsOf (lib.types.submodule [ overlayOpts ]);
+    };
+
+  };
+
+  config =
+    let
+      overlayFileSystems = lib.filterAttrs (_name: fs: (fs.overlay.lowerdir != null)) config.fileSystems;
+      initrdFileSystems = lib.filterAttrs (_name: utils.fsNeededForBoot) overlayFileSystems;
+      userspaceFileSystems = lib.filterAttrs (_name: fs: (!utils.fsNeededForBoot fs)) overlayFileSystems;
+    in
+    {
+
+      boot.initrd.availableKernelModules = lib.mkIf (initrdFileSystems != { }) [ "overlay" ];
+
+      assertions = lib.concatLists (lib.mapAttrsToList
+        (_name: fs: [
+          {
+            assertion = (fs.overlay.upperdir == null) == (fs.overlay.workdir == null);
+            message = "You cannot define a `lowerdir` without a `workdir` and vice versa for mount point: ${fs.mountPoint}";
+          }
+          {
+            assertion = (fs.overlay.lowerdir != null && fs.overlay.upperdir == null) -> (lib.length fs.overlay.lowerdir) >= 2;
+            message = "A read-only overlay (without an `upperdir`) requires at least 2 `lowerdir`s: ${fs.mountPoint}";
+          }
+        ])
+        config.fileSystems);
+
+      boot.initrd.systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService initrdFileSystems);
+      systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService userspaceFileSystems);
+
+    };
+
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index c2114098fe059..31af6ec642141 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -301,6 +301,7 @@ in {
   fenics = handleTest ./fenics.nix {};
   ferm = handleTest ./ferm.nix {};
   ferretdb = handleTest ./ferretdb.nix {};
+  filesystems-overlayfs = runTest ./filesystems-overlayfs.nix;
   firefox = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox; };
   firefox-beta = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-beta; };
   firefox-devedition = handleTest ./firefox.nix { firefoxPackage = pkgs.firefox-devedition; };
diff --git a/nixos/tests/filesystems-overlayfs.nix b/nixos/tests/filesystems-overlayfs.nix
new file mode 100644
index 0000000000000..d7cbf640abe49
--- /dev/null
+++ b/nixos/tests/filesystems-overlayfs.nix
@@ -0,0 +1,89 @@
+{ lib, pkgs, ... }:
+
+let
+  initrdLowerdir = pkgs.runCommand "initrd-lowerdir" { } ''
+    mkdir -p $out
+    echo "initrd" > $out/initrd.txt
+  '';
+  initrdLowerdir2 = pkgs.runCommand "initrd-lowerdir-2" { } ''
+    mkdir -p $out
+    echo "initrd2" > $out/initrd2.txt
+  '';
+  userspaceLowerdir = pkgs.runCommand "userspace-lowerdir" { } ''
+    mkdir -p $out
+    echo "userspace" > $out/userspace.txt
+  '';
+  userspaceLowerdir2 = pkgs.runCommand "userspace-lowerdir-2" { } ''
+    mkdir -p $out
+    echo "userspace2" > $out/userspace2.txt
+  '';
+in
+{
+
+  name = "writable-overlays";
+
+  meta.maintainers = with lib.maintainers; [ nikstur ];
+
+  nodes.machine = { config, pkgs, ... }: {
+    boot.initrd.systemd.enable = true;
+    boot.initrd.availableKernelModules = [ "overlay" ];
+
+    virtualisation.fileSystems = {
+      "/initrd-overlay" = {
+        overlay = {
+          lowerdir = [ initrdLowerdir ];
+          upperdir = "/.rw-initrd-overlay/upper";
+          workdir = "/.rw-initrd-overlay/work";
+        };
+        neededForBoot = true;
+      };
+      "/userspace-overlay" = {
+        overlay = {
+          lowerdir = [ userspaceLowerdir ];
+          upperdir = "/.rw-userspace-overlay/upper";
+          workdir = "/.rw-userspace-overlay/work";
+        };
+      };
+      "/ro-initrd-overlay" = {
+        overlay.lowerdir = [
+          initrdLowerdir
+          initrdLowerdir2
+        ];
+        neededForBoot = true;
+      };
+      "/ro-userspace-overlay" = {
+        overlay.lowerdir = [
+          userspaceLowerdir
+          userspaceLowerdir2
+        ];
+      };
+    };
+  };
+
+  testScript = ''
+    machine.wait_for_unit("default.target")
+
+    with subtest("Initrd overlay"):
+      machine.wait_for_file("/initrd-overlay/initrd.txt", 5)
+      machine.succeed("touch /initrd-overlay/writable.txt")
+      machine.succeed("findmnt --kernel --types overlay /initrd-overlay")
+
+    with subtest("Userspace overlay"):
+      machine.wait_for_file("/userspace-overlay/userspace.txt", 5)
+      machine.succeed("touch /userspace-overlay/writable.txt")
+      machine.succeed("findmnt --kernel --types overlay /userspace-overlay")
+
+    with subtest("Read only initrd overlay"):
+      machine.wait_for_file("/ro-initrd-overlay/initrd.txt", 5)
+      machine.wait_for_file("/ro-initrd-overlay/initrd2.txt", 5)
+      machine.fail("touch /ro-initrd-overlay/not-writable.txt")
+      machine.succeed("findmnt --kernel --types overlay /ro-initrd-overlay")
+
+    with subtest("Read only userspace overlay"):
+      machine.wait_for_file("/ro-userspace-overlay/userspace.txt", 5)
+      machine.wait_for_file("/ro-userspace-overlay/userspace2.txt", 5)
+      machine.fail("touch /ro-userspace-overlay/not-writable.txt")
+      machine.succeed("findmnt --kernel --types overlay /ro-userspace-overlay")
+  '';
+
+}