Adds services.arrInit.<name>.configXml for declaratively ensuring XML elements exist in a Servarr config.xml before the service starts. Generates a preStart hook on the main service that runs a Python helper to patch or create config.xml. Undeclared elements are preserved; declared elements are written with exact values. Primary use case: preventing recurring Prowlarr 'not listening on port' failures when config.xml loses the <Port> element — now guaranteed to exist before Prowlarr starts. Hardening: - Atomic writes (tmp + rename): power loss cannot corrupt config.xml - Malformed XML recovery: fresh <Config> root instead of blocking boot - Secure default mode (0600) for new files containing ApiKey - Preserves existing file mode on rewrite - Assertion against duplicate serviceName targeting Tests (10 subtests): creates-from-missing, patches-existing, preserves- undeclared, corrects-tampered, idempotent, malformed-recovery, ownership-preserved, not-world-readable.
192 lines
5.4 KiB
Nix
192 lines
5.4 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
cfg = config.services.jellyseerrInit;
|
|
|
|
scriptDir = ../scripts;
|
|
|
|
pythonEnv = pkgs.python3.withPackages (
|
|
ps: with ps; [
|
|
pyyaml
|
|
requests
|
|
]
|
|
);
|
|
|
|
jellyseerrProviderModule = lib.types.submodule {
|
|
options = {
|
|
profileName = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Quality profile name to set as the default. Resolved to an ID at runtime by querying the Servarr API.";
|
|
example = "Remux + WEB 2160p";
|
|
};
|
|
|
|
dataDir = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Path to the Servarr application data directory containing config.xml.";
|
|
example = "/services/radarr";
|
|
};
|
|
|
|
bindAddress = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "127.0.0.1";
|
|
description = "IP address the Servarr application API is listening on.";
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
description = "API port of the Servarr application.";
|
|
example = 7878;
|
|
};
|
|
|
|
serviceName = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Name of the systemd service to depend on.";
|
|
example = "radarr";
|
|
};
|
|
};
|
|
};
|
|
|
|
jellyseerrInitModule = lib.types.submodule {
|
|
options = {
|
|
enable = lib.mkEnableOption "Jellyseerr quality profile initialization";
|
|
|
|
configDir = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Path to Jellyseerr's data directory containing settings.json.";
|
|
example = "/services/jellyseerr";
|
|
};
|
|
|
|
apiTimeout = lib.mkOption {
|
|
type = lib.types.ints.positive;
|
|
default = 90;
|
|
description = "Seconds to wait for Radarr/Sonarr APIs to become available.";
|
|
};
|
|
|
|
radarr = lib.mkOption {
|
|
type = jellyseerrProviderModule;
|
|
description = "Radarr quality profile configuration for Jellyseerr.";
|
|
};
|
|
|
|
sonarr = lib.mkOption {
|
|
type = lib.types.submodule {
|
|
options = {
|
|
profileName = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Quality profile name for TV series.";
|
|
example = "WEB-2160p";
|
|
};
|
|
|
|
animeProfileName = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.str;
|
|
default = null;
|
|
description = "Quality profile name for anime. Defaults to profileName when null.";
|
|
};
|
|
|
|
dataDir = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Path to the Sonarr data directory containing config.xml.";
|
|
example = "/services/sonarr";
|
|
};
|
|
|
|
bindAddress = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "127.0.0.1";
|
|
description = "IP address the Sonarr API is listening on.";
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
description = "API port of Sonarr.";
|
|
example = 8989;
|
|
};
|
|
|
|
serviceName = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Name of the systemd service to depend on.";
|
|
example = "sonarr";
|
|
};
|
|
};
|
|
};
|
|
description = "Sonarr quality profile configuration for Jellyseerr.";
|
|
};
|
|
};
|
|
};
|
|
|
|
mkJellyseerrInitConfig = builtins.toJSON {
|
|
configDir = cfg.configDir;
|
|
apiTimeout = cfg.apiTimeout;
|
|
radarr = {
|
|
profileName = cfg.radarr.profileName;
|
|
dataDir = cfg.radarr.dataDir;
|
|
bindAddress = cfg.radarr.bindAddress;
|
|
port = cfg.radarr.port;
|
|
};
|
|
sonarr = {
|
|
profileName = cfg.sonarr.profileName;
|
|
animeProfileName =
|
|
if cfg.sonarr.animeProfileName != null then cfg.sonarr.animeProfileName else cfg.sonarr.profileName;
|
|
dataDir = cfg.sonarr.dataDir;
|
|
bindAddress = cfg.sonarr.bindAddress;
|
|
port = cfg.sonarr.port;
|
|
};
|
|
};
|
|
|
|
configFile = pkgs.writeText "jellyseerr-init-config.json" mkJellyseerrInitConfig;
|
|
|
|
jellyseerrDeps = [
|
|
"jellyseerr.service"
|
|
"${cfg.radarr.serviceName}.service"
|
|
"${cfg.sonarr.serviceName}.service"
|
|
];
|
|
|
|
hardeningConfig = {
|
|
PrivateTmp = true;
|
|
NoNewPrivileges = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
RestrictSUIDSGID = true;
|
|
ProtectHome = true;
|
|
SystemCallArchitectures = "native";
|
|
};
|
|
in
|
|
{
|
|
options.services.jellyseerrInit = lib.mkOption {
|
|
type = jellyseerrInitModule;
|
|
default = {
|
|
enable = false;
|
|
};
|
|
description = ''
|
|
Jellyseerr quality profile initialization.
|
|
Patches Jellyseerr's settings.json so new requests default to the
|
|
correct Radarr/Sonarr quality profiles, resolved by name at runtime.
|
|
'';
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
systemd.services.jellyseerr-init = {
|
|
description = "Initialize Jellyseerr quality profile defaults";
|
|
after = jellyseerrDeps;
|
|
requires = jellyseerrDeps;
|
|
wantedBy = [ "multi-user.target" ];
|
|
environment.PYTHONPATH = "${scriptDir}";
|
|
unitConfig = {
|
|
StartLimitIntervalSec = 5 * (cfg.apiTimeout + 30);
|
|
StartLimitBurst = 5;
|
|
};
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
RemainAfterExit = true;
|
|
Restart = "on-failure";
|
|
RestartSec = 30;
|
|
ExecStart = "${pythonEnv}/bin/python3 ${scriptDir}/jellyseerr_init.py ${configFile}";
|
|
}
|
|
// hardeningConfig;
|
|
};
|
|
};
|
|
}
|