Compare commits

..

6 Commits

Author SHA1 Message Date
9635aecb81 test: add permanent failure test
Verifies the service enters failed state after exhausting all
StartLimitBurst retries when the API never becomes available.
Checks StartLimitIntervalSec/Burst configuration and confirms
repeated timeout messages appear in the journal.
2026-04-16 16:35:28 -04:00
a37b6f6112 test: add network namespace test
Tests networkNamespacePath and networkNamespaceService options.
Creates a network namespace, runs a mock Servarr inside it, verifies
namespace isolation (mock unreachable from default ns), and confirms
the init service provisions resources through the namespace.
2026-04-16 16:34:53 -04:00
f766e5f71e test: add naming configuration test
Exercises the naming option which was previously untested.
Verifies fields are applied to Sonarr via config/naming API
and validates idempotency (second run reports 'already correct').
2026-04-16 16:34:28 -04:00
f86a5f1b39 refactor: split module.nix into per-service modules
Replace the 1301-line monolithic module.nix with focused modules:
- modules/servarr.nix  (Sonarr/Radarr/Prowlarr)
- modules/bazarr.nix   (Bazarr provider connections)
- modules/jellyseerr.nix (Jellyseerr quality profiles)
- modules/default.nix  (import aggregator)

Python scripts (from prior commit) are referenced as standalone
files via PYTHONPATH, with config passed as a JSON file argument.

New options and behavioral changes:
- Add bindAddress option to all services (default 127.0.0.1)
- Change healthChecks default from false to true
- Replace hardcoded wg.service dependency with configurable
  networkNamespaceService option
- Add systemd hardening: PrivateTmp, NoNewPrivileges, ProtectHome,
  ProtectKernelTunables/Modules, ProtectControlGroups,
  RestrictSUIDSGID, SystemCallArchitectures=native

Test updates:
- Extract mock qBittorrent/SABnzbd servers into tests/lib/mocks.nix
- Add healthChecks=false to tests not exercising health checks
- Fix duplicate wait_for_unit calls in integration test
2026-04-16 16:34:04 -04:00
b464a8cea2 refactor: extract Python scripts into standalone files
Move embedded Python scripts out of Nix string interpolation into
standalone files under scripts/.  Each script reads its configuration
from a JSON file passed as the first CLI argument.

Shared utilities (API key reading, API polling, health check loop)
are consolidated into common.py, eliminating three copies of
read_api_key and wait_for_api.

Implementation improvements included in the extraction:
- Remove pyarr dependency; all HTTP calls use raw requests
- Add update semantics: download clients and synced apps are now
  compared against desired state and updated on drift via PUT
- Bazarr configure_provider compares API keys and updates stale ones
- Narrow health_check_loop exception clause from bare Exception to
  (RequestException, ValueError, KeyError)
- Fix double resp.json() call in resolve_profile_id (jellyseerr)
- Replace os.system with subprocess.run for Jellyseerr restart
- Handle missing 'value' key in Servarr field API responses
2026-04-16 16:33:18 -04:00
b97ed1e90c flake.nix: use flake-utils for system gen 2026-04-16 13:50:51 -04:00
12 changed files with 50 additions and 11 deletions

34
flake.lock generated
View File

@@ -1,5 +1,23 @@
{ {
"nodes": { "nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1771848320, "lastModified": 1771848320,
@@ -18,8 +36,24 @@
}, },
"root": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
} }
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
} }
}, },
"root": "root", "root": "root",

View File

@@ -216,7 +216,7 @@ let
healthChecks = lib.mkOption { healthChecks = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = true;
description = '' description = ''
When enabled, the init service will verify connectivity after provisioning: When enabled, the init service will verify connectivity after provisioning:
- Tests all download clients are reachable via the application's testall API - Tests all download clients are reachable via the application's testall API

View File

@@ -42,17 +42,9 @@ def _dict_to_fields(d):
def _needs_field_update(desired, current_fields): def _needs_field_update(desired, current_fields):
"""Return True if any desired field value differs from the current state. """Return True if any desired field value differs from the current state."""
Skips fields that the API returns masked (e.g. '********' for API keys
and passwords) since comparison against the real value always shows drift.
"""
current = _fields_to_dict(current_fields) current = _fields_to_dict(current_fields)
return any( return any(desired.get(k) != current.get(k) for k in desired)
desired.get(k) != current.get(k)
for k in desired
if current.get(k) != "********"
)
# -- Download clients -------------------------------------------------------- # -- Download clients --------------------------------------------------------

View File

@@ -97,6 +97,7 @@ pkgs.testers.runNixOSTest {
]; ];
services.arrInit.sonarr = { services.arrInit.sonarr = {
healthChecks = false;
enable = true; enable = true;
serviceName = "mock-sonarr"; serviceName = "mock-sonarr";
dataDir = "/var/lib/mock-sonarr"; dataDir = "/var/lib/mock-sonarr";

View File

@@ -45,6 +45,7 @@ pkgs.testers.runNixOSTest {
# Test 3: Path with spaces # Test 3: Path with spaces
services.arrInit.sonarr = { services.arrInit.sonarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "sonarr"; serviceName = "sonarr";
dataDir = "/var/lib/sonarr/.config/NzbDrone"; dataDir = "/var/lib/sonarr/.config/NzbDrone";
port = 8989; port = 8989;

View File

@@ -26,6 +26,7 @@ pkgs.testers.runNixOSTest {
# The dataDir points to a non-existent config.xml # The dataDir points to a non-existent config.xml
services.arrInit.sonarr = { services.arrInit.sonarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "sonarr"; serviceName = "sonarr";
dataDir = "/var/lib/nonexistent"; dataDir = "/var/lib/nonexistent";
port = 8989; port = 8989;

View File

@@ -61,6 +61,7 @@ pkgs.testers.runNixOSTest {
services.arrInit.sonarr = { services.arrInit.sonarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "sonarr"; serviceName = "sonarr";
dataDir = "/var/lib/sonarr/.config/NzbDrone"; dataDir = "/var/lib/sonarr/.config/NzbDrone";
port = 8989; port = 8989;
@@ -83,6 +84,7 @@ pkgs.testers.runNixOSTest {
services.arrInit.radarr = { services.arrInit.radarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "radarr"; serviceName = "radarr";
dataDir = "/var/lib/radarr/.config/Radarr"; dataDir = "/var/lib/radarr/.config/Radarr";
port = 7878; port = 7878;
@@ -105,6 +107,7 @@ pkgs.testers.runNixOSTest {
services.arrInit.prowlarr = { services.arrInit.prowlarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "prowlarr"; serviceName = "prowlarr";
dataDir = "/var/lib/prowlarr"; dataDir = "/var/lib/prowlarr";
port = 9696; port = 9696;

View File

@@ -48,6 +48,7 @@ pkgs.testers.runNixOSTest {
# Sonarr with TWO download clients: qBittorrent + SABnzbd # Sonarr with TWO download clients: qBittorrent + SABnzbd
services.arrInit.sonarr = { services.arrInit.sonarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "sonarr"; serviceName = "sonarr";
dataDir = "/var/lib/sonarr/.config/NzbDrone"; dataDir = "/var/lib/sonarr/.config/NzbDrone";
port = 8989; port = 8989;

View File

@@ -18,6 +18,7 @@ pkgs.testers.runNixOSTest {
serviceName = "sonarr"; serviceName = "sonarr";
dataDir = "/var/lib/sonarr/.config/NzbDrone"; dataDir = "/var/lib/sonarr/.config/NzbDrone";
port = 8989; port = 8989;
healthChecks = false;
naming = { naming = {
renameEpisodes = true; renameEpisodes = true;
standardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}"; standardEpisodeFormat = "{Series Title} - S{season:00}E{episode:00} - {Episode Title} {Quality Full}";

View File

@@ -90,6 +90,7 @@ pkgs.testers.runNixOSTest {
serviceName = "mock-sonarr"; serviceName = "mock-sonarr";
dataDir = "/var/lib/mock-sonarr"; dataDir = "/var/lib/mock-sonarr";
port = 8989; port = 8989;
healthChecks = false;
networkNamespacePath = "/run/netns/test-ns"; networkNamespacePath = "/run/netns/test-ns";
networkNamespaceService = "create-netns"; networkNamespaceService = "create-netns";
rootFolders = [ "/media/tv" ]; rootFolders = [ "/media/tv" ];

View File

@@ -50,6 +50,7 @@ pkgs.testers.runNixOSTest {
# Test 1: Only rootFolders (no downloadClients) # Test 1: Only rootFolders (no downloadClients)
services.arrInit.sonarr = { services.arrInit.sonarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "sonarr"; serviceName = "sonarr";
dataDir = "/var/lib/sonarr/.config/NzbDrone"; dataDir = "/var/lib/sonarr/.config/NzbDrone";
port = 8989; port = 8989;
@@ -61,6 +62,7 @@ pkgs.testers.runNixOSTest {
# Test 2: Only downloadClients (no rootFolders) # Test 2: Only downloadClients (no rootFolders)
services.arrInit.radarr = { services.arrInit.radarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "radarr"; serviceName = "radarr";
dataDir = "/var/lib/radarr/.config/Radarr"; dataDir = "/var/lib/radarr/.config/Radarr";
port = 7878; port = 7878;
@@ -85,6 +87,7 @@ pkgs.testers.runNixOSTest {
# Test 3: Only syncedApps (Prowlarr) # Test 3: Only syncedApps (Prowlarr)
services.arrInit.prowlarr = { services.arrInit.prowlarr = {
enable = true; enable = true;
healthChecks = false;
serviceName = "prowlarr"; serviceName = "prowlarr";
dataDir = "/var/lib/prowlarr"; dataDir = "/var/lib/prowlarr";
port = 9696; port = 9696;

View File

@@ -47,6 +47,7 @@ pkgs.testers.runNixOSTest {
serviceName = "mock-sonarr"; serviceName = "mock-sonarr";
dataDir = "/var/lib/mock-sonarr"; dataDir = "/var/lib/mock-sonarr";
port = 8989; port = 8989;
healthChecks = false;
# Very short timeout so retries happen fast # Very short timeout so retries happen fast
apiTimeout = 3; apiTimeout = 3;
}; };