From 1e1d27b85afdde0e22a566cd1d598df8b5e94d49 Mon Sep 17 00:00:00 2001 From: Joseph Hanson Date: Mon, 3 Feb 2025 16:13:03 -0600 Subject: [PATCH] add sonarr service and overlay sonarr/radarr unstable packages. --- nixos/hosts/shadowfax/config/sops-secrets.nix | 85 +++--- nixos/hosts/shadowfax/default.nix | 20 ++ nixos/modules/nixos/services/default.nix | 1 + .../modules/nixos/services/sonarr/default.nix | 283 ++++++++++++++++++ nixos/overlays/arr/radarr.nix | 57 ++-- nixos/overlays/arr/sonarr.nix | 55 ++-- nixos/overlays/default.nix | 21 +- 7 files changed, 415 insertions(+), 107 deletions(-) create mode 100644 nixos/modules/nixos/services/sonarr/default.nix diff --git a/nixos/hosts/shadowfax/config/sops-secrets.nix b/nixos/hosts/shadowfax/config/sops-secrets.nix index ee22b33..fc8f445 100644 --- a/nixos/hosts/shadowfax/config/sops-secrets.nix +++ b/nixos/hosts/shadowfax/config/sops-secrets.nix @@ -1,4 +1,5 @@ -{...}: { +{ ... }: +{ secrets = { # Minio "minio" = { @@ -6,107 +7,113 @@ owner = "minio"; group = "minio"; mode = "400"; - restartUnits = ["minio.service"]; + restartUnits = [ "minio.service" ]; }; # Syncthing "syncthing/publicCert" = { sopsFile = ../secrets.sops.yaml; owner = "jahanson"; mode = "400"; - restartUnits = ["syncthing.service"]; + restartUnits = [ "syncthing.service" ]; }; "syncthing/privateKey" = { sopsFile = ../secrets.sops.yaml; owner = "jahanson"; mode = "400"; - restartUnits = ["syncthing.service"]; + restartUnits = [ "syncthing.service" ]; }; # Prowlarr "arr/prowlarr/apiKey" = { sopsFile = ../secrets.sops.yaml; owner = "prowlarr"; mode = "400"; - restartUnits = ["prowlarr.service"]; + restartUnits = [ "prowlarr.service" ]; }; "arr/prowlarr/postgres/dbName" = { sopsFile = ../secrets.sops.yaml; owner = "prowlarr"; mode = "400"; - restartUnits = ["prowlarr.service"]; + restartUnits = [ "prowlarr.service" ]; }; "arr/prowlarr/postgres/user" = { sopsFile = ../secrets.sops.yaml; owner = "prowlarr"; mode = "400"; - restartUnits = ["prowlarr.service"]; + restartUnits = [ "prowlarr.service" ]; }; "arr/prowlarr/postgres/password" = { sopsFile = ../secrets.sops.yaml; owner = "prowlarr"; mode = "400"; - restartUnits = ["prowlarr.service"]; + restartUnits = [ "prowlarr.service" ]; }; "arr/prowlarr/postgres/host" = { sopsFile = ../secrets.sops.yaml; owner = "prowlarr"; mode = "400"; - restartUnits = ["prowlarr.service"]; + restartUnits = [ "prowlarr.service" ]; + }; + # Sonarr + "arr/sonarr/apiKey" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr.service" ]; + }; + "arr/sonarr/postgres/dbName" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr.service" ]; + }; + "arr/sonarr/postgres/user" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr.service" ]; + }; + "arr/sonarr/postgres/password" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr.service" ]; + }; + "arr/sonarr/postgres/host" = { + sopsFile = ../secrets.sops.yaml; + owner = "sonarr"; + mode = "400"; + restartUnits = [ "sonarr.service" ]; }; - # # Sonarr - # "arr/sonarr/apiKey" = { - # sopsFile = ../secrets.sops.yaml; - # owner = "sonarr"; - # mode = "400"; - # restartUnits = [ "sonarr.service" ]; - # }; - # "arr/sonarr/postgres/dbName" = { - # sopsFile = ../secrets.sops.yaml; - # owner = "sonarr"; - # mode = "400"; - # restartUnits = [ "sonarr.service" ]; - # }; - # "arr/sonarr/postgres/user" = { - # sopsFile = ../secrets.sops.yaml; - # owner = "sonarr"; - # mode = "400"; - # restartUnits = [ "sonarr.service" ]; - # }; - # "arr/sonarr/postgres/password" = { - # sopsFile = ../secrets.sops.yaml; - # owner = "sonarr"; - # mode = "400"; - # restartUnits = [ "sonarr.service" ]; - # }; # # Radarr "arr/radarr/apiKey" = { sopsFile = ../secrets.sops.yaml; owner = "radarr"; mode = "400"; - restartUnits = ["radarr.service"]; + restartUnits = [ "radarr.service" ]; }; "arr/radarr/postgres/dbName" = { sopsFile = ../secrets.sops.yaml; owner = "radarr"; mode = "400"; - restartUnits = ["radarr.service"]; + restartUnits = [ "radarr.service" ]; }; "arr/radarr/postgres/user" = { sopsFile = ../secrets.sops.yaml; owner = "radarr"; mode = "400"; - restartUnits = ["radarr.service"]; + restartUnits = [ "radarr.service" ]; }; "arr/radarr/postgres/password" = { sopsFile = ../secrets.sops.yaml; owner = "radarr"; mode = "400"; - restartUnits = ["radarr.service"]; + restartUnits = [ "radarr.service" ]; }; "arr/radarr/postgres/host" = { sopsFile = ../secrets.sops.yaml; owner = "radarr"; mode = "400"; - restartUnits = ["radarr.service"]; + restartUnits = [ "radarr.service" ]; }; }; } diff --git a/nixos/hosts/shadowfax/default.nix b/nixos/hosts/shadowfax/default.nix index 386f863..355b6c0 100644 --- a/nixos/hosts/shadowfax/default.nix +++ b/nixos/hosts/shadowfax/default.nix @@ -252,6 +252,26 @@ in passwordFile = config.sops.secrets."arr/radarr/postgres/password".path; }; }; + # Sonarr + sonarr = { + enable = true; + package = pkgs.unstable.sonarr; + dataDir = "/nahar/sonarr"; + 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; + }; + }; # Sabnzbd sabnzbd = { enable = true; diff --git a/nixos/modules/nixos/services/default.nix b/nixos/modules/nixos/services/default.nix index 7d0cba4..7bed208 100644 --- a/nixos/modules/nixos/services/default.nix +++ b/nixos/modules/nixos/services/default.nix @@ -16,6 +16,7 @@ ./reboot-required-check.nix ./sabnzbd ./sanoid + ./sonarr ./syncthing ./zfs-nightly-snap ]; diff --git a/nixos/modules/nixos/services/sonarr/default.nix b/nixos/modules/nixos/services/sonarr/default.nix new file mode 100644 index 0000000..1f1af9b --- /dev/null +++ b/nixos/modules/nixos/services/sonarr/default.nix @@ -0,0 +1,283 @@ +{ + config, + pkgs, + lib, + utils, + ... +}: +with lib; +let + cfg = config.mySystem.services.sonarr; + dbOptions = { + options = { + enable = mkEnableOption "Database configuration for sonarr"; + host = mkOption { + type = types.str; + default = ""; + example = "127.0.0.1"; + description = "Direct database host (mutually exclusive with hostFile)"; + }; + hostFile = mkOption { + type = types.str; + default = ""; + example = "/run/secrets/sonarr_db_host"; + description = "Database host from a file (mutually exclusive with host)"; + }; + port = mkOption { + type = types.port; + default = "5432"; + description = "Database port"; + }; + user = mkOption { + type = types.str; + default = "sonarr"; + description = "Direct database user (mutually exclusive with userFile)"; + }; + userFile = mkOption { + type = types.str; + default = ""; + example = "/run/secrets/sonarr_db_user"; + description = "Database user from a file (mutually exclusive with user)"; + }; + passwordFile = mkOption { + type = types.path; + default = "/run/secrets/sonarr_db_password"; + description = "Database password from a file (always used)"; + }; + dbname = mkOption { + type = types.str; + default = "sonarr_main"; + description = "Database name"; + }; + }; + }; +in +{ + options.mySystem.services.sonarr = { + enable = mkEnableOption "Sonarr"; + + package = mkPackageOption pkgs "Sonarr" { }; + + user = mkOption { + type = types.str; + default = "sonarr"; + description = "User account under which sonarr runs."; + }; + + group = mkOption { + type = types.str; + default = "sonarr"; + description = "Group under which sonarr runs."; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/sonarr"; + description = "Storage directory for sonarr data"; + }; + + tvDir = mkOption { + type = types.path; + default = "/mnt/media/tv"; + description = "Directory where tv shows are stored"; + }; + + port = mkOption { + type = types.port; + default = 8989; + description = "Port for sonarr web interface"; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open firewall ports for sonarr"; + }; + + hardening = mkOption { + type = types.bool; + default = true; + description = "Enable security hardening features"; + }; + + apiKey = mkOption { + type = types.str; + default = ""; + example = "abc123"; + description = "Direct API key for sonarr (mutually exclusive with apiKeyFile)"; + }; + + apiKeyFile = mkOption { + type = types.path; + default = "/run/secrets/sonarr_api_key"; + description = "API key for sonarr from a file (mutually exclusive with apiKey)"; + }; + + 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."; + }; + }; + + 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; + }) + ]; + + 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" + ]; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = [ + "uts" + "ipc" + "pid" + "user" + "cgroup" + "net" + ]; + 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 + } + + # 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 + + # 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})" + + # Final permissions + chmod 600 /run/sonarr/secrets.env + chown ${cfg.user}:${cfg.group} /run/sonarr/secrets.env + ''}"; + + EnvironmentFile = [ "-/run/sonarr/secrets.env" ]; + }) + ]; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; + + users.groups.${cfg.group} = { }; + users.users = mkIf (cfg.user == "sonarr") { + sonarr = { + inherit (cfg) group; + isSystemUser = true; + home = cfg.dataDir; + }; + }; + }; +} diff --git a/nixos/overlays/arr/radarr.nix b/nixos/overlays/arr/radarr.nix index 15a4963..7fb6f4b 100644 --- a/nixos/overlays/arr/radarr.nix +++ b/nixos/overlays/arr/radarr.nix @@ -12,11 +12,9 @@ openssl, nixosTests, zlib, -}: let - os = - if stdenv.hostPlatform.isDarwin - then "osx" - else "linux"; +}: +let + os = if stdenv.hostPlatform.isDarwin then "osx" else "linux"; arch = { x86_64-linux = "x64"; @@ -24,8 +22,7 @@ x86_64-darwin = "x64"; aarch64-darwin = "arm64"; } - ."${stdenv.hostPlatform.system}" - or (throw "Unsupported system: ${stdenv.hostPlatform.system}"); + ."${stdenv.hostPlatform.system}" or (throw "Unsupported system: ${stdenv.hostPlatform.system}"); hash = { @@ -36,26 +33,26 @@ } ."${arch}-${os}_hash"; in - stdenv.mkDerivation rec { - pname = "radarr"; - version = "5.17.2.9580"; +stdenv.mkDerivation rec { + pname = "radarr"; + version = "5.17.2.9580"; - src = fetchurl { - url = "https://github.com/Radarr/Radarr/releases/download/v${version}/Radarr.master.${version}.${os}-core-${arch}.tar.gz"; - sha256 = hash; - }; + src = fetchurl { + url = "https://github.com/Radarr/Radarr/releases/download/v${version}/Radarr.master.${version}.${os}-core-${arch}.tar.gz"; + sha256 = hash; + }; - nativeBuildInputs = [makeWrapper]; + nativeBuildInputs = [ makeWrapper ]; - installPhase = '' - runHook preInstall + installPhase = '' + runHook preInstall - mkdir -p $out/{bin,share/${pname}-${version}} - cp -r * $out/share/${pname}-${version}/. + mkdir -p $out/{bin,share/${pname}-${version}} + cp -r * $out/share/${pname}-${version}/. - makeWrapper "${dotnet-runtime}/bin/dotnet" $out/bin/Radarr \ - --add-flags "$out/share/${pname}-${version}/Radarr.dll" \ - --prefix LD_LIBRARY_PATH : ${ + makeWrapper "${dotnet-runtime}/bin/dotnet" $out/bin/Radarr \ + --add-flags "$out/share/${pname}-${version}/Radarr.dll" \ + --prefix LD_LIBRARY_PATH : ${ lib.makeLibraryPath [ curl sqlite @@ -67,13 +64,13 @@ in ] } - runHook postInstall - ''; + runHook postInstall + ''; - passthru = { - updateScript = ./update.sh; - tests.smoke-test = nixosTests.radarr; - }; + passthru = { + updateScript = ./update.sh; + tests.smoke-test = nixosTests.radarr; + }; - mainProgram = "Radarr"; - } + meta.mainProgram = "Radarr"; +} diff --git a/nixos/overlays/arr/sonarr.nix b/nixos/overlays/arr/sonarr.nix index b6aba8a..fe0a1ac 100644 --- a/nixos/overlays/arr/sonarr.nix +++ b/nixos/overlays/arr/sonarr.nix @@ -12,11 +12,9 @@ openssl, nixosTests, zlib, -}: let - os = - if stdenv.hostPlatform.isDarwin - then "osx" - else "linux"; +}: +let + os = if stdenv.hostPlatform.isDarwin then "osx" else "linux"; arch = { x86_64-linux = "x64"; @@ -24,8 +22,7 @@ x86_64-darwin = "x64"; aarch64-darwin = "arm64"; } - ."${stdenv.hostPlatform.system}" - or (throw "Unsupported system: ${stdenv.hostPlatform.system}"); + ."${stdenv.hostPlatform.system}" or (throw "Unsupported system: ${stdenv.hostPlatform.system}"); hash = { @@ -36,26 +33,26 @@ } ."${arch}-${os}_hash"; in - stdenv.mkDerivation rec { - pname = "sonarr"; - version = "4.0.12.2823"; +stdenv.mkDerivation rec { + pname = "sonarr"; + version = "4.0.12.2823"; - src = fetchurl { - url = "https://github.com/Sonarr/Sonarr/releases/download/v${version}/Sonarr.main.${version}.${os}-${arch}.tar.gz"; - sha256 = hash; - }; + src = fetchurl { + url = "https://github.com/Sonarr/Sonarr/releases/download/v${version}/Sonarr.main.${version}.${os}-${arch}.tar.gz"; + sha256 = hash; + }; - nativeBuildInputs = [makeWrapper]; + nativeBuildInputs = [ makeWrapper ]; - installPhase = '' - runHook preInstall + installPhase = '' + runHook preInstall - mkdir -p $out/{bin,share/${pname}-${version}} - cp -r * $out/share/${pname}-${version}/. + mkdir -p $out/{bin,share/${pname}-${version}} + cp -r * $out/share/${pname}-${version}/. - makeWrapper "${dotnet-runtime}/bin/dotnet" $out/bin/Sonarr \ - --add-flags "$out/share/${pname}-${version}/Sonarr.dll" \ - --prefix LD_LIBRARY_PATH : ${ + makeWrapper "${dotnet-runtime}/bin/dotnet" $out/bin/Sonarr \ + --add-flags "$out/share/${pname}-${version}/Sonarr.dll" \ + --prefix LD_LIBRARY_PATH : ${ lib.makeLibraryPath [ curl sqlite @@ -67,11 +64,11 @@ in ] } - runHook postInstall - ''; - passthru = { - tests.smoke-test = nixosTests.radarr; - }; + runHook postInstall + ''; + passthru = { + tests.smoke-test = nixosTests.radarr; + }; - mainProgram = "Sonarr"; - } + meta.mainProgram = "Sonarr"; +} diff --git a/nixos/overlays/default.nix b/nixos/overlays/default.nix index d9fc3bd..dd21ba8 100644 --- a/nixos/overlays/default.nix +++ b/nixos/overlays/default.nix @@ -1,10 +1,12 @@ -{inputs, ...}: let +{ inputs, ... }: +let # smartmontoolsOverlay = import ./smartmontools { }; # vivaldiOverlay = self: super: { vivaldi = super.callPackage ./vivaldi { }; }; - coderOverlay = self: super: {coder = super.callPackage ./coder {};}; - modsOverlay = self: super: {mods = super.callPackage ./charm-mods {};}; - termiusOverlay = self: super: {termius = super.callPackage ./termius {};}; -in { + coderOverlay = self: super: { coder = super.callPackage ./coder { }; }; + modsOverlay = self: super: { mods = super.callPackage ./charm-mods { }; }; + termiusOverlay = self: super: { termius = super.callPackage ./termius { }; }; +in +{ # smartmontools = smartmontoolsOverlay; # vivaldi = vivaldiOverlay; coder = coderOverlay; @@ -25,16 +27,17 @@ in { // { # Add talosctl to the unstable set talosctl = final.unstable.callPackage ./talosctl { - inherit - (final.unstable) + inherit (final.unstable) lib buildGoModule fetchFromGitHub installShellFiles ; }; - xpipe = final.unstable.callPackage ./xpipe/ptb.nix {}; - prowlarr = final.unstable.callPackage ./arr/prowlarr.nix {}; + xpipe = final.unstable.callPackage ./xpipe/ptb.nix { }; + prowlarr = final.unstable.callPackage ./arr/prowlarr.nix { }; + radarr = final.unstable.callPackage ./arr/radarr.nix { }; + sonarr = final.unstable.callPackage ./arr/sonarr.nix { }; }; }; }