Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nixos/doc/manual/configuration/file-systems.chapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
27 changes: 27 additions & 0 deletions nixos/doc/manual/configuration/overlayfs.section.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions nixos/doc/manual/release-notes/rl-2405.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,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.).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1519,6 +1519,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
Expand Down
144 changes: 144 additions & 0 deletions nixos/modules/tasks/filesystems/overlayfs.nix
Original file line number Diff line number Diff line change
@@ -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);

};

}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -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; };
Expand Down
89 changes: 89 additions & 0 deletions nixos/tests/filesystems-overlayfs.nix
Original file line number Diff line number Diff line change
@@ -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")
'';

}