db is now optional for sonarr, radarr, and prowlarr.

This commit is contained in:
Joseph Hanson 2025-02-19 15:33:09 -06:00
parent 18274be266
commit a7e673ac69
Signed by: jahanson
SSH key fingerprint: SHA256:vy6dKBECV522aPAwklFM3ReKAVB086rT3oWwiuiFG7o
4 changed files with 408 additions and 372 deletions

View file

@ -101,6 +101,7 @@ in {
# System packages # System packages
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
# Hyprland
libva-utils # to view graphics capabilities libva-utils # to view graphics capabilities
greetd.tuigreet greetd.tuigreet
rofi-wayland rofi-wayland
@ -114,6 +115,8 @@ in {
wl-clipboard wl-clipboard
wlogout wlogout
wlr-randr wlr-randr
# dev
uv
# fun # fun
fastfetch fastfetch
# Scripts # Scripts
@ -169,6 +172,8 @@ in {
9001 # api interface 9001 # api interface
# Beszel-agent # Beszel-agent
45876 45876
# scrypted
45005
]; ];
}; };
@ -265,13 +270,13 @@ in {
openFirewall = true; openFirewall = true;
hardening = true; hardening = true;
apiKeyFile = config.sops.secrets."arr/prowlarr/apiKey".path; apiKeyFile = config.sops.secrets."arr/prowlarr/apiKey".path;
db = { # db = {
enable = true; # enable = true;
hostFile = config.sops.secrets."arr/prowlarr/postgres/host".path; # hostFile = config.sops.secrets."arr/prowlarr/postgres/host".path;
port = 5432; # port = 5432;
userFile = config.sops.secrets."arr/prowlarr/postgres/user".path; # userFile = config.sops.secrets."arr/prowlarr/postgres/user".path;
passwordFile = config.sops.secrets."arr/prowlarr/postgres/password".path; # passwordFile = config.sops.secrets."arr/prowlarr/postgres/password".path;
}; # };
}; };
# Radarr # Radarr
radarr = { radarr = {
@ -289,14 +294,14 @@ in {
openFirewall = true; openFirewall = true;
hardening = true; hardening = true;
apiKeyFile = config.sops.secrets."arr/radarr/1080p/apiKey".path; apiKeyFile = config.sops.secrets."arr/radarr/1080p/apiKey".path;
db = { # db = {
enable = true; # enable = true;
hostFile = config.sops.secrets."arr/radarr/1080p/postgres/host".path; # hostFile = config.sops.secrets."arr/radarr/1080p/postgres/host".path;
port = 5432; # port = 5432;
dbname = "radarr_main"; # dbname = "radarr_main";
userFile = config.sops.secrets."arr/radarr/1080p/postgres/user".path; # userFile = config.sops.secrets."arr/radarr/1080p/postgres/user".path;
passwordFile = config.sops.secrets."arr/radarr/1080p/postgres/password".path; # passwordFile = config.sops.secrets."arr/radarr/1080p/postgres/password".path;
}; # };
}; };
moviesAnime = { moviesAnime = {
enable = true; enable = true;
@ -310,14 +315,14 @@ in {
openFirewall = true; openFirewall = true;
hardening = true; hardening = true;
apiKeyFile = config.sops.secrets."arr/radarr/anime/apiKey".path; apiKeyFile = config.sops.secrets."arr/radarr/anime/apiKey".path;
db = { # db = {
enable = true; # enable = true;
hostFile = config.sops.secrets."arr/radarr/anime/postgres/host".path; # hostFile = config.sops.secrets."arr/radarr/anime/postgres/host".path;
port = 5432; # port = 5432;
dbname = "radarr_anime"; # dbname = "radarr_anime";
userFile = config.sops.secrets."arr/radarr/anime/postgres/user".path; # userFile = config.sops.secrets."arr/radarr/anime/postgres/user".path;
passwordFile = config.sops.secrets."arr/radarr/anime/postgres/password".path; # passwordFile = config.sops.secrets."arr/radarr/anime/postgres/password".path;
}; # };
}; };
}; };
}; };
@ -337,14 +342,14 @@ in {
openFirewall = true; openFirewall = true;
hardening = true; hardening = true;
apiKeyFile = config.sops.secrets."arr/sonarr/1080p/apiKey".path; apiKeyFile = config.sops.secrets."arr/sonarr/1080p/apiKey".path;
db = { # db = {
enable = true; # enable = true;
hostFile = config.sops.secrets."arr/sonarr/1080p/postgres/host".path; # hostFile = config.sops.secrets."arr/sonarr/1080p/postgres/host".path;
port = 5432; # port = 5432;
dbname = "sonarr_main"; # dbname = "sonarr_main";
userFile = config.sops.secrets."arr/sonarr/1080p/postgres/user".path; # userFile = config.sops.secrets."arr/sonarr/1080p/postgres/user".path;
passwordFile = config.sops.secrets."arr/sonarr/1080p/postgres/password".path; # passwordFile = config.sops.secrets."arr/sonarr/1080p/postgres/password".path;
}; # };
}; };
anime = { anime = {
enable = true; enable = true;
@ -358,14 +363,14 @@ in {
openFirewall = true; openFirewall = true;
hardening = true; hardening = true;
apiKeyFile = config.sops.secrets."arr/sonarr/anime/apiKey".path; apiKeyFile = config.sops.secrets."arr/sonarr/anime/apiKey".path;
db = { # db = {
enable = true; # enable = true;
hostFile = config.sops.secrets."arr/sonarr/anime/postgres/host".path; # hostFile = config.sops.secrets."arr/sonarr/anime/postgres/host".path;
port = 5432; # port = 5432;
dbname = "sonarr_anime"; # dbname = "sonarr_anime";
userFile = config.sops.secrets."arr/sonarr/anime/postgres/user".path; # userFile = config.sops.secrets."arr/sonarr/anime/postgres/user".path;
passwordFile = config.sops.secrets."arr/sonarr/anime/postgres/password".path; # passwordFile = config.sops.secrets."arr/sonarr/anime/postgres/password".path;
}; # };
}; };
}; };
}; };

View file

@ -5,8 +5,7 @@
utils, utils,
... ...
}: }:
with lib; with lib; let
let
cfg = config.mySystem.services.prowlarr; cfg = config.mySystem.services.prowlarr;
dbOptions = { dbOptions = {
options = { options = {
@ -51,12 +50,11 @@ let
}; };
}; };
}; };
in in {
{
options.mySystem.services.prowlarr = { options.mySystem.services.prowlarr = {
enable = mkEnableOption "Prowlarr"; enable = mkEnableOption "Prowlarr";
package = mkPackageOption pkgs "prowlarr" { }; package = mkPackageOption pkgs "prowlarr" {};
user = mkOption { user = mkOption {
type = types.str; type = types.str;
@ -109,7 +107,7 @@ in
extraEnvVars = mkOption { extraEnvVars = mkOption {
type = types.attrs; type = types.attrs;
default = { }; default = {};
example = { example = {
MY_VAR = "my value"; MY_VAR = "my value";
}; };
@ -125,6 +123,14 @@ in
db = mkOption { db = mkOption {
type = types.submodule dbOptions; type = types.submodule dbOptions;
default = {
enable = false;
host = "";
port = "5432";
user = "";
passwordFile = "";
dbname = "";
};
example = { example = {
enable = true; enable = true;
host = "10.5.0.5"; # or use hostFile host = "10.5.0.5"; # or use hostFile
@ -140,11 +146,11 @@ in
config = mkIf cfg.enable { config = mkIf cfg.enable {
assertions = [ assertions = [
{ {
assertion = !(cfg.db.host != "" && cfg.db.hostFile != ""); assertion = !(cfg.db.enable && (cfg.db.host != "" && cfg.db.hostFile != ""));
message = "Specify either a direct database host via db.host or a file via db.hostFile (leave direct host empty)."; message = "Specify either a direct database host via db.host or a file via db.hostFile (leave direct host empty).";
} }
{ {
assertion = !(cfg.db.user != "prowlarr" && cfg.db.userFile != ""); assertion = !(cfg.db.enable && (cfg.db.user != "prowlarr" && cfg.db.userFile != ""));
message = "Specify either a direct database user via db.user or a file via db.userFile."; message = "Specify either a direct database user via db.user or a file via db.userFile.";
} }
{ {
@ -159,7 +165,7 @@ in
"network.target" "network.target"
"nss-lookup.target" "nss-lookup.target"
]; ];
wantedBy = [ "multi-user.target" ]; wantedBy = ["multi-user.target"];
environment = lib.mkMerge [ environment = lib.mkMerge [
{ {
PROWLARR__APP__INSTANCENAME = "Prowlarr"; PROWLARR__APP__INSTANCENAME = "Prowlarr";
@ -171,7 +177,7 @@ in
PROWLARR__SERVER__PORT = toString cfg.port; PROWLARR__SERVER__PORT = toString cfg.port;
PROWLARR__UPDATE__BRANCH = "develop"; PROWLARR__UPDATE__BRANCH = "develop";
} }
(lib.mkIf cfg.db.enable { (lib.optionalAttrs cfg.db.enable {
PROWLARR__POSTGRES__PORT = toString cfg.db.port; PROWLARR__POSTGRES__PORT = toString cfg.db.port;
PROWLARR__POSTGRES__MAINDB = cfg.db.dbname; PROWLARR__POSTGRES__MAINDB = cfg.db.dbname;
}) })
@ -197,8 +203,8 @@ in
RestartSec = 5; RestartSec = 5;
} }
(lib.mkIf cfg.hardening { (lib.mkIf cfg.hardening {
CapabilityBoundingSet = [ "" ]; CapabilityBoundingSet = [""];
DeviceAllow = [ "" ]; DeviceAllow = [""];
DevicePolicy = "closed"; DevicePolicy = "closed";
LockPersonality = true; LockPersonality = true;
# Needs access to .Net CLR memory space. # Needs access to .Net CLR memory space.
@ -237,7 +243,7 @@ in
#"~@resources" #"~@resources"
]; ];
}) })
(lib.mkIf cfg.db.enable { {
ExecStartPre = "+${pkgs.writeShellScript "prowlarr-pre-script" '' ExecStartPre = "+${pkgs.writeShellScript "prowlarr-pre-script" ''
mkdir -p /run/prowlarr mkdir -p /run/prowlarr
rm -f /run/prowlarr/secrets.env rm -f /run/prowlarr/secrets.env
@ -258,10 +264,12 @@ in
write_var "PROWLARR__AUTH__APIKEY" "$(cat ${cfg.apiKeyFile})" write_var "PROWLARR__AUTH__APIKEY" "$(cat ${cfg.apiKeyFile})"
fi fi
# Database Configuration ${lib.optionalString cfg.db.enable ''
write_var "PROWLARR__POSTGRES__HOST" "$([ -n "${cfg.db.host}" ] && echo "${cfg.db.host}" || cat "${cfg.db.hostFile}")" # Database Configuration
write_var "PROWLARR__POSTGRES__USER" "$([ -n "${cfg.db.user}" ] && echo "${cfg.db.user}" || cat "${cfg.db.userFile}")" write_var "PROWLARR__POSTGRES__HOST" "$([ -n "${cfg.db.host}" ] && echo "${cfg.db.host}" || cat "${cfg.db.hostFile}")"
write_var "PROWLARR__POSTGRES__PASSWORD" "$(cat ${cfg.db.passwordFile})" write_var "PROWLARR__POSTGRES__USER" "$([ -n "${cfg.db.user}" ] && echo "${cfg.db.user}" || cat "${cfg.db.userFile}")"
write_var "PROWLARR__POSTGRES__PASSWORD" "$(cat ${cfg.db.passwordFile})"
''}
# Final permissions # Final permissions
chmod 600 /run/prowlarr/secrets.env chmod 600 /run/prowlarr/secrets.env
@ -269,18 +277,18 @@ in
''}"; ''}";
EnvironmentFile = ( EnvironmentFile = (
[ "-/run/prowlarr/secrets.env" ] ["-/run/prowlarr/secrets.env"]
++ lib.optional (cfg.extraEnvVarFile != null && cfg.extraEnvVarFile != "") cfg.extraEnvVarFile ++ lib.optional (cfg.extraEnvVarFile != null && cfg.extraEnvVarFile != "") cfg.extraEnvVarFile
); );
}) }
]; ];
}; };
networking.firewall = mkIf cfg.openFirewall { networking.firewall = mkIf cfg.openFirewall {
allowedTCPPorts = [ cfg.port ]; allowedTCPPorts = [cfg.port];
}; };
users.groups.${cfg.group} = { }; users.groups.${cfg.group} = {};
users.users = mkIf (cfg.user == "prowlarr") { users.users = mkIf (cfg.user == "prowlarr") {
prowlarr = { prowlarr = {
inherit (cfg) group; inherit (cfg) group;

View file

@ -5,8 +5,7 @@
utils, utils,
... ...
}: }:
with lib; with lib; let
let
cfg = config.mySystem.services.radarr; cfg = config.mySystem.services.radarr;
dbOptions = { dbOptions = {
options = { options = {
@ -51,20 +50,18 @@ let
}; };
}; };
}; };
in in {
{
options.mySystem.services.radarr = { options.mySystem.services.radarr = {
enable = mkEnableOption "Radarr (global)"; enable = mkEnableOption "Radarr (global)";
instances = mkOption { instances = mkOption {
type = types.attrsOf ( type = types.attrsOf (
types.submodule ( types.submodule (
{ name, ... }: {name, ...}: {
{
options = { options = {
enable = mkEnableOption "Radarr (instance)"; enable = mkEnableOption "Radarr (instance)";
package = mkPackageOption pkgs "Radarr" { }; package = mkPackageOption pkgs "Radarr" {};
user = mkOption { user = mkOption {
type = types.str; type = types.str;
@ -131,12 +128,20 @@ in
passwordFile = "/run/secrets/radarr_db_password"; passwordFile = "/run/secrets/radarr_db_password";
dbname = "radarr_main"; dbname = "radarr_main";
}; };
default = {
enable = false;
host = "";
port = "5432";
user = "";
passwordFile = "";
dbname = "";
};
description = "Database settings for radarr."; description = "Database settings for radarr.";
}; };
extraEnvVars = mkOption { extraEnvVars = mkOption {
type = types.attrs; type = types.attrs;
default = { }; default = {};
example = { example = {
MY_VAR = "my value"; MY_VAR = "my value";
}; };
@ -153,7 +158,7 @@ in
} }
) )
); );
default = { }; default = {};
description = "Radarr instance configurations."; description = "Radarr instance configurations.";
}; };
}; };
@ -163,8 +168,8 @@ in
assertions = flatten ( assertions = flatten (
mapAttrsToList ( mapAttrsToList (
name: instanceCfg: name: instanceCfg:
if instanceCfg.enable then if instanceCfg.enable
[ then [
{ {
assertion = !(instanceCfg.db.host != "" && instanceCfg.db.hostFile != ""); assertion = !(instanceCfg.db.host != "" && instanceCfg.db.hostFile != "");
message = "Specify either a direct database host via db.host or a file via db.hostFile (leave direct host empty)."; message = "Specify either a direct database host via db.host or a file via db.hostFile (leave direct host empty).";
@ -178,180 +183,186 @@ in
message = "Specify either a direct API key via apiKey or a file via apiKeyFile (leave direct API key empty)."; message = "Specify either a direct API key via apiKey or a file via apiKeyFile (leave direct API key empty).";
} }
] ]
else else []
[ ] )
) cfg.instances cfg.instances
); );
# Create systemd tmpfiles rules for each enabled instance # Create systemd tmpfiles rules for each enabled instance
systemd.tmpfiles.rules = flatten ( systemd.tmpfiles.rules = flatten (
mapAttrsToList ( mapAttrsToList (
name: instanceCfg: name: instanceCfg:
if instanceCfg.enable then if instanceCfg.enable
[ then [
"d ${instanceCfg.dataDir} 0775 ${instanceCfg.user} ${instanceCfg.group}" "d ${instanceCfg.dataDir} 0775 ${instanceCfg.user} ${instanceCfg.group}"
] ]
else else []
[ ] )
) cfg.instances cfg.instances
); );
# Create services for each enabled instance # Create services for each enabled instance
systemd.services = mapAttrs' ( systemd.services =
name: instanceCfg: mapAttrs' (
nameValuePair "radarr-${name}" ( name: instanceCfg:
mkIf instanceCfg.enable { nameValuePair "radarr-${name}" (
description = "Radarr (${name})"; mkIf instanceCfg.enable {
after = [ description = "Radarr (${name})";
"network.target" after = [
"nss-lookup.target" "network.target"
]; "nss-lookup.target"
wantedBy = [ "multi-user.target" ];
environment = lib.mkMerge [
{
RADARR__APP__INSTANCENAME = name;
RADARR__APP__THEME = "dark";
RADARR__AUTH__METHOD = "External";
RADARR__AUTH__REQUIRED = "DisabledForLocalAddresses";
RADARR__LOG__DBENABLED = "False";
RADARR__LOG__LEVEL = "info";
RADARR__SERVER__PORT = toString instanceCfg.port;
RADARR__UPDATE__BRANCH = "develop";
}
(lib.mkIf instanceCfg.db.enable {
RADARR__POSTGRES__PORT = toString instanceCfg.db.port;
RADARR__POSTGRES__MAINDB = instanceCfg.db.dbname;
})
instanceCfg.extraEnvVars
];
serviceConfig = lib.mkMerge [
{
Type = "simple";
User = instanceCfg.user;
Group = instanceCfg.group;
ExecStart = utils.escapeSystemdExecArgs [
(lib.getExe instanceCfg.package)
"-nobrowser"
"-data=${instanceCfg.dataDir}"
"-port=${toString instanceCfg.port}"
]; ];
WorkingDirectory = instanceCfg.dataDir; wantedBy = ["multi-user.target"];
RuntimeDirectory = "radarr-${name}"; environment = lib.mkMerge [
LogsDirectory = "radarr-${name}"; {
RuntimeDirectoryMode = "0750"; RADARR__APP__INSTANCENAME = name;
Restart = "on-failure"; RADARR__APP__THEME = "dark";
RestartSec = 5; RADARR__AUTH__METHOD = "External";
} RADARR__AUTH__REQUIRED = "DisabledForLocalAddresses";
(lib.mkIf instanceCfg.hardening { RADARR__LOG__DBENABLED = "False";
CapabilityBoundingSet = [ "" ]; RADARR__LOG__LEVEL = "info";
DeviceAllow = [ "" ]; RADARR__SERVER__PORT = toString instanceCfg.port;
DevicePolicy = "closed"; RADARR__UPDATE__BRANCH = "develop";
LockPersonality = true;
MemoryDenyWriteExecute = false;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectControlGroups = true;
ProtectHome = "read-only";
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ReadWritePaths = [
instanceCfg.dataDir
instanceCfg.moviesDir
"/var/log/radarr-${name}"
"/eru/media"
];
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = [
"uts"
"ipc"
"pid"
"user"
"cgroup"
"net"
];
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
];
})
(lib.mkIf instanceCfg.db.enable {
ExecStartPre = "+${pkgs.writeShellScript "radarr-${name}-pre-script" ''
mkdir -p /run/radarr-${name}
rm -f /run/radarr-${name}/secrets.env
# Helper function to safely write variables
write_var() {
local var_name="$1"
local value="$2"
if [ -n "$value" ]; then
printf "%s=%s\n" "$var_name" "$value" >> /run/radarr-${name}/secrets.env
fi
} }
(lib.mkIf instanceCfg.db.enable {
RADARR__POSTGRES__PORT = toString instanceCfg.db.port;
RADARR__POSTGRES__MAINDB = instanceCfg.db.dbname;
})
instanceCfg.extraEnvVars
];
# API Key (direct value or file) serviceConfig = lib.mkMerge [
if [ -n "${instanceCfg.apiKey}" ]; then {
write_var "RADARR__AUTH__APIKEY" "${instanceCfg.apiKey}" Type = "simple";
else User = instanceCfg.user;
write_var "RADARR__AUTH__APIKEY" "$(cat ${instanceCfg.apiKeyFile})" Group = instanceCfg.group;
fi ExecStart = utils.escapeSystemdExecArgs [
(lib.getExe instanceCfg.package)
"-nobrowser"
"-data=${instanceCfg.dataDir}"
"-port=${toString instanceCfg.port}"
];
WorkingDirectory = instanceCfg.dataDir;
RuntimeDirectory = "radarr-${name}";
LogsDirectory = "radarr-${name}";
RuntimeDirectoryMode = "0750";
Restart = "on-failure";
RestartSec = 5;
}
(lib.mkIf instanceCfg.hardening {
CapabilityBoundingSet = [""];
DeviceAllow = [""];
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = false;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectControlGroups = true;
ProtectHome = "read-only";
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ReadWritePaths = [
instanceCfg.dataDir
instanceCfg.moviesDir
"/var/log/radarr-${name}"
"/eru/media"
];
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = [
"uts"
"ipc"
"pid"
"user"
"cgroup"
"net"
];
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
];
})
{
ExecStartPre = "+${pkgs.writeShellScript "radarr-${name}-pre-script" ''
mkdir -p /run/radarr-${name}
rm -f /run/radarr-${name}/secrets.env
# Database Configuration # Helper function to safely write variables
write_var "RADARR__POSTGRES__HOST" "$([ -n "${instanceCfg.db.host}" ] && echo "${instanceCfg.db.host}" || cat "${instanceCfg.db.hostFile}")" write_var() {
write_var "RADARR__POSTGRES__USER" "$([ -n "${instanceCfg.db.userFile}" ] && cat "${instanceCfg.db.userFile}" || echo "${instanceCfg.db.user}")" local var_name="$1"
write_var "RADARR__POSTGRES__PASSWORD" "$(cat ${instanceCfg.db.passwordFile})" local value="$2"
if [ -n "$value" ]; then
printf "%s=%s\n" "$var_name" "$value" >> /run/radarr-${name}/secrets.env
fi
}
# Final permissions # API Key (direct value or file)
chmod 600 /run/radarr-${name}/secrets.env if [ -n "${instanceCfg.apiKey}" ]; then
chown ${instanceCfg.user}:${instanceCfg.group} /run/radarr-${name}/secrets.env write_var "RADARR__AUTH__APIKEY" "${instanceCfg.apiKey}"
''}"; else
write_var "RADARR__AUTH__APIKEY" "$(cat ${instanceCfg.apiKeyFile})"
fi
EnvironmentFile = ( ${lib.optionalString instanceCfg.db.enable ''
[ "-/run/radarr-${name}/secrets.env" ] # Database Configuration
++ lib.optional ( write_var "RADARR__POSTGRES__HOST" "$([ -n "${instanceCfg.db.host}" ] && echo "${instanceCfg.db.host}" || cat "${instanceCfg.db.hostFile}")"
instanceCfg.extraEnvVarFile != null && instanceCfg.extraEnvVarFile != "" write_var "RADARR__POSTGRES__USER" "$([ -n "${instanceCfg.db.userFile}" ] && cat "${instanceCfg.db.userFile}" || echo "${instanceCfg.db.user}")"
) instanceCfg.extraEnvVarFile write_var "RADARR__POSTGRES__PASSWORD" "$(cat ${instanceCfg.db.passwordFile})"
); ''}
})
]; # Final permissions
} chmod 600 /run/radarr-${name}/secrets.env
chown ${instanceCfg.user}:${instanceCfg.group} /run/radarr-${name}/secrets.env
''}";
EnvironmentFile = (
["-/run/radarr-${name}/secrets.env"]
++ lib.optional (
instanceCfg.extraEnvVarFile != null && instanceCfg.extraEnvVarFile != ""
)
instanceCfg.extraEnvVarFile
);
}
];
}
)
) )
) cfg.instances; cfg.instances;
# Firewall configurations # Firewall configurations
networking.firewall = mkMerge ( networking.firewall = mkMerge (
mapAttrsToList ( mapAttrsToList (
name: instanceCfg: name: instanceCfg:
mkIf (instanceCfg.enable && instanceCfg.openFirewall) { mkIf (instanceCfg.enable && instanceCfg.openFirewall) {
allowedTCPPorts = [ instanceCfg.port ]; allowedTCPPorts = [instanceCfg.port];
} }
) cfg.instances )
cfg.instances
); );
# Users and groups # Users and groups
users = mkMerge ( users = mkMerge (
mapAttrsToList ( mapAttrsToList (
name: instanceCfg: name: instanceCfg:
mkIf instanceCfg.enable { mkIf instanceCfg.enable {
groups.${instanceCfg.group} = { }; groups.${instanceCfg.group} = {};
users = mkIf (instanceCfg.user == "radarr") { users = mkIf (instanceCfg.user == "radarr") {
radarr = { radarr = {
inherit (instanceCfg) group; inherit (instanceCfg) group;
isSystemUser = true; isSystemUser = true;
# home = instanceCfg.dataDir; # home = instanceCfg.dataDir;
home = "/nahar/radarr"; home = "/nahar/radarr";
};
}; };
}; }
} )
) cfg.instances cfg.instances
); );
}; };
} }

View file

@ -5,8 +5,7 @@
utils, utils,
... ...
}: }:
with lib; with lib; let
let
cfg = config.mySystem.services.sonarr; cfg = config.mySystem.services.sonarr;
dbOptions = { dbOptions = {
options = { options = {
@ -51,20 +50,18 @@ let
}; };
}; };
}; };
in in {
{
options.mySystem.services.sonarr = { options.mySystem.services.sonarr = {
enable = mkEnableOption "Sonarr (global)"; enable = mkEnableOption "Sonarr (global)";
instances = mkOption { instances = mkOption {
type = types.attrsOf ( type = types.attrsOf (
types.submodule ( types.submodule (
{ name, ... }: {name, ...}: {
{
options = { options = {
enable = mkEnableOption "Sonarr (instance)"; enable = mkEnableOption "Sonarr (instance)";
package = mkPackageOption pkgs "Sonarr" { }; package = mkPackageOption pkgs "Sonarr" {};
user = mkOption { user = mkOption {
type = types.str; type = types.str;
@ -131,12 +128,20 @@ in
passwordFile = "/run/secrets/sonarr_db_password"; passwordFile = "/run/secrets/sonarr_db_password";
dbname = "sonarr_main"; dbname = "sonarr_main";
}; };
default = {
enable = false;
host = "";
port = "5432";
user = "";
passwordFile = "";
dbname = "";
};
description = "Database settings for sonarr."; description = "Database settings for sonarr.";
}; };
extraEnvVars = mkOption { extraEnvVars = mkOption {
type = types.attrs; type = types.attrs;
default = { }; default = {};
example = { example = {
MY_VAR = "my value"; MY_VAR = "my value";
}; };
@ -153,7 +158,7 @@ in
} }
) )
); );
default = { }; default = {};
description = "Sonarr instance configurations."; description = "Sonarr instance configurations.";
}; };
}; };
@ -163,14 +168,14 @@ in
assertions = flatten ( assertions = flatten (
mapAttrsToList ( mapAttrsToList (
name: instanceCfg: name: instanceCfg:
if instanceCfg.enable then if instanceCfg.enable
[ then [
{ {
assertion = !(instanceCfg.db.host != "" && instanceCfg.db.hostFile != ""); assertion = !(instanceCfg.db.enable && (instanceCfg.db.host != "" && instanceCfg.db.hostFile != ""));
message = "Specify either a direct database host via db.host or a file via db.hostFile (leave direct host empty)."; message = "Specify either a direct database host via db.host or a file via db.hostFile (leave direct host empty).";
} }
{ {
assertion = !(instanceCfg.db.user != "sonarr" && instanceCfg.db.userFile != ""); assertion = !(instanceCfg.db.enable && (instanceCfg.db.user != "sonarr" && instanceCfg.db.userFile != ""));
message = "Specify either a direct database user via db.user or a file via db.userFile."; message = "Specify either a direct database user via db.user or a file via db.userFile.";
} }
{ {
@ -178,180 +183,187 @@ in
message = "Specify either a direct API key via apiKey or a file via apiKeyFile (leave direct API key empty)."; message = "Specify either a direct API key via apiKey or a file via apiKeyFile (leave direct API key empty).";
} }
] ]
else else []
[ ] )
) cfg.instances cfg.instances
); );
# Create systemd tmpfiles rules for each enabled instance # Create systemd tmpfiles rules for each enabled instance
systemd.tmpfiles.rules = flatten ( systemd.tmpfiles.rules = flatten (
mapAttrsToList ( mapAttrsToList (
name: instanceCfg: name: instanceCfg:
if instanceCfg.enable then if instanceCfg.enable
[ then [
"d ${instanceCfg.dataDir} 0775 ${instanceCfg.user} ${instanceCfg.group}" "d ${instanceCfg.dataDir} 0775 ${instanceCfg.user} ${instanceCfg.group}"
] ]
else else []
[ ] )
) cfg.instances cfg.instances
); );
# Create services for each enabled instance # Create services for each enabled instance
systemd.services = mapAttrs' ( systemd.services =
name: instanceCfg: mapAttrs' (
nameValuePair "sonarr-${name}" ( name: instanceCfg:
mkIf instanceCfg.enable { nameValuePair "sonarr-${name}" (
description = "Sonarr (${name})"; mkIf instanceCfg.enable {
after = [ description = "Sonarr (${name})";
"network.target" after = [
"nss-lookup.target" "network.target"
]; "nss-lookup.target"
wantedBy = [ "multi-user.target" ];
environment = lib.mkMerge [
{
SONARR__APP__INSTANCENAME = name;
SONARR__APP__THEME = "dark";
SONARR__AUTH__METHOD = "External";
SONARR__AUTH__REQUIRED = "DisabledForLocalAddresses";
SONARR__LOG__DBENABLED = "False";
SONARR__LOG__LEVEL = "info";
SONARR__SERVER__PORT = toString instanceCfg.port;
SONARR__UPDATE__BRANCH = "develop";
}
(lib.mkIf instanceCfg.db.enable {
SONARR__POSTGRES__PORT = toString instanceCfg.db.port;
SONARR__POSTGRES__MAINDB = instanceCfg.db.dbname;
})
instanceCfg.extraEnvVars
];
serviceConfig = lib.mkMerge [
{
Type = "simple";
User = instanceCfg.user;
Group = instanceCfg.group;
ExecStart = utils.escapeSystemdExecArgs [
(lib.getExe instanceCfg.package)
"-nobrowser"
"-data=${instanceCfg.dataDir}"
"-port=${toString instanceCfg.port}"
]; ];
WorkingDirectory = instanceCfg.dataDir; wantedBy = ["multi-user.target"];
RuntimeDirectory = "sonarr-${name}"; environment = lib.mkMerge [
LogsDirectory = "sonarr-${name}"; {
RuntimeDirectoryMode = "0750"; SONARR__APP__INSTANCENAME = name;
Restart = "on-failure"; SONARR__APP__THEME = "dark";
RestartSec = 5; SONARR__AUTH__METHOD = "External";
} SONARR__AUTH__REQUIRED = "DisabledForLocalAddresses";
(lib.mkIf instanceCfg.hardening { SONARR__LOG__DBENABLED = "False";
CapabilityBoundingSet = [ "" ]; SONARR__LOG__LEVEL = "info";
DeviceAllow = [ "" ]; SONARR__SERVER__PORT = toString instanceCfg.port;
DevicePolicy = "closed"; SONARR__UPDATE__BRANCH = "develop";
LockPersonality = true;
MemoryDenyWriteExecute = false;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectControlGroups = true;
ProtectHome = "read-only";
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ReadWritePaths = [
instanceCfg.dataDir
instanceCfg.tvDir
"/var/log/sonarr-${name}"
"/eru/media"
];
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = [
"uts"
"ipc"
"pid"
"user"
"cgroup"
"net"
"mnt"
];
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
];
})
(lib.mkIf instanceCfg.db.enable {
ExecStartPre = "+${pkgs.writeShellScript "sonarr-${name}-pre-script" ''
mkdir -p /run/sonarr-${name}
rm -f /run/sonarr-${name}/secrets.env
# Helper function to safely write variables
write_var() {
local var_name="$1"
local value="$2"
if [ -n "$value" ]; then
printf "%s=%s\n" "$var_name" "$value" >> /run/sonarr-${name}/secrets.env
fi
} }
(lib.mkIf instanceCfg.db.enable {
SONARR__POSTGRES__PORT = toString instanceCfg.db.port;
SONARR__POSTGRES__MAINDB = instanceCfg.db.dbname;
})
instanceCfg.extraEnvVars
];
# API Key (direct value or file) serviceConfig = lib.mkMerge [
if [ -n "${instanceCfg.apiKey}" ]; then {
write_var "SONARR__AUTH__APIKEY" "${instanceCfg.apiKey}" Type = "simple";
else User = instanceCfg.user;
write_var "SONARR__AUTH__APIKEY" "$(cat ${instanceCfg.apiKeyFile})" Group = instanceCfg.group;
fi ExecStart = utils.escapeSystemdExecArgs [
(lib.getExe instanceCfg.package)
"-nobrowser"
"-data=${instanceCfg.dataDir}"
"-port=${toString instanceCfg.port}"
];
WorkingDirectory = instanceCfg.dataDir;
RuntimeDirectory = "sonarr-${name}";
LogsDirectory = "sonarr-${name}";
RuntimeDirectoryMode = "0750";
Restart = "on-failure";
RestartSec = 5;
}
(lib.mkIf instanceCfg.hardening {
CapabilityBoundingSet = [""];
DeviceAllow = [""];
DevicePolicy = "closed";
LockPersonality = true;
MemoryDenyWriteExecute = false;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateTmp = true;
ProtectControlGroups = true;
ProtectHome = "read-only";
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
ReadWritePaths = [
instanceCfg.dataDir
instanceCfg.tvDir
"/var/log/sonarr-${name}"
"/eru/media"
];
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_NETLINK"
];
RestrictNamespaces = [
"uts"
"ipc"
"pid"
"user"
"cgroup"
"net"
"mnt"
];
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
];
})
{
ExecStartPre = "+${pkgs.writeShellScript "sonarr-${name}-pre-script" ''
mkdir -p /run/sonarr-${name}
rm -f /run/sonarr-${name}/secrets.env
# Database Configuration # Helper function to safely write variables
write_var "SONARR__POSTGRES__HOST" "$([ -n "${instanceCfg.db.host}" ] && echo "${instanceCfg.db.host}" || cat "${instanceCfg.db.hostFile}")" write_var() {
write_var "SONARR__POSTGRES__USER" "$([ -n "${instanceCfg.db.userFile}" ] && cat "${instanceCfg.db.userFile}" || echo "${instanceCfg.db.user}")" local var_name="$1"
write_var "SONARR__POSTGRES__PASSWORD" "$(cat ${instanceCfg.db.passwordFile})" local value="$2"
if [ -n "$value" ]; then
printf "%s=%s\n" "$var_name" "$value" >> /run/sonarr-${name}/secrets.env
fi
}
# Final permissions # API Key (direct value or file)
chmod 600 /run/sonarr-${name}/secrets.env if [ -n "${instanceCfg.apiKey}" ]; then
chown ${instanceCfg.user}:${instanceCfg.group} /run/sonarr-${name}/secrets.env write_var "SONARR__AUTH__APIKEY" "${instanceCfg.apiKey}"
''}"; else
write_var "SONARR__AUTH__APIKEY" "$(cat ${instanceCfg.apiKeyFile})"
fi
EnvironmentFile = ( ${lib.optionalString instanceCfg.db.enable ''
[ "-/run/sonarr-${name}/secrets.env" ] # Database Configuration
++ lib.optional ( write_var "SONARR__POSTGRES__HOST" "$([ -n "${instanceCfg.db.host}" ] && echo "${instanceCfg.db.host}" || cat "${instanceCfg.db.hostFile}")"
instanceCfg.extraEnvVarFile != null && instanceCfg.extraEnvVarFile != "" write_var "SONARR__POSTGRES__USER" "$([ -n "${instanceCfg.db.userFile}" ] && cat "${instanceCfg.db.userFile}" || echo "${instanceCfg.db.user}")"
) instanceCfg.extraEnvVarFile write_var "SONARR__POSTGRES__PASSWORD" "$(cat ${instanceCfg.db.passwordFile})"
); ''}
})
]; # Final permissions
} chmod 600 /run/sonarr-${name}/secrets.env
chown ${instanceCfg.user}:${instanceCfg.group} /run/sonarr-${name}/secrets.env
''}";
EnvironmentFile = (
["-/run/sonarr-${name}/secrets.env"]
++ lib.optional (
instanceCfg.extraEnvVarFile != null && instanceCfg.extraEnvVarFile != ""
)
instanceCfg.extraEnvVarFile
);
}
];
}
)
) )
) cfg.instances; cfg.instances;
# Firewall configurations # Firewall configurations
networking.firewall = mkMerge ( networking.firewall = mkMerge (
mapAttrsToList ( mapAttrsToList (
name: instanceCfg: name: instanceCfg:
mkIf (instanceCfg.enable && instanceCfg.openFirewall) { mkIf (instanceCfg.enable && instanceCfg.openFirewall) {
allowedTCPPorts = [ instanceCfg.port ]; allowedTCPPorts = [instanceCfg.port];
} }
) cfg.instances )
cfg.instances
); );
# Users and groups # Users and groups
users = mkMerge ( users = mkMerge (
mapAttrsToList ( mapAttrsToList (
name: instanceCfg: name: instanceCfg:
mkIf instanceCfg.enable { mkIf instanceCfg.enable {
groups.${instanceCfg.group} = { }; groups.${instanceCfg.group} = {};
users = mkIf (instanceCfg.user == "sonarr") { users = mkIf (instanceCfg.user == "sonarr") {
sonarr = { sonarr = {
inherit (instanceCfg) group; inherit (instanceCfg) group;
isSystemUser = true; isSystemUser = true;
# home = instanceCfg.dataDir; # home = instanceCfg.dataDir;
home = "/nahar/sonarr"; home = "/nahar/sonarr";
};
}; };
}; }
} )
) cfg.instances cfg.instances
); );
}; };
} }