diff --git a/nixos/modules/services/networking/sabnzbd/default.nix b/nixos/modules/services/networking/sabnzbd/default.nix index 3f94eebfc9d9..013c87834674 100644 --- a/nixos/modules/services/networking/sabnzbd/default.nix +++ b/nixos/modules/services/networking/sabnzbd/default.nix @@ -5,19 +5,108 @@ ... }: -with lib; - let + inherit (lib) + mkOption + mkPackageOption + mkEnableOption + mkOptionDefault + mkIf + literalExpression + optionalString + types + ; + inherit (lib.generators) + mkKeyValueDefault + mkValueStringDefault + toKeyValue + ; + + enumFromAttrs = + enum_values: + types.coercedTo (types.enum (lib.attrNames enum_values)) (name: enum_values.${name}) ( + types.enum (lib.attrValues enum_values) + ); cfg = config.services.sabnzbd; - inherit (pkgs) sabnzbd; + mandatoryGlobalSettings = { + "__version__" = 19; + "__encoding__" = "utf-8"; + }; + allSettings = cfg.settings // mandatoryGlobalSettings; + + # sabnzbd uses configobj type inis, which support + # nested sections specified by increasing numbers + # of square brackets (but not toml style dotted paths) + configObjAtom = types.oneOf [ + types.str + types.int + types.bool + ]; + + configObjValue = + let + valueType = + types.oneOf [ + types.str + types.int + types.bool + (types.listOf configObjAtom) + (types.attrsOf valueType) + ] + // { + description = "ConfigObj type"; + }; + in + valueType; + + configObjIni = + { }: + let + extractAtoms = lib.filterAttrs (k: v: v != null && !lib.isAttrs v); + extractSections = lib.filterAttrs (k: v: lib.isAttrs v); + mkValueString = ( + v: + if true == v then + "1" + else if false == v then + "0" + else + mkValueStringDefault { } v + ); + mkKeyValue = mkKeyValueDefault { inherit mkValueString; } "="; + mkSection = ( + depth: attrs: + let + atoms = extractAtoms attrs; + sections = extractSections attrs; + sectionHeadingLeft = lib.concatStrings (lib.replicate (depth + 1) "["); + sectionHeadingRight = lib.concatStrings (lib.replicate (depth + 1) "]"); + mkSectionHeading = + name: "${sectionHeadingLeft}${lib.escape [ "[" "]" ] name}${sectionHeadingRight}"; + mkSubsection = name: attrs: (mkSectionHeading name) + "\n" + (mkSection (depth + 1) attrs) + "\n"; + in + toKeyValue { inherit mkKeyValue; } (extractAtoms attrs) + + "\n" + + lib.concatStrings (lib.mapAttrsToList mkSubsection sections) + ); + in + { + type = types.attrsOf configObjValue; + generate = name: attrs: pkgs.writeText name (mkSection 0 attrs); + }; + + publicSettingsIni = + if cfg.configFile != null then + cfg.configFile + else + (configObjIni { }).generate "public-settings.ini" allSettings; + + sabnzbdIniPath = "/var/lib/${cfg.stateDir}/sabnzbd.ini"; in { - - ###### interface - options = { services.sabnzbd = { enable = mkEnableOption "the sabnzbd server"; @@ -25,9 +114,15 @@ in package = mkPackageOption pkgs "sabnzbd" { }; configFile = mkOption { - type = types.path; - default = "/var/lib/sabnzbd/sabnzbd.ini"; - description = "Path to config file."; + type = types.nullOr types.path; + default = null; + description = "Path to config file (deprecated, use `settings` instead)"; + }; + + stateDir = mkOption { + type = types.str; + default = "sabnzbd"; + description = "State directory of the service under /var/lib/"; }; user = mkOption { @@ -49,12 +144,329 @@ in Open ports in the firewall for the sabnzbd web interface ''; }; + + settings = mkOption { + description = '' + The sabnzbd configuration (see also + [sabnzbd's wiki](https://sabnzbd.org/wiki/configuration/4.5/configure) + for extra documentation) + ''; + default = { }; + type = types.submodule { + freeformType = (configObjIni { }).type; + config = { + misc = { + config_conversion_version = mkOptionDefault 4; + # config_lock = 1 turns the alert that the config is read-only from an error + # into a warning. But the warnings still come, and additionally read access + # to the config from the web ui is blocked as well, so better keep it at 0 + # and live with the error + # optionally, misc.helpful_warnings = 0 will silence the warnings (but not the error) + # at the cost of also silencing other, potentially useful warnings + # config_lock = mkOptionDefault (if !cfg.allowConfigWrite then 1 else 0); + config_lock = mkOptionDefault false; + notified_new_skin = mkOptionDefault true; + # don't open the browser on a daemonized service + auto_browser = mkOptionDefault false; + # don't check for new updates since we're using the distro version + check_new_rel = mkOptionDefault false; + }; + }; + options = { + misc = { + bandwidth_max = mkOption { + type = types.str; + description = '' + Maximum bandwidth in bytes(!)/sec (supports prefixes). Use + in conjunction with `bandwidth_perc` to set a bandwidth + limit. Empty string disables limit. + ''; + default = ""; + example = "50MB/s"; + }; + bandwidth_perc = mkOption { + type = types.int; + description = '' + Percentage of `bandwidth_max` that sabnzbd is allowed to use. + 0 means no limit. + ''; + default = 0; + example = 50; + }; + host = mkOption { + type = types.str; + description = '' + Address for the Web UI to listen on for incoming connections. + ''; + default = "127.0.0.1"; + example = "0.0.0.0"; + }; + port = mkOption { + type = types.port; + description = '' + Port for the Web UI to listen on for incoming connections. + ''; + default = 8080; + example = 12345; + }; + https_cert = mkOption { + type = types.nullOr types.path; + description = '' + Path to the TLS certificate for the web UI. If not set + and https is enabled, a self-signed certificate will + be generated. + ''; + default = null; + example = literalExpression "\${config.acme.certs.\${domain}.directory}/fullchain.pem"; + }; + https_key = mkOption { + type = types.nullOr types.path; + description = '' + Path to the TLS key for the web UI. If not set and + https is enabled, a self-signed certificate will be + generated + ''; + default = null; + example = literalExpression "\${config.acme.certs.\${domain}.directory}/key.pem"; + }; + enable_https = mkOption { + type = types.bool; + description = "Whether to enable HTTPS for the web UI"; + default = cfg.settings.misc.https_cert != null; + defaultText = "cfg.settings.misc.https_cert != null"; + example = true; + }; + cache_limit = mkOption { + type = types.str; + description = '' + Size of the RAM cache, in bytes (prefixes supported). + Sabnzbd recommends 25% of available RAM. Empty means + no cache. + ''; + default = ""; + example = "500M"; + }; + html_login = mkOption { + type = types.bool; + description = '' + Prompt for login with an html login mask if enabled, + otherwise prompt for basic auth (useful for SSO) + ''; + default = true; + }; + inet_exposure = mkOption { + type = enumFromAttrs { + "none" = 0; + "api (add nzbs)" = 1; + "api (no config)" = 2; + "api (full)" = 3; + "api+web (auth needed)" = 4; + "api+web (locally no auth)" = 5; + }; + description = '' + Restrictions for access from non-local IP addresses. + Values are: + 0, 'none' -- no access + 1, 'api (add nzbs)' -- api access only, only add nzb files + 2, 'api (no config)' -- api access only, config changes not allowed + 3, 'api (full)' -- api access only, full api access + 4, 'api+web (auth needed)' -- api and web ui, login required always + 5, 'api+web (locally no auth)' -- api and web ui, login required from non-local IPs only + ''; + default = "none"; + }; + email_endjob = mkOption { + type = enumFromAttrs { + "never" = 0; + "always" = 1; + "on error" = 2; + }; + description = '' + Whether to send emails on job completion. Values are: + 0, 'never' -- Never + 1, 'always' -- Always + 2, 'on error' -- On error + ''; + default = if cfg.settings.misc.email_server != "" then "on error" else "never"; + defaultText = ''if cfg.settings.misc.email_server != "" then "on error" else "never"''; + }; + email_full = mkOption { + type = types.bool; + description = "Whether to send alerts for full disks"; + default = cfg.settings.misc.email_server != ""; + defaultText = ''cfg.settings.misc.email_server != ""''; + }; + email_rss = mkOption { + type = types.bool; + description = "Whether to send alerts for jobs added by RSS feeds"; + default = false; + }; + email_server = mkOption { + type = types.str; + description = "SMTP server for email alerts (server:host)"; + default = ""; + }; + email_to = mkOption { + type = types.str; + description = "Receiving address for email alerts"; + default = ""; + }; + email_from = mkOption { + type = types.str; + description = "'From:' field for emails (needs to be an address)"; + default = ""; + }; + }; + ntfosd = mkOption { + default = { }; + description = "NotifyOSD settings"; + type = types.submodule { + freeformType = (configObjIni { }).type; + options = { + ntfosd_enable = mkOption { + type = types.bool; + description = '' + Whether to enable NotifyOSD alerts. Does not really make sense + in a server environment, hence we default to false despite + upstream's default true. + ''; + default = false; + }; + }; + }; + }; + servers = mkOption { + default = { }; + description = "Usenet provider specification"; + type = types.attrsOf ( + types.submodule { + freeformType = (configObjIni { }).type; + options = { + enable = mkOption { + type = types.bool; + description = "Enable this server by default"; + default = true; + example = false; + }; + required = mkOption { + type = types.bool; + description = '' + In case of connection failures, wait for the + server to come back online instead of skipping + it. + ''; + default = false; + example = true; + }; + optional = mkOption { + type = types.bool; + description = '' + In case of connection failures, temporarily + disable this server. (See sabnzbd's documentation + for usage guides). + ''; + default = false; + example = true; + }; + priority = mkOption { + type = types.int; + description = '' + Priority of this servers. Servers are queried in + order of priority, from highest (0) to lowest (100). + ''; + default = 0; + }; + name = mkOption { + type = types.str; + description = '' + The name of the server + ''; + example = "Example News Provider"; + }; + displayname = mkOption { + type = types.str; + description = '' + Human-friendly description of the server + ''; + example = "Example News Provider"; + }; + host = mkOption { + type = types.str; + description = '' + Hostname of the server + ''; + example = "news.example.com"; + }; + port = mkOption { + type = types.port; + description = "Port of the server"; + example = 443; + default = 563; + }; + connections = mkOption { + type = types.int; + description = '' + Number of parallel connections permitted by + the server. + ''; + example = 50; + default = 8; + }; + timeout = mkOption { + type = types.int; + description = '' + Time, in seconds, to wait for a response before + attempting error recovery. + ''; + default = 60; + }; + ssl = mkOption { + type = types.bool; + description = '' + Whether the server supports TLS + ''; + default = true; + }; + ssl_verify = mkOption { + type = enumFromAttrs { + "strict" = 3; + "allow injection" = 2; + "none" = 0; + }; + description = '' + Level of TLS verification. Supported values: + 3, 'strict' -- strict (normal) verification + 2, 'allow injection' -- allow locally injected certificates + 0, 'none' -- no verification + ''; + default = "strict"; + }; + expire_date = mkOption { + type = types.nullOr types.str; + description = '' + If Notifications are enabled and an expiry date is + set, warn 5 days before expiry. This setting + does not automatically disable the server. + Expected format: yyyy-mm-dd + ''; + default = null; + }; + }; + } + ); + }; + }; + }; + }; }; }; - ###### implementation - config = mkIf cfg.enable { + warnings = lib.optional (cfg.configFile != null) '' + `sabnzbd.configFile` is deprecated, consider using `sabnzbd.settings` instead. + If you have values set in `sabnzbd.settings` set, they will be ignored. + ''; + users.users = mkIf (cfg.user == "sabnzbd") { sabnzbd = { isSystemUser = true; @@ -67,22 +479,26 @@ in sabnzbd = { }; }; - systemd.services.sabnzbd = { - description = "sabnzbd server"; - wantedBy = [ "multi-user.target" ]; - after = [ "network.target" ]; - serviceConfig = { - Type = "forking"; - GuessMainPID = "no"; - User = cfg.user; - Group = cfg.group; - StateDirectory = "sabnzbd"; - ExecStart = "${lib.getBin cfg.package}/bin/sabnzbd -d -f ${cfg.configFile}"; + systemd.services.sabnzbd = + let + publicIniQuoted = lib.escapeShellArg publicSettingsIni; + in + { + description = "sabnzbd server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + serviceConfig = { + Type = "forking"; + GuessMainPID = "no"; + User = cfg.user; + Group = cfg.group; + StateDirectory = cfg.stateDir; + ExecStart = "${lib.getExe cfg.package} -d -f ${iniPathQuoted}"; + }; }; - }; networking.firewall = mkIf cfg.openFirewall { - allowedTCPPorts = [ 8080 ]; + allowedTCPPorts = [ cfg.settings.misc.port ]; }; }; }