1
0
Fork 0
selfhostblocks/modules/arr.nix
ibizaman e5110cace6 add xml config generator for radarr
This is cool but also needed because we now must set the authentication method to "External" for
radarr to be happy with our SSO integration.
2023-11-15 12:46:46 -08:00

336 lines
10 KiB
Nix

{ config, pkgs, lib, ... }:
let
cfg = config.shb.arr;
apps = {
radarr = {
defaultPort = 7001;
settingsFormat = formatXML {};
moreOptions = {
settings = lib.mkOption {
default = {};
type = lib.types.submodule {
freeformType = apps.radarr.settingsFormat.type;
options = {
APIKeyFile = lib.mkOption {
type = lib.types.path;
};
LogLevel = lib.mkOption {
type = lib.types.enum ["debug" "info"];
default = "info";
};
};
};
};
};
};
sonarr = {
defaultPort = 8989;
};
bazarr = {
defaultPort = 6767;
};
readarr = {
defaultPort = 8787;
};
lidarr = {
defaultPort = 8686;
};
jackett = {
defaultPort = 9117;
settingsFormat = pkgs.formats.json {};
moreOptions = {
settings = lib.mkOption {
default = {};
type = lib.types.submodule {
freeformType = apps.jackett.settingsFormat.type;
options = {
APIKeyFile = lib.mkOption {
type = lib.types.path;
};
FlareSolverrUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
OmdbApiKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
};
ProxyType = lib.mkOption {
type = lib.types.enum [ "-1" "0" "1" "2" ];
default = "0";
description = ''
-1 = disabled
0 = HTTP
1 = SOCKS4
2 = SOCKS5
'';
};
ProxyUrl = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
ProxyPort = lib.mkOption {
type = lib.types.nullOr lib.types.port;
default = null;
};
};
};
};
};
};
};
formatXML = {}: {
type = with lib.types; let
valueType = nullOr (oneOf [
bool
int
float
str
path
(attrsOf valueType)
(listOf valueType)
]) // {
description = "XML value";
};
in valueType;
generate = name: value: pkgs.callPackage ({ runCommand, python3 }: runCommand name {
value = builtins.toJSON {Config = value;};
passAsFile = [ "value" ];
} (pkgs.writers.writePython3 "dict2xml" {
libraries = with python3.pkgs; [ python dict2xml ];
} ''
import os
import json
from dict2xml import dict2xml
with open(os.environ["valuePath"]) as f:
content = json.loads(f.read())
if content is None:
print("Could not parse env var valuePath as json")
os.exit(2)
with open(os.environ["out"], "w") as out:
out.write(dict2xml(content))
'')) {};
};
appOption = name: c: lib.nameValuePair name (lib.mkOption {
description = "Configuration for ${name}";
default = {};
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "selfhostblocks.${name}";
subdomain = lib.mkOption {
type = lib.types.str;
description = "Subdomain under which ${name} will be served.";
example = name;
};
domain = lib.mkOption {
type = lib.types.str;
description = "Domain under which ${name} will be served.";
example = "mydomain.com";
};
port = lib.mkOption {
type = lib.types.port;
description = "Port on which ${name} listens to incoming requests.";
default = c.defaultPort;
};
dataDir = lib.mkOption {
type = lib.types.str;
description = "Directory where state of ${name} is stored.";
default = "/var/lib/${name}";
};
oidcEndpoint = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "OIDC endpoint for SSO";
example = "https://authelia.example.com";
};
} // (c.moreOptions or {});
};
});
template = file: newPath: replacements:
let
templatePath = newPath + ".template";
sedPatterns = lib.strings.concatStringsSep " " (lib.attrsets.mapAttrsToList (from: to: "-e \"s|${from}|${to}|\"") replacements);
in
''
ln -fs ${file} ${templatePath}
rm ${newPath} || :
sed ${sedPatterns} ${templatePath} > ${newPath}
'';
in
{
options.shb.arr = lib.listToAttrs (lib.mapAttrsToList appOption apps);
config = lib.mkMerge ([
{
services.radarr = lib.mkIf cfg.radarr.enable {
enable = true;
dataDir = "/var/lib/radarr";
};
users.users.radarr = lib.mkIf cfg.radarr.enable {
extraGroups = [ "media" ];
};
shb.arr.radarr.settings = lib.mkIf cfg.radarr.enable {
Port = config.shb.arr.radarr.port;
BindAddress = "127.0.0.1";
UrlBase = "";
EnableSsl = "false";
AuthenticationMethod = "External";
AuthenticationRequired = "Enabled";
};
systemd.services.radarr.preStart =
let
s = cfg.radarr.settings;
templatedfileSettings =
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
ApiKey = "%APIKEY%";
};
templatedSettings = (removeAttrs s [ "APIKeyFile" ]) // templatedfileSettings;
t = template (apps.radarr.settingsFormat.generate "
config.xml" templatedSettings) "${config.services.radarr.dataDir}/config.xml" (
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
"%APIKEY%" = "$(cat ${s.APIKeyFile})";
}
);
in
lib.mkIf cfg.radarr.enable t;
# Listens on port 8989
services.sonarr = lib.mkIf cfg.sonarr.enable {
enable = true;
dataDir = "/var/lib/sonarr";
};
users.users.sonarr = lib.mkIf cfg.sonarr.enable {
extraGroups = [ "media" ];
};
services.bazarr = lib.mkIf cfg.bazarr.enable {
enable = true;
listenPort = cfg.bazarr.port;
};
users.users.bazarr = lib.mkIf cfg.bazarr.enable {
extraGroups = [ "media" ];
};
# Listens on port 8787
services.readarr = lib.mkIf cfg.readarr.enable {
enable = true;
dataDir = "/var/lib/readarr";
};
users.users.readarr = lib.mkIf cfg.readarr.enable {
extraGroups = [ "media" ];
};
# Listens on port 8686
services.lidarr = lib.mkIf cfg.lidarr.enable {
enable = true;
dataDir = "/var/lib/lidarr";
};
users.users.lidarr = lib.mkIf cfg.lidarr.enable {
extraGroups = [ "media" ];
};
# Listens on port 9117
services.jackett = lib.mkIf cfg.jackett.enable {
enable = true;
dataDir = "/var/lib/jackett";
};
shb.arr.jackett.settings = lib.mkIf cfg.jackett.enable {
Port = config.shb.arr.jackett.port;
AllowExternal = "false";
UpdateDisabled = "true";
};
users.users.jackett = lib.mkIf cfg.jackett.enable {
extraGroups = [ "media" ];
};
systemd.services.jackett.preStart =
let
s = cfg.jackett.settings;
templatedfileSettings =
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
APIKey = "%APIKEY%";
} // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) {
OmdbApiKey = "%OMDBAPIKEY%";
};
templatedSettings = (removeAttrs s [ "APIKeyFile" "OmdbApiKeyFile" ]) // templatedfileSettings;
t = template (apps.jackett.settingsFormat.generate "jackett.json" templatedSettings) "${config.services.jackett.dataDir}/ServerConfig.json" (
lib.optionalAttrs (!(isNull s.APIKeyFile)) {
"%APIKEY%" = "$(cat ${s.APIKeyFile})";
} // lib.optionalAttrs (!(isNull s.OmdbApiKeyFile)) {
"%OMDBAPIKEY%" = "$(cat ${s.OmdbApiKeyFile})";
}
);
in
lib.mkIf cfg.jackett.enable t;
shb.nginx.autheliaProtect =
let
appProtectConfig = name: _defaults:
let
c = cfg.${name};
in
lib.mkIf (c.oidcEndpoint != null) {
inherit (c) subdomain domain oidcEndpoint;
upstream = "http://127.0.0.1:${toString c.port}";
autheliaRules = [
{
domain = "${c.subdomain}.${c.domain}";
policy = "bypass";
resources = [
"^/api.*"
] ++ lib.optionals (name == "jackett") [
"^/dl.*"
];
}
{
domain = "${c.subdomain}.${c.domain}";
policy = "two_factor";
subject = ["group:arr_user"];
}
];
};
in
lib.mapAttrsToList appProtectConfig apps;
shb.backup.instances =
let
backupConfig = name: _defaults: {
${name} = {
sourceDirectories = [
config.shb.arr.${name}.dataDir
];
excludePatterns = [".db-shm" ".db-wal" ".mono"];
};
};
in
lib.mkMerge (lib.mapAttrsToList backupConfig apps);
}
] ++ map (name: lib.mkIf cfg.${name}.enable {
systemd.tmpfiles.rules = lib.mkIf (lib.hasAttr "dataDir" config.services.${name}) [
"d '${config.services.${name}.dataDir}' 0750 ${config.services.${name}.user} ${config.services.${name}.group} - -"
];
users.groups.${name} = {
members = [ "backup" ];
};
systemd.services.${name}.serviceConfig = {
# Setup permissions needed for backups, as the backup user is member of the jellyfin group.
UMask = lib.mkForce "0027";
StateDirectoryMode = lib.mkForce "0750";
};
}) (lib.attrNames apps));
}