Files
Alexander 908c37f73f Add NixOS module for full agregators stack with VM test
Module manages PostgreSQL (both databases + schema init), qBittorrent
(VPN-confined via VPN-Confinement), Jackett, metadata-agregator,
musicfs, and the main orchestrator. All services are independently
enableable with auto-wired inter-service configuration.

Includes NixOS VM test validating PostgreSQL setup, schema
initialization, service startup, and directory creation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/claude-agent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-05-20 12:39:04 +02:00

143 lines
3.8 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.agregators;
mfsCfg = cfg.musicfs;
toml = pkgs.formats.toml { };
configFile = toml.generate "musicfs.toml" (
{
mount_point = mfsCfg.mountPoint;
cache_dir = mfsCfg.cacheDir;
origins = [
{
id = mfsCfg.originId;
origin_type = "local";
priority = 1;
enabled = true;
path = mfsCfg.originPath;
}
];
}
// lib.optionalAttrs (mfsCfg.cache != { }) { cache = mfsCfg.cache; }
// lib.optionalAttrs (mfsCfg.logging != { }) { logging = mfsCfg.logging; }
// mfsCfg.extraConfig
);
in
{
options.services.agregators.musicfs = {
enable = lib.mkEnableOption "musicfs virtual FUSE filesystem";
package = lib.mkOption {
type = lib.types.package;
description = "The musicfs package.";
};
port = lib.mkOption {
type = lib.types.port;
default = 50052;
description = "gRPC control API port.";
};
mountPoint = lib.mkOption {
type = lib.types.path;
default = "/mnt/music";
description = "Where to mount the virtual filesystem.";
};
cacheDir = lib.mkOption {
type = lib.types.path;
default = "${cfg.stateDir}/musicfs/cache";
defaultText = lib.literalExpression ''"''${cfg.stateDir}/musicfs/cache"'';
description = "Directory for cache data (CAS chunks, metadata, search index).";
};
originPath = lib.mkOption {
type = lib.types.path;
default = "${cfg.mediaDir}/downloads";
defaultText = lib.literalExpression ''"''${cfg.mediaDir}/downloads"'';
description = "Source directory for music files (typically qBittorrent download dir).";
};
originId = lib.mkOption {
type = lib.types.str;
default = "local-storage";
description = "Origin identifier used by musicfs and music-agregator.";
};
cache = lib.mkOption {
type = lib.types.attrsOf toml.type;
default = { };
description = "Cache settings passed to [cache] in config.";
};
logging = lib.mkOption {
type = lib.types.attrsOf toml.type;
default = { };
description = "Logging settings passed to [logging] in config.";
};
extraConfig = lib.mkOption {
type = lib.types.attrsOf toml.type;
default = { };
description = "Additional top-level config keys.";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open the gRPC port in the firewall.";
};
};
config = lib.mkIf (cfg.enable && mfsCfg.enable) {
programs.fuse.userAllowOther = true;
systemd.tmpfiles.rules = [
"d '${mfsCfg.mountPoint}' 0755 ${cfg.user} ${cfg.group} - -"
"d '${mfsCfg.cacheDir}' 0750 ${cfg.user} ${cfg.group} - -"
];
systemd.services.musicfs = {
description = "MusicFS - Virtual FUSE Filesystem for Music";
after = [
"network.target"
"local-fs.target"
];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
Type = "notify";
ExecStart = "${lib.getExe mfsCfg.package} mount --config ${configFile} --grpc-port ${toString mfsCfg.port}";
ExecStopPost = "${pkgs.fuse3}/bin/fusermount3 -u ${mfsCfg.mountPoint}";
User = cfg.user;
Group = cfg.group;
Restart = "on-failure";
RestartSec = 5;
ProtectSystem = "strict";
ReadWritePaths = [
mfsCfg.mountPoint
mfsCfg.cacheDir
mfsCfg.originPath
];
PrivateTmp = true;
NoNewPrivileges = true;
ProtectHome = "read-only";
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
DeviceAllow = [ "/dev/fuse rw" ];
};
};
networking.firewall.allowedTCPPorts = lib.mkIf mfsCfg.openFirewall [ mfsCfg.port ];
};
}