From 9987f275085f41bf5f9b7c252418499d724d4a47 Mon Sep 17 00:00:00 2001 From: Ethan Girouard Date: Thu, 30 Jul 2026 18:13:19 -0400 Subject: [PATCH] Create module and flake --- flake.nix | 8 ++++ zfs-containers.nix | 98 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 flake.nix create mode 100644 zfs-containers.nix diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..c421ec4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,8 @@ +{ + description = "NixOS module to allow ZFS dataset use in systemd-nspawn containers"; + + outputs = { ... }: rec { + nixosModules.zfs-containers = import ./zfs-containers.nix; + nixosModules.default = nixosModules.zfs-containers; + }; +} diff --git a/zfs-containers.nix b/zfs-containers.nix new file mode 100644 index 0000000..fcbde26 --- /dev/null +++ b/zfs-containers.nix @@ -0,0 +1,98 @@ +{ config, pkgs, lib, ... }: +with lib; +let + cfg = config.containers; + + concatMapLines = concatMapStringsSep "\n"; + + wait_cmds = concatMapLines + (dataset: ''until zfs list "${dataset}"; do echo "Waiting for dataset \"${dataset}\"..."; sleep 0.5; done''); + + zoned_cmds = concatMapLines (dataset: ''zfs set zoned=on "${dataset}"''); + delegate_cmds = concatMapLines (dataset: ''zfs zone /proc/"$LEADER_PID"/ns/user "${dataset}"''); +in +{ + options.containers = mkOption { + type = types.attrsOf (types.submodule ( + { config, ... }: + { + options.zfs_datasets = mkOption { + description = "ZFS datasets to mount into the container"; + type = types.listOf types.str; + default = []; + }; + + config = lib.mkIf (length config.zfs_datasets >= 1) { + allowedDevices = [ + { + node = "/dev/zfs"; + modifier = "rw"; + } + ]; + + bindMounts."/dev/zfs" = { + hostPath = "/dev/zfs"; + isReadOnly = false; + }; + + # This makes systemd-nspawn immediately notify the parent service that the container is + # ready without waiting for its services to start. ExecStartPost is run when this ready + # signal is received. Turning this off creates a deadlock where the container's + # `zfs-datasets` service needs the `ExecStartPost` script to run, but that script won't + # run until the service exits + extraFlags = [ "--notify-ready=no" ]; + + config = { pkgs, ... }: { + systemd.targets."zfs" = { + wantedBy = [ "multi-user.target" ]; + }; + + systemd.services."zfs-datasets" = { + description = "Waits for container ZFS datasets to become available"; + + path = with pkgs; [ + zfs + ]; + + script = '' + ${wait_cmds config.zfs_datasets} + echo "All ZFS datasets available" + ''; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + TimeoutStartSec = 60 * 5; + }; + + wantedBy = [ "zfs.target" ]; + }; + }; + }; + } + )); + }; + + config.systemd.services = mapAttrs' (name: config: + { + name = "container@${name}"; + + value = lib.mkIf (length config.zfs_datasets >= 1) { + path = with pkgs; [ + zfs + systemd + ]; + + preStart = mkBefore '' + ${zoned_cmds config.zfs_datasets} + echo "Enabled zoned setting on zfs datasets" + ''; + + postStart = mkBefore '' + LEADER_PID=$(machinectl show -P Leader ${name}) + ${delegate_cmds config.zfs_datasets} + echo "Delegated zfs dataset to namespace of PID $LEADER_PID" + ''; + }; + }) cfg; +}