Compare commits

..

No commits in common. "ae5189b6c6166ce524d83ccda7d5106542e50021" and "86753581f1e64495dbe3fc4807ca54bb49e6bc34" have entirely different histories.

14 changed files with 92 additions and 185 deletions

View File

@ -39,8 +39,6 @@
./services/bitwarden.nix ./services/bitwarden.nix
./services/monero.nix
# KEEP UNTIL 2028 # KEEP UNTIL 2028
./services/caddy_senior_project.nix ./services/caddy_senior_project.nix
]; ];

View File

@ -155,10 +155,6 @@
vaultwarden = { vaultwarden = {
path = "/var/lib/vaultwarden"; path = "/var/lib/vaultwarden";
}; };
monero = {
dataDir = "/services/monero";
};
}; };
pkgs = import nixpkgs { pkgs = import nixpkgs {

126
lib.nix
View File

@ -9,6 +9,28 @@ inputs.nixpkgs.lib.extend (
lib = prev; lib = prev;
in in
{ {
serviceMountDeps =
serviceName: dirs:
{ pkgs, ... }:
{
systemd.services."${serviceName}_mounts" = {
wants = [ "zfs.target" ];
before = [ "${serviceName}.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = "${lib.getExe pkgs.ensureZfsMounts} ${lib.strings.concatStringsSep " " dirs}";
};
};
systemd.services.${serviceName} = {
wants = [ "${serviceName}_mounts.service" ];
after = [ "${serviceName}_mounts.service" ];
requires = [ "${serviceName}_mounts.service" ];
};
};
# stolen from: https://stackoverflow.com/a/42398526 # stolen from: https://stackoverflow.com/a/42398526
optimizeWithFlags = optimizeWithFlags =
pkg: flags: pkg: flags:
@ -57,101 +79,25 @@ inputs.nixpkgs.lib.extend (
}; };
}; };
serviceMountWithZpool = serviceDependZpool =
serviceName: zpool: dirs: serviceName: zpool:
{ pkgs, config, ... }: { config, ... }:
{ {
systemd.services."${serviceName}-mounts" = { config = lib.mkIf (zpool != "") {
wants = [ "zfs.target" ] ++ lib.optionals (zpool != "") [ "zfs-import-${zpool}.service" ]; systemd.services.${serviceName} = {
after = lib.optionals (zpool != "") [ "zfs-import-${zpool}.service" ]; wants = [ "zfs-import-${zpool}.service" ];
before = [ "${serviceName}.service" ]; after = [ "zfs-import-${zpool}.service" ];
requires = [ "zfs-import-${zpool}.service" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
ExecStart = lib.getExe (
pkgs.writeShellApplication {
name = "ensure-zfs-mounts-with-pool-${serviceName}";
runtimeInputs = with pkgs; [
gawk
coreutils
config.boot.zfs.package
];
text = ''
set -euo pipefail
echo "Ensuring ZFS mounts for service: ${serviceName}"
echo "Directories: ${lib.strings.concatStringsSep ", " dirs}"
# Validate mounts exist (ensureZfsMounts already has proper PATH)
${lib.getExe pkgs.ensureZfsMounts} ${lib.strings.concatStringsSep " " dirs}
# Additional runtime check: verify paths are on correct zpool
${lib.optionalString (zpool != "") ''
echo "Verifying ZFS mountpoints are on pool '${zpool}'..."
if ! zfs_list_output=$(zfs list -H -o name,mountpoint 2>&1); then
echo "ERROR: Failed to query ZFS datasets: $zfs_list_output" >&2
exit 1
fi
# This loop handles variable number of directories, shellcheck false positive
# shellcheck disable=SC2043
for target in ${lib.strings.concatStringsSep " " dirs}; do
echo "Checking: $target"
# Find dataset that has this mountpoint
dataset=$(echo "$zfs_list_output" | awk -v target="$target" '$2 == target {print $1; exit}')
if [ -z "$dataset" ]; then
echo "ERROR: No ZFS dataset found for mountpoint: $target" >&2
exit 1
fi
# Extract pool name from dataset (first part before /)
actual_pool=$(echo "$dataset" | cut -d'/' -f1)
if [ "$actual_pool" != "${zpool}" ]; then
echo "ERROR: ZFS pool mismatch for $target" >&2
echo " Expected pool: ${zpool}" >&2
echo " Actual pool: $actual_pool" >&2
echo " Dataset: $dataset" >&2
exit 1
fi
echo "$target is on $dataset (pool: $actual_pool)"
done
echo "All paths verified successfully on pool '${zpool}'"
''}
echo "Mount validation completed for ${serviceName}"
'';
}
);
}; };
};
systemd.services.${serviceName} = { # assert that the pool is even enabled
wants = [ assertions = [
"${serviceName}-mounts.service" {
]; assertion = builtins.elem zpool config.boot.zfs.extraPools;
after = [ message = "${zpool} is not enabled in `boot.zfs.extraPools`";
"${serviceName}-mounts.service" }
];
requires = [
"${serviceName}-mounts.service"
]; ];
}; };
# assert that the pool is even enabled
#assertions = lib.optionals (zpool != "") [
# {
# assertion = builtins.elem zpool config.boot.zfs.extraPools;
# message = "${zpool} is not enabled in `boot.zfs.extraPools`";
# }
#];
}; };
} }
) )

View File

@ -7,14 +7,16 @@
}: }:
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "vaultwarden" service_configs.zpool_ssds [ (lib.serviceMountDeps "vaultwarden" [
service_configs.vaultwarden.path service_configs.vaultwarden.path
config.services.vaultwarden.backupDir config.services.vaultwarden.backupDir
]) ])
(lib.serviceMountWithZpool "backup-vaultwarden" service_configs.zpool_ssds [ (lib.serviceMountDeps "backup-vaultwarden" [
service_configs.vaultwarden.path service_configs.vaultwarden.path
config.services.vaultwarden.backupDir config.services.vaultwarden.backupDir
]) ])
(lib.serviceDependZpool "vaultwarden" service_configs.zpool_ssds)
(lib.serviceDependZpool "backup-vaultwarden" service_configs.zpool_ssds)
]; ];
services.vaultwarden = { services.vaultwarden = {

View File

@ -44,9 +44,10 @@ let
in in
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "caddy" service_configs.zpool_ssds [ (lib.serviceMountDeps "caddy" [
config.services.caddy.dataDir config.services.caddy.dataDir
]) ])
(lib.serviceDependZpool "caddy" service_configs.zpool_ssds)
]; ];
services.caddy = { services.caddy = {

View File

@ -7,7 +7,8 @@
}: }:
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "gitea" service_configs.zpool_ssds [ config.services.gitea.stateDir ]) (lib.serviceMountDeps "gitea" [ config.services.gitea.stateDir ])
(lib.serviceDependZpool "gitea" service_configs.zpool_ssds)
]; ];
services.gitea = { services.gitea = {

View File

@ -7,12 +7,10 @@
}: }:
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "immich-server" service_configs.zpool_ssds [ (lib.serviceMountDeps "immich-server" [ config.services.immich.mediaLocation ])
config.services.immich.mediaLocation (lib.serviceMountDeps "immich-machine-learning" [ config.services.immich.mediaLocation ])
]) (lib.serviceDependZpool "immich-server" service_configs.zpool_ssds)
(lib.serviceMountWithZpool "immich-machine-learning" service_configs.zpool_ssds [ (lib.serviceDependZpool "immich-machine-learning" service_configs.zpool_ssds)
config.services.immich.mediaLocation
])
]; ];
services.immich = { services.immich = {

View File

@ -7,10 +7,11 @@
}: }:
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "jellyfin" service_configs.zpool_ssds [ (lib.serviceMountDeps "jellyfin" [
config.services.jellyfin.dataDir config.services.jellyfin.dataDir
config.services.jellyfin.cacheDir config.services.jellyfin.cacheDir
]) ])
(lib.serviceDependZpool "jellyfin" service_configs.zpool_ssds)
]; ];
services.jellyfin = { services.jellyfin = {

View File

@ -7,12 +7,10 @@
}: }:
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "minecraft-server-${service_configs.minecraft.server_name}" (lib.serviceMountDeps "minecraft-server-${service_configs.minecraft.server_name}" [
service_configs.zpool_ssds "${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}"
[ ])
"${service_configs.minecraft.parent_dir}/${service_configs.minecraft.server_name}" (lib.serviceDependZpool "minecraft-server-${service_configs.minecraft.server_name}" service_configs.zpool_ssds)
]
)
]; ];
environment.systemPackages = [ environment.systemPackages = [

View File

@ -1,24 +0,0 @@
{
service_configs,
lib,
...
}:
{
imports = [
(lib.serviceMountWithZpool "monero" service_configs.zpool_hdds [
service_configs.monero.dataDir
])
];
services.monero = {
enable = true;
dataDir = service_configs.monero.dataDir;
rpc = {
restricted = true;
};
};
systemd.tmpfiles.rules = [
"Z ${service_configs.monero.dataDir} 0700 monero monero"
];
}

View File

@ -7,9 +7,8 @@
}: }:
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "postgresql" service_configs.zpool_ssds [ (lib.serviceMountDeps "postgresql" [ config.services.postgresql.dataDir ])
config.services.postgresql.dataDir (lib.serviceDependZpool "postgresql" service_configs.zpool_ssds)
])
]; ];
services.postgresql = { services.postgresql = {

View File

@ -8,12 +8,13 @@
}: }:
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "qbittorrent" service_configs.zpool_hdds [ (lib.serviceMountDeps "qbittorrent" [
service_configs.torrents_path service_configs.torrents_path
config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath config.services.qbittorrent.serverConfig.Preferences.Downloads.TempPath
"${config.services.qbittorrent.profileDir}/qBittorrent" "${config.services.qbittorrent.profileDir}/qBittorrent"
]) ])
(lib.vpnNamespaceOpenPort config.services.qbittorrent.webuiPort "qbittorrent") (lib.vpnNamespaceOpenPort config.services.qbittorrent.webuiPort "qbittorrent")
(lib.serviceDependZpool "qbittorrent" service_configs.zpool_hdds)
]; ];
services.qbittorrent = { services.qbittorrent = {

View File

@ -11,11 +11,13 @@ let
in in
{ {
imports = [ imports = [
(lib.serviceMountWithZpool "slskd" "" [ (lib.serviceMountDeps "slskd" [
service_configs.slskd.base service_configs.slskd.base
service_configs.slskd.downloads service_configs.slskd.downloads
service_configs.slskd.incomplete service_configs.slskd.incomplete
]) ])
(lib.serviceDependZpool "slskd" service_configs.zpool_ssds)
(lib.serviceDependZpool "slskd" service_configs.zpool_hdds)
]; ];
users.groups."music" = { }; users.groups."music" = { };

View File

@ -7,29 +7,29 @@
}: }:
let let
# Create pkgs with ensureZfsMounts overlay # Create pkgs with ensureZfsMounts overlay
testPkgs = pkgs.appendOverlays [ (import ../overlays.nix) ]; testPkgs = import inputs.nixpkgs {
system = pkgs.system;
overlays = [ (import ../overlays.nix) ];
};
in in
testPkgs.testers.runNixOSTest { testPkgs.testers.runNixOSTest {
name = "zfs test"; name = "zfs folder dependency and mounting test";
nodes.machine = nodes.machine =
{ pkgs, ... }: { pkgs, ... }:
{ {
imports = [ imports = [
# Test valid paths within zpool (lib.serviceMountDeps "foobar" [ "/mnt/foobar_data" ])
(lib.serviceMountWithZpool "test-service" "rpool" [ "/mnt/rpool_data" ]) (lib.serviceMountDeps "foobarSadge" [
"/mnt/foobar_data"
"/mnt/does_not_exist_lol"
])
# Test service with paths outside zpool (should fail assertion)
(lib.serviceMountWithZpool "invalid-service" "rpool2" [ "/mnt/rpool_data" ])
]; ];
virtualisation = { virtualisation = {
emptyDiskImages = [ emptyDiskImages = [
4096 4096
4096
]; ];
# Add this to avoid ZFS hanging issues
additionalPaths = [ pkgs.zfs ];
}; };
networking.hostId = "deadbeef"; networking.hostId = "deadbeef";
boot.kernelPackages = config.boot.kernelPackages; boot.kernelPackages = config.boot.kernelPackages;
@ -41,7 +41,7 @@ testPkgs.testers.runNixOSTest {
ensureZfsMounts ensureZfsMounts
]; ];
systemd.services."test-service" = { systemd.services.foobar = {
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
@ -49,7 +49,7 @@ testPkgs.testers.runNixOSTest {
}; };
}; };
systemd.services."invalid-service" = { systemd.services.foobarSadge = {
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
RemainAfterExit = true; RemainAfterExit = true;
@ -61,50 +61,38 @@ testPkgs.testers.runNixOSTest {
testScript = '' testScript = ''
start_all() start_all()
machine.wait_for_unit("multi-user.target") machine.wait_for_unit("multi-user.target")
# Setup ZFS pool
machine.succeed( machine.succeed(
"parted --script /dev/vdb mklabel msdos", "parted --script /dev/vdb mklabel msdos",
"parted --script /dev/vdb -- mkpart primary 1024M -1s", "parted --script /dev/vdb -- mkpart primary 1024M -1s",
"zpool create rpool /dev/vdb1"
) )
# Setup ZFS pool 2 machine.fail("zfsEnsureMounted")
machine.succeed( machine.fail("zfsEnsureMounted /mnt/test_mountpoint")
"parted --script /dev/vdc mklabel msdos",
"parted --script /dev/vdc -- mkpart primary 1024M -1s",
"zpool create rpool2 /dev/vdc1"
)
machine.succeed("zfs create -o mountpoint=/mnt/rpool_data rpool/data") machine.succeed("zpool create rpool /dev/vdb1")
machine.succeed("zfs create -o mountpoint=/mnt/test_mountpoint rpool/test")
machine.succeed("zfs create -o mountpoint=/mnt/rpool2_data rpool2/data") machine.succeed("zfsEnsureMounted /mnt/test_mountpoint")
# Test that valid service starts successfully machine.fail("zfsEnsureMounted /mnt/does_not_exist_lol")
machine.succeed("systemctl start test-service") machine.fail("zfsEnsureMounted /mnt/test_mountpoint /mnt/does_not_exist_lol")
# Manually test our validation logic by checking the debug output machine.succeed("zfs create -o mountpoint=/mnt/test_mountpoint_dos rpool/test2")
zfs_output = machine.succeed("zfs list -H -o name,mountpoint")
print("ZFS LIST OUTPUT:")
print(zfs_output)
dataset = machine.succeed("zfs list -H -o name,mountpoint | awk '/\\/mnt\\/rpool_data/ { print $1 }'") machine.succeed("zfsEnsureMounted /mnt/test_mountpoint /mnt/test_mountpoint_dos")
print("DATASET FOR /mnt/rpool_data:")
print(dataset)
# Test that invalid-service mount service fails validation machine.succeed("zfs create -o mountpoint='/mnt/test path with spaces' rpool/test3")
machine.fail("systemctl start invalid-service.service")
# Check the journal for our detailed validation error message machine.succeed("zfsEnsureMounted '/mnt/test path with spaces'")
journal_output = machine.succeed("journalctl -u invalid-service-mounts.service --no-pager")
print("JOURNAL OUTPUT:")
print(journal_output)
# Verify our validation error is in the journal using Python string matching machine.succeed("zfs create -o mountpoint='/mnt/test escaped spaces' rpool/test4")
assert "ERROR: ZFS pool mismatch for /mnt/rpool_data" in journal_output machine.succeed("zfsEnsureMounted /mnt/test\ escaped\ spaces")
assert "Expected pool: rpool2" in journal_output
assert "Actual pool: rpool" in journal_output
print("SUCCESS: Runtime validation correctly detected zpool mismatch!") machine.succeed("zfsEnsureMounted /mnt/test_mountpoint '/mnt/test path with spaces' /mnt/test_mountpoint_dos")
machine.succeed("zfs create -o mountpoint=/mnt/foobar_data rpool/foobar")
machine.succeed("systemctl start foobar")
machine.fail("systemctl start foobarSadge")
''; '';
} }