From fd800ff25d572b2df96ed9e6124c3ad391f997c6 Mon Sep 17 00:00:00 2001 From: Joseph Hanson Date: Mon, 10 Feb 2025 15:01:31 -0600 Subject: [PATCH] add multi-sonarr --- nixos/hosts/shadowfax/config/sops-secrets.nix | 60 ++- nixos/hosts/shadowfax/default.nix | 59 +- nixos/hosts/shadowfax/secrets.sops.yaml | 27 +- .../modules/nixos/services/sonarr/default.nix | 506 ++++++++++-------- 4 files changed, 389 insertions(+), 263 deletions(-) diff --git a/nixos/hosts/shadowfax/config/sops-secrets.nix b/nixos/hosts/shadowfax/config/sops-secrets.nix index c99120e..5d23620 100644 --- a/nixos/hosts/shadowfax/config/sops-secrets.nix +++ b/nixos/hosts/shadowfax/config/sops-secrets.nix @@ -54,41 +54,77 @@ restartUnits = [ "prowlarr.service" ]; }; # Sonarr - "arr/sonarr/apiKey" = { + "arr/sonarr/1080p/apiKey" = { sopsFile = ../secrets.sops.yaml; owner = "sonarr"; mode = "400"; - restartUnits = [ "sonarr.service" ]; + restartUnits = [ "sonarr-tv1080p.service" ]; }; - "arr/sonarr/postgres/dbName" = { + "arr/sonarr/1080p/postgres/dbName" = { sopsFile = ../secrets.sops.yaml; owner = "sonarr"; mode = "400"; - restartUnits = [ "sonarr.service" ]; + restartUnits = [ "sonarr-tv1080p.service" ]; }; - "arr/sonarr/postgres/user" = { + "arr/sonarr/1080p/postgres/user" = { sopsFile = ../secrets.sops.yaml; owner = "sonarr"; mode = "400"; - restartUnits = [ "sonarr.service" ]; + restartUnits = [ "sonarr-tv1080p.service" ]; }; - "arr/sonarr/postgres/password" = { + "arr/sonarr/1080p/postgres/password" = { sopsFile = ../secrets.sops.yaml; owner = "sonarr"; mode = "400"; - restartUnits = [ "sonarr.service" ]; + restartUnits = [ "sonarr-tv1080p.service" ]; }; - "arr/sonarr/postgres/host" = { + "arr/sonarr/1080p/postgres/host" = { sopsFile = ../secrets.sops.yaml; owner = "sonarr"; mode = "400"; - restartUnits = [ "sonarr.service" ]; + restartUnits = [ "sonarr-tv1080p.service" ]; }; - "arr/sonarr/extraEnvVars" = { + "arr/sonarr/1080p/extraEnvVars" = { sopsFile = ../secrets.sops.yaml; owner = "sonarr"; mode = "400"; - restartUnits = [ "sonarr.service" ]; + restartUnits = [ "sonarr-tv1080p.service" ]; + }; + "arr/sonarr/anime/apiKey" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr-anime.service" ]; + }; + "arr/sonarr/anime/postgres/dbName" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr-anime.service" ]; + }; + "arr/sonarr/anime/postgres/user" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr-anime.service" ]; + }; + "arr/sonarr/anime/postgres/password" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr-anime.service" ]; + }; + "arr/sonarr/anime/postgres/host" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr-anime.service" ]; + }; + "arr/sonarr/anime/extraEnvVars" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr-anime.service" ]; }; # Radarr "arr/radarr/1080p/apiKey" = { diff --git a/nixos/hosts/shadowfax/default.nix b/nixos/hosts/shadowfax/default.nix index fa06c95..6e8b709 100644 --- a/nixos/hosts/shadowfax/default.nix +++ b/nixos/hosts/shadowfax/default.nix @@ -317,22 +317,49 @@ in # Sonarr sonarr = { enable = true; - package = pkgs.unstable.sonarr; - dataDir = "/nahar/sonarr"; - extraEnvVarFile = config.sops.secrets."arr/sonarr/extraEnvVars".path; - tvDir = "/moria/media/TV"; - user = "sonarr"; - group = "kah"; - port = 8989; - openFirewall = true; - hardening = true; - apiKeyFile = config.sops.secrets."arr/sonarr/apiKey".path; - db = { - enable = true; - hostFile = config.sops.secrets."arr/sonarr/postgres/host".path; - port = 5432; - userFile = config.sops.secrets."arr/sonarr/postgres/user".path; - passwordFile = config.sops.secrets."arr/sonarr/postgres/password".path; + instances = { + tv1080p = { + enable = true; + package = pkgs.unstable.sonarr; + dataDir = "/nahar/sonarr/1080p"; + extraEnvVarFile = config.sops.secrets."arr/sonarr/1080p/extraEnvVars".path; + tvDir = "/moria/media/TV"; + user = "sonarr"; + group = "kah"; + port = 8989; + openFirewall = true; + hardening = true; + apiKeyFile = config.sops.secrets."arr/sonarr/1080p/apiKey".path; + db = { + enable = true; + hostFile = config.sops.secrets."arr/sonarr/1080p/postgres/host".path; + port = 5432; + dbname = "sonarr_main"; + userFile = config.sops.secrets."arr/sonarr/1080p/postgres/user".path; + passwordFile = config.sops.secrets."arr/sonarr/1080p/postgres/password".path; + }; + }; + anime = { + enable = true; + package = pkgs.unstable.sonarr; + dataDir = "/nahar/sonarr/anime"; + extraEnvVarFile = config.sops.secrets."arr/sonarr/anime/extraEnvVars".path; + tvDir = "/moria/media/Anime/Shows"; + user = "sonarr"; + group = "kah"; + port = 8990; + openFirewall = true; + hardening = true; + apiKeyFile = config.sops.secrets."arr/sonarr/anime/apiKey".path; + db = { + enable = true; + hostFile = config.sops.secrets."arr/sonarr/anime/postgres/host".path; + port = 5432; + dbname = "sonarr_anime"; + userFile = config.sops.secrets."arr/sonarr/anime/postgres/user".path; + passwordFile = config.sops.secrets."arr/sonarr/anime/postgres/password".path; + }; + }; }; }; # Sabnzbd diff --git a/nixos/hosts/shadowfax/secrets.sops.yaml b/nixos/hosts/shadowfax/secrets.sops.yaml index 75deae8..73f8132 100644 --- a/nixos/hosts/shadowfax/secrets.sops.yaml +++ b/nixos/hosts/shadowfax/secrets.sops.yaml @@ -20,13 +20,22 @@ arr: user: ENC[AES256_GCM,data:gO8c5bZ3oDY=,iv:YggC8TNFzqHRcRxSBDiV580xF3kLQKgR/ScfyW+5Y5A=,tag:bY7QduxooRkR5SFBxlKjxQ==,type:str] password: ENC[AES256_GCM,data:rqryseQj0lMiNmB21ezXYQ7ceaOtiZJLPA==,iv:f0ahBkII+pZOPB50EcCIEMbvHriYHv7ax/u1515KAA8=,tag:EObTc1yd/GTNuVE87tPg0g==,type:str] sonarr: - apiKey: ENC[AES256_GCM,data:TVy4L0ctHhT3gNp+WCaLCUVc0no8VIkWenroFOYk8h4z,iv:A0a6IUBeDDxPiLlrPCXhXu586QRnXha0RthuXUKkU4I=,tag:oVMS5Ys/NiDrA6YSiCjqsw==,type:str] - postgres: - host: ENC[AES256_GCM,data:X0suLwp9bZE=,iv:UQXhPilmi0ix0GruDXfLeHXGUY8k+iAL03/3K/EfcCc=,tag:y3tzbv3nxbOzNEnZQCdeVg==,type:str] - dbName: ENC[AES256_GCM,data:Um9YpALoU7qQfTo=,iv:q0IVjaxyaG8MWAxp43kZjHIBm6dWv37maykSfhAxe1M=,tag:NLqIikfWculCeuoRqPHc8Q==,type:str] - user: ENC[AES256_GCM,data:Vd68IvZs,iv:DYT3PudE94JZZTZHzV8QgRYADtThZhxTjFJByLcZP1c=,tag:pX1ZNC+M9Jm+PlQ22BZMRw==,type:str] - password: ENC[AES256_GCM,data:XOrycMom2utnefraGPoAq7xtP6yfSzTb8g==,iv:WQInK+bJuDNI9uN/GeQ2Fb1Mmlux6+lXwkGS1ZEh+kQ=,tag:DGqLerxomCVfVv15Gt3b8A==,type:str] - extraEnvVars: ENC[AES256_GCM,data:KnZSJ2YbNLawSzrj7syx0cfAFseHbgjGvjpB7yWajXfCIy+CV800z9YU2SVO2kV6b+9OrmyKKFFbM5ac4cWnc5Pcx8TUxfiAuL5RSi6ZTmUrZUA7Zqx5UDTHwXgvhDI=,iv:TX8sFk7uc1TYG/gkuA9plGZlhP25WuczEXd+QKsPi4c=,tag:zhVeZxrTgcv+Y2OP8I+k5g==,type:str] + 1080p: + apiKey: ENC[AES256_GCM,data:e3v+MR2BXCRXXm01/Y09mWN9SnjwH/qQ5tJ5/lNsuqRL,iv:nAT5aFT0ihHGGSTOUBrPZy4d6S+kbOzjNebXftPjoAI=,tag:uAZ1nuDcIYWKAOA2XLdBFQ==,type:str] + postgres: + host: ENC[AES256_GCM,data:GH0XC+qbTC4=,iv:WnLz/rHhM44zmP/OyU/AOmsIg8xNhtgExO5MYnA0gqw=,tag:VFrMySTEhDsz6fZU9s+ZKg==,type:str] + dbName: ENC[AES256_GCM,data:ftOhHHtk/McHs0k=,iv:Zh9/QsLYMTrtpukvA8CM0bj0scxIUaiKwlRkr38mDkI=,tag:DHpH90q2cuLk+KzMeciVQQ==,type:str] + user: ENC[AES256_GCM,data:M5/SpaeO,iv:GZlxnQl+Vt9jE+f1obSqPJGC9peN3qlqw/ZaIf6p2YU=,tag:vXJHGjvNFLV5LirfBnNs2g==,type:str] + password: ENC[AES256_GCM,data:M0YWtT/ikSKqQCLkF+Ln+jm3t6ltsLzYJQ==,iv:L6sCp1R1KkNRry2j/PO6+nHsL4gwOpzXVt3btcsC9QI=,tag:fiv3scPVQQzBjuQyzkMYFw==,type:str] + extraEnvVars: ENC[AES256_GCM,data:W4WQwgSErsTzRAdnfhdYMn0QCrF30GGegTIXnwmweDb5cDRmLaVml5WoYd7PVRcZMlqOnOl7ZomzvkkVeJ77It3iOHiAsoSQ9b8zZCjkkanFGvQw6y3vDdOWXKhyonk=,iv:UK9g5aivhcoSNO7BBmIi8qtEssYEMoae6oPvbIIBS0c=,tag:KlF6idwZmRVJCITe1Sf4PA==,type:str] + anime: + apiKey: ENC[AES256_GCM,data:4Xg3O7Xrw0f8ijNvhnDnmizpAeSVL9qS8KbR7qeosz/K,iv:qlIyhT6bqMZCBNtuCKbRm9x/2g/qFCbL3xqa+mEG6ZM=,tag:I1gqP1caMpfIj0eXuS1t3w==,type:str] + postgres: + host: ENC[AES256_GCM,data:AZrIFD0MRlI=,iv:lC/CDAq65vSRacWCi7m3Jr3j2l2r9deNbqMyJN0ZNXE=,tag:GiNvGupolm4w9Z7XgQTryg==,type:str] + dbName: ENC[AES256_GCM,data:74KryUh6B2mBZyIW,iv:fdnogNwmx5+qlfH7eOnDZhNeNJ1C2v8gT/g7X1nNtLI=,tag:S28PslyTuawLopyrUDLD5Q==,type:str] + user: ENC[AES256_GCM,data:cM133gBrl0WM/DSz,iv:CNyUxvV1VOfAy+D2+dMmJMIoWXGH3qoJ8dXXU76NSXo=,tag:03dMKeF9daZZUwxXW5mz7g==,type:str] + password: ENC[AES256_GCM,data:/XQNL9a3QUeDhPY7JRE+4/X6WmJ6dliHUhty+m3OmA==,iv:+Nn9Q+lfGGDmpd089qXc26+3PxIbOrHLsoolsgpQK24=,tag:HL7J0TTvd4DDIob71/Zfuw==,type:str] + extraEnvVars: ENC[AES256_GCM,data:G8muZh4O/kiDFW3CjZXwXnoLxtsimq9evShea0aAbfPdNd3nenudPoI2PYgIF2vT4vHzMaWLCrvbqcd/gLBJUrZZyn4YAvuRo6LBliAD394EyXHZrdrTNNDkLZ8LQRU=,iv:Nh6QZhpwftrMs4Km1emymk1ilmFCtYeW0l05yRmXxug=,tag:CVl6nWN3wyc1r23eKPljmg==,type:str] radarr: 1080p: apiKey: ENC[AES256_GCM,data:NVIa3nK4/QxZF1/peKei9Qsxn2AjRiBnBdr2N08QiXhi,iv:h/Rb38FCF8auMCTT+Cuj/i8YAeavntwbbJEA9LwDYUY=,tag:OwAQy76GjIrY9/126zExdA==,type:str] @@ -124,8 +133,8 @@ sops: aVlOSHhFb2I5UnYwVytyQzlWTXBDYUUKdQKilmfJ1F7UYKtQV9zV95FcRIK17p4M vGvu/pGJ32tH8xI7cNs9I5Hmg9c5wOam21W1FDk+VlJ/ClXqQzS0MA== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-02-10T04:49:46Z" - mac: ENC[AES256_GCM,data:wT82lve5wNbxXUgcw3EZkOrsLFOmtriJtSNtpcfcH2KYkFEZTyzYCO10EBMrm1FjJxJFmhtde9f+CkSkT5ypjP6Ou4TGn9nP0jOlCyLxYyQkv7Lr31aEF8d2Q0NDjSHePW4YUiY88YrOX8shWYFVtNoZSonyCv8G6lwGVIsZzi4=,iv:MkHiwV5qRBE3LfTt0WHV56LWAeTEcRlux4xIf90R3AU=,tag:qUgCaPLa+W/pZ8uM1Gw7Xw==,type:str] + lastmodified: "2025-02-10T19:15:54Z" + mac: ENC[AES256_GCM,data:KyP1lzKD/iV6SgHbsEYvVRwgrFy/PqHGMVEjeTAcLFCFrIyn9/Gd5ravTOj+37phVARnHI3h2vetqzOBO7/tE4jz0nsEGawOfVMOTKR1Y7+35TKJaC4FTO3/hfczjzolvoLk8011J9aoeC44yx+UT8Ijehc3mkd4x8zVvOK/OG4=,iv:e6FudQPXESAw/CNVqJWGZG+/8j/J88Z8InjM++GJ9lM=,tag:KaQkmLGSU+jJF3f6YBKhmQ==,type:str] pgp: [] unencrypted_suffix: _unencrypted version: 3.9.4 diff --git a/nixos/modules/nixos/services/sonarr/default.nix b/nixos/modules/nixos/services/sonarr/default.nix index 4f43f62..4ee670a 100644 --- a/nixos/modules/nixos/services/sonarr/default.nix +++ b/nixos/modules/nixos/services/sonarr/default.nix @@ -5,11 +5,12 @@ utils, ... }: -with lib; let +with lib; +let cfg = config.mySystem.services.sonarr; dbOptions = { options = { - enable = mkEnableOption "Database configuration for sonarr"; + enable = mkEnableOption "Database configuration for Sonarr"; host = mkOption { type = types.str; default = ""; @@ -50,254 +51,307 @@ with lib; let }; }; }; -in { +in +{ options.mySystem.services.sonarr = { - enable = mkEnableOption "Sonarr"; + enable = mkEnableOption "Sonarr (global)"; - package = mkPackageOption pkgs "Sonarr" {}; + instances = mkOption { + type = types.attrsOf ( + types.submodule ( + { name, ... }: + { + options = { + enable = mkEnableOption "Sonarr (instance)"; - user = mkOption { - type = types.str; - default = "sonarr"; - description = "User account under which sonarr runs."; - }; + package = mkPackageOption pkgs "Sonarr" { }; - group = mkOption { - type = types.str; - default = "sonarr"; - description = "Group under which sonarr runs."; - }; + user = mkOption { + type = types.str; + default = "sonarr"; + description = "User account under which sonarr runs."; + }; - dataDir = mkOption { - type = types.path; - default = "/var/lib/sonarr"; - description = "Storage directory for sonarr data"; - }; + group = mkOption { + type = types.str; + default = "sonarr"; + description = "Group under which sonarr runs."; + }; - tvDir = mkOption { - type = types.path; - default = "/mnt/media/tv"; - description = "Directory where tv shows are stored"; - }; + dataDir = mkOption { + type = types.path; + default = "/var/lib/sonarr/${name}"; + description = "Storage directory for sonarr data"; + }; - port = mkOption { - type = types.port; - default = 8989; - description = "Port for sonarr web interface"; - }; + tvDir = mkOption { + type = types.path; + default = "/mnt/media/tv"; + description = "Directory where tv shows are stored"; + }; - openFirewall = mkOption { - type = types.bool; - default = false; - description = "Open firewall ports for sonarr"; - }; + port = mkOption { + type = types.port; + default = 8989; + description = "Port for sonarr web interface"; + }; - hardening = mkOption { - type = types.bool; - default = true; - description = "Enable security hardening features"; - }; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open firewall ports for sonarr"; + }; - apiKey = mkOption { - type = types.str; - default = ""; - example = "abc123"; - description = "Direct API key for sonarr (mutually exclusive with apiKeyFile)"; - }; + hardening = mkOption { + type = types.bool; + default = true; + description = "Enable security hardening features"; + }; - apiKeyFile = mkOption { - type = types.path; - default = "/run/secrets/sonarr_api_key"; - description = "API key for sonarr from a file (mutually exclusive with apiKey)"; - }; + apiKey = mkOption { + type = types.str; + default = ""; + example = "abc123"; + description = "Direct API key for sonarr (mutually exclusive with apiKeyFile)"; + }; - extraEnvVars = mkOption { - type = types.attrs; - default = {}; - example = { - MY_VAR = "my value"; - }; - description = "Extra environment variables for sonarr."; - }; + apiKeyFile = mkOption { + type = types.path; + default = "/run/secrets/sonarr_api_key"; + description = "API key for sonarr from a file (mutually exclusive with apiKey)"; + }; - extraEnvVarFile = mkOption { - type = lib.types.nullOr lib.types.path; - default = null; - example = "/run/secrets/sonarr_extra_env"; - description = "Extra environment file for Sonarr."; - }; + db = mkOption { + type = types.submodule dbOptions; + example = { + enable = true; + host = "10.5.0.5"; # or use hostFile + port = "5432"; + user = "sonarr"; # or userFile + passwordFile = "/run/secrets/sonarr_db_password"; + dbname = "sonarr_main"; + }; + description = "Database settings for sonarr."; + }; - db = mkOption { - type = types.submodule dbOptions; - example = { - enable = true; - host = "10.5.0.5"; # or use hostFile - port = "5432"; - user = "sonarr"; # or userFile - passwordFile = "/run/secrets/sonarr_db_password"; - dbname = "sonarr_main"; - }; - description = "Database settings for sonarr."; + extraEnvVars = mkOption { + type = types.attrs; + default = { }; + example = { + MY_VAR = "my value"; + }; + description = "Extra environment variables for sonarr."; + }; + + extraEnvVarFile = mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/secrets/sonarr_extra_env"; + description = "Extra environment file for Sonarr."; + }; + }; + } + ) + ); + default = { }; + description = "Sonarr instance configurations."; }; }; config = mkIf cfg.enable { - assertions = [ - { - assertion = !(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)."; - } - { - assertion = !(cfg.db.user != "sonarr" && cfg.db.userFile != ""); - message = "Specify either a direct database user via db.user or a file via db.userFile."; - } - { - assertion = !(cfg.apiKey != "" && cfg.apiKeyFile != ""); - message = "Specify either a direct API key via apiKey or a file via apiKeyFile (leave direct API key empty)."; - } - ]; - - systemd.tmpfiles.rules = [ - "d ${cfg.dataDir} 0775 ${cfg.user} ${cfg.group}" - ]; - - systemd.services.sonarr = { - description = "Sonarr"; - after = [ - "network.target" - "nss-lookup.target" - ]; - wantedBy = ["multi-user.target"]; - environment = lib.mkMerge [ - { - SONARR__APP__INSTANCENAME = "Sonarr"; - SONARR__APP__THEME = "dark"; - SONARR__AUTH__METHOD = "External"; - SONARR__AUTH__REQUIRED = "DisabledForLocalAddresses"; - SONARR__LOG__DBENABLED = "False"; - SONARR__LOG__LEVEL = "info"; - SONARR__SERVER__PORT = toString cfg.port; - SONARR__UPDATE__BRANCH = "develop"; - } - (lib.mkIf cfg.db.enable { - SONARR__POSTGRES__PORT = toString cfg.db.port; - SONARR__POSTGRES__MAINDB = cfg.db.dbname; - }) - cfg.extraEnvVars - ]; - - serviceConfig = lib.mkMerge [ - { - Type = "simple"; - User = cfg.user; - Group = cfg.group; - ExecStart = utils.escapeSystemdExecArgs [ - (lib.getExe cfg.package) - "-nobrowser" - "-data=${cfg.dataDir}" - "-port=${toString cfg.port}" - ]; - WorkingDirectory = cfg.dataDir; - RuntimeDirectory = "sonarr"; - LogsDirectory = "sonarr"; - RuntimeDirectoryMode = "0750"; - Restart = "on-failure"; - RestartSec = 5; - } - (lib.mkIf cfg.hardening { - CapabilityBoundingSet = [""]; - DeviceAllow = [""]; - DevicePolicy = "closed"; - LockPersonality = true; - # Needs access to .Net CLR memory space. - MemoryDenyWriteExecute = false; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateTmp = true; - ProtectControlGroups = true; - ProtectHome = "read-only"; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectSystem = "strict"; - ReadWritePaths = [ - cfg.dataDir - cfg.tvDir - "/var/log/sonarr" - "/eru/media" - ]; - RestrictAddressFamilies = [ - "AF_INET" - "AF_INET6" - "AF_NETLINK" - ]; - RestrictNamespaces = [ - "uts" - "ipc" - "pid" - "user" - "cgroup" - "net" - "mnt" - ]; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ - "@system-service" - #"~@privileged" - # .Net CLR requirement - #"~@resources" - ]; - }) - (lib.mkIf cfg.db.enable { - ExecStartPre = "+${pkgs.writeShellScript "sonarr-pre-script" '' - mkdir -p /run/sonarr - rm -f /run/sonarr/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/secrets.env - fi + # Add assertions for all instances + assertions = flatten ( + mapAttrsToList ( + name: instanceCfg: + if instanceCfg.enable then + [ + { + 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)."; } + { + assertion = !(instanceCfg.db.user != "sonarr" && instanceCfg.db.userFile != ""); + message = "Specify either a direct database user via db.user or a file via db.userFile."; + } + { + assertion = !(instanceCfg.apiKey != "" && instanceCfg.apiKeyFile != ""); + message = "Specify either a direct API key via apiKey or a file via apiKeyFile (leave direct API key empty)."; + } + ] + else + [ ] + ) cfg.instances + ); - # API Key (direct value or file) - if [ -n "${cfg.apiKey}" ]; then - write_var "SONARR__AUTH__APIKEY" "${cfg.apiKey}" - else - write_var "SONARR__AUTH__APIKEY" "$(cat ${cfg.apiKeyFile})" - fi + # Create systemd tmpfiles rules for each enabled instance + systemd.tmpfiles.rules = flatten ( + mapAttrsToList ( + name: instanceCfg: + if instanceCfg.enable then + [ + "d ${instanceCfg.dataDir} 0775 ${instanceCfg.user} ${instanceCfg.group}" + ] + else + [ ] + ) cfg.instances + ); - # Database Configuration - write_var "SONARR__POSTGRES__HOST" "$([ -n "${cfg.db.host}" ] && echo "${cfg.db.host}" || cat "${cfg.db.hostFile}")" - write_var "SONARR__POSTGRES__USER" "$([ -n "${cfg.db.user}" ] && echo "${cfg.db.user}" || cat "${cfg.db.userFile}")" - write_var "SONARR__POSTGRES__PASSWORD" "$(cat ${cfg.db.passwordFile})" + # Create services for each enabled instance + systemd.services = mapAttrs' ( + name: instanceCfg: + nameValuePair "sonarr-${name}" ( + mkIf instanceCfg.enable { + description = "Sonarr (${name})"; + after = [ + "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 + ]; - # Final permissions - chmod 600 /run/sonarr/secrets.env - chown ${cfg.user}:${cfg.group} /run/sonarr/secrets.env - ''}"; + 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; + 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" + ]; + }) + (lib.mkIf instanceCfg.db.enable { + ExecStartPre = "+${pkgs.writeShellScript "sonarr-${name}-pre-script" '' + mkdir -p /run/sonarr-${name} + rm -f /run/sonarr-${name}/secrets.env - EnvironmentFile = ( - ["-/run/sonarr/secrets.env"] - ++ lib.optional (cfg.extraEnvVarFile != null && cfg.extraEnvVarFile != "") cfg.extraEnvVarFile - ); - }) - ]; - }; + # 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 + } - networking.firewall = mkIf cfg.openFirewall { - allowedTCPPorts = [cfg.port]; - }; + # API Key (direct value or file) + if [ -n "${instanceCfg.apiKey}" ]; then + write_var "SONARR__AUTH__APIKEY" "${instanceCfg.apiKey}" + else + write_var "SONARR__AUTH__APIKEY" "$(cat ${instanceCfg.apiKeyFile})" + fi - users.groups.${cfg.group} = {}; - users.users = mkIf (cfg.user == "sonarr") { - sonarr = { - inherit (cfg) group; - isSystemUser = true; - home = cfg.dataDir; - }; - }; + # Database Configuration + write_var "SONARR__POSTGRES__HOST" "$([ -n "${instanceCfg.db.host}" ] && echo "${instanceCfg.db.host}" || cat "${instanceCfg.db.hostFile}")" + write_var "SONARR__POSTGRES__USER" "$([ -n "${instanceCfg.db.userFile}" ] && cat "${instanceCfg.db.userFile}" || echo "${instanceCfg.db.user}")" + 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; + + # Firewall configurations + networking.firewall = mkMerge ( + mapAttrsToList ( + name: instanceCfg: + mkIf (instanceCfg.enable && instanceCfg.openFirewall) { + allowedTCPPorts = [ instanceCfg.port ]; + } + ) cfg.instances + ); + + # Users and groups + users = mkMerge ( + mapAttrsToList ( + name: instanceCfg: + mkIf instanceCfg.enable { + groups.${instanceCfg.group} = { }; + users = mkIf (instanceCfg.user == "sonarr") { + sonarr = { + inherit (instanceCfg) group; + isSystemUser = true; + # home = instanceCfg.dataDir; + home = "/nahar/sonarr"; + }; + }; + } + ) cfg.instances + ); }; }