jellyfin-qbittorrent-monitor: write proper test
This commit is contained in:
parent
b1b9a3755f
commit
e9c1df44e8
@ -28,6 +28,7 @@
|
|||||||
|
|
||||||
./services/wg.nix
|
./services/wg.nix
|
||||||
./services/qbittorrent.nix
|
./services/qbittorrent.nix
|
||||||
|
./services/jellyfin-qbittorrent-monitor.nix
|
||||||
./services/bitmagnet.nix
|
./services/bitmagnet.nix
|
||||||
|
|
||||||
./services/soulseek.nix
|
./services/soulseek.nix
|
||||||
|
|||||||
48
services/jellyfin-qbittorrent-monitor.nix
Normal file
48
services/jellyfin-qbittorrent-monitor.nix
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
service_configs,
|
||||||
|
config,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
{
|
||||||
|
systemd.services."jellyfin-qbittorrent-monitor" = {
|
||||||
|
description = "Monitor Jellyfin streaming and control qBittorrent rate limits";
|
||||||
|
after = [
|
||||||
|
"network.target"
|
||||||
|
"jellyfin.service"
|
||||||
|
"qbittorrent.service"
|
||||||
|
];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = pkgs.writeShellScript "jellyfin-monitor-start" ''
|
||||||
|
export JELLYFIN_API_KEY=$(cat ${config.age.secrets.jellyfin-api-key.path})
|
||||||
|
exec ${
|
||||||
|
pkgs.python3.withPackages (ps: with ps; [ requests ])
|
||||||
|
}/bin/python ${./jellyfin-qbittorrent-monitor.py}
|
||||||
|
'';
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "10s";
|
||||||
|
|
||||||
|
# Security hardening
|
||||||
|
DynamicUser = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RemoveIPC = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
JELLYFIN_URL = "http://localhost:${builtins.toString service_configs.ports.jellyfin}";
|
||||||
|
QBITTORRENT_URL = "http://${service_configs.https.wg_ip}:${builtins.toString service_configs.ports.torrent}";
|
||||||
|
CHECK_INTERVAL = "30";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -15,44 +15,4 @@
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services."jellyfin-qbittorrent-monitor" = {
|
|
||||||
description = "Monitor Jellyfin streaming and control qBittorrent rate limits";
|
|
||||||
after = [
|
|
||||||
"network.target"
|
|
||||||
"jellyfin.service"
|
|
||||||
"qbittorrent.service"
|
|
||||||
];
|
|
||||||
wantedBy = [ "multi-user.target" ];
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "simple";
|
|
||||||
ExecStart = pkgs.writeShellScript "jellyfin-monitor-start" ''
|
|
||||||
export JELLYFIN_API_KEY=$(cat ${config.age.secrets.jellyfin-api-key.path})
|
|
||||||
exec ${
|
|
||||||
pkgs.python3.withPackages (ps: with ps; [ requests ])
|
|
||||||
}/bin/python ${./jellyfin-qbittorrent-monitor.py}
|
|
||||||
'';
|
|
||||||
Restart = "always";
|
|
||||||
RestartSec = "10s";
|
|
||||||
|
|
||||||
# Security hardening
|
|
||||||
DynamicUser = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
ProtectHome = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
MemoryDenyWriteExecute = true;
|
|
||||||
RestrictRealtime = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
RemoveIPC = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
environment = {
|
|
||||||
JELLYFIN_URL = "http://localhost:${builtins.toString service_configs.ports.jellyfin}";
|
|
||||||
QBITTORRENT_URL = "http://${service_configs.https.wg_ip}:${builtins.toString service_configs.ports.torrent}";
|
|
||||||
CHECK_INTERVAL = "30";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
201
tests/jellyfin-qbittorrent-monitor.nix
Normal file
201
tests/jellyfin-qbittorrent-monitor.nix
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
{
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
pkgs.testers.runNixOSTest {
|
||||||
|
name = "jellyfin-qbittorrent-monitor";
|
||||||
|
|
||||||
|
nodes = {
|
||||||
|
server =
|
||||||
|
{ pkgs, config, ... }:
|
||||||
|
{
|
||||||
|
# Mock qBittorrent service
|
||||||
|
systemd.services.mock-qbittorrent = {
|
||||||
|
description = "Mock qBittorrent API server";
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = lib.getExe (
|
||||||
|
pkgs.writers.writePython3Bin "mock-qbt" { flakeIgnore = [ "E501" ]; } ''
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
|
||||||
|
|
||||||
|
class MockQBittorrentHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/api/v2/transfer/speedLimitsMode':
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'text/plain')
|
||||||
|
self.end_headers()
|
||||||
|
response = '1' if getattr(self.server, 'speed_limits_mode', False) else '0'
|
||||||
|
self.wfile.write(response.encode())
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path == '/api/v2/transfer/toggleSpeedLimitsMode':
|
||||||
|
self.server.speed_limits_mode = not getattr(self.server, 'speed_limits_mode', False)
|
||||||
|
self.send_response(200)
|
||||||
|
self.end_headers()
|
||||||
|
print(f'MONITOR_TEST: Speed limits toggled to {self.server.speed_limits_mode}')
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
print(f'qBittorrent Mock: {format % args}')
|
||||||
|
|
||||||
|
|
||||||
|
with socketserver.TCPServer(('127.0.0.1', 8080), MockQBittorrentHandler) as httpd:
|
||||||
|
httpd.speed_limits_mode = False
|
||||||
|
print('Mock qBittorrent server started on port 8080')
|
||||||
|
httpd.serve_forever()
|
||||||
|
''
|
||||||
|
);
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "5s";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Mock Jellyfin service with controllable streaming state
|
||||||
|
systemd.services.mock-jellyfin = {
|
||||||
|
description = "Mock Jellyfin API server";
|
||||||
|
after = [ "network.target" ];
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "simple";
|
||||||
|
ExecStart = lib.getExe (
|
||||||
|
pkgs.writers.writePython3Bin "mock-jellyfin" { } ''
|
||||||
|
import http.server
|
||||||
|
import socketserver
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class MockJellyfinHandler(http.server.BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
if self.path == '/Sessions':
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
# Check if we should simulate streaming
|
||||||
|
streaming_file = '/tmp/jellyfin_streaming'
|
||||||
|
if os.path.exists(streaming_file):
|
||||||
|
# Simulate external streaming session
|
||||||
|
sessions = [{
|
||||||
|
'Id': 'test-session-1',
|
||||||
|
'UserName': 'ExternalUser',
|
||||||
|
'RemoteEndPoint': '203.0.113.42', # External IP
|
||||||
|
'NowPlayingItem': {
|
||||||
|
'Name': 'Test Movie',
|
||||||
|
'Type': 'Movie'
|
||||||
|
},
|
||||||
|
'PlayState': {
|
||||||
|
'IsPaused': False
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
else:
|
||||||
|
# No streaming sessions
|
||||||
|
sessions = []
|
||||||
|
|
||||||
|
self.wfile.write(json.dumps(sessions).encode())
|
||||||
|
else:
|
||||||
|
self.send_response(404)
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
def log_message(self, format, *args):
|
||||||
|
print(f'Jellyfin Mock: {format % args}')
|
||||||
|
|
||||||
|
|
||||||
|
with socketserver.TCPServer(('127.0.0.1', 8096), MockJellyfinHandler) as httpd:
|
||||||
|
print('Mock Jellyfin server started on port 8096')
|
||||||
|
httpd.serve_forever()
|
||||||
|
''
|
||||||
|
);
|
||||||
|
Restart = "always";
|
||||||
|
RestartSec = "5s";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
curl
|
||||||
|
python3
|
||||||
|
];
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [
|
||||||
|
8096
|
||||||
|
8080
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = ''
|
||||||
|
start_all()
|
||||||
|
|
||||||
|
# Wait for services to start
|
||||||
|
server.wait_for_unit("multi-user.target")
|
||||||
|
server.wait_for_unit("mock-jellyfin.service")
|
||||||
|
server.wait_for_unit("mock-qbittorrent.service")
|
||||||
|
|
||||||
|
# Wait for services to be accessible
|
||||||
|
server.wait_for_open_port(8096) # Mock Jellyfin
|
||||||
|
server.wait_for_open_port(8080) # Mock qBittorrent
|
||||||
|
|
||||||
|
import time
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# TEST 1: Verify initial state - no streaming, normal speed limits
|
||||||
|
qbt_result = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||||
|
assert qbt_result.strip() == "0", "qBittorrent should start with normal speed limits"
|
||||||
|
|
||||||
|
# Verify no streaming sessions initially
|
||||||
|
sessions_result = server.succeed("curl -s http://localhost:8096/Sessions")
|
||||||
|
print(f"Initial Jellyfin sessions: {sessions_result}")
|
||||||
|
assert "[]" in sessions_result, "Should be no streaming sessions initially"
|
||||||
|
|
||||||
|
# Start the monitor
|
||||||
|
python_path = "${pkgs.python3.withPackages (ps: with ps; [ requests ])}/bin/python"
|
||||||
|
monitor_path = "${../services/jellyfin-qbittorrent-monitor.py}"
|
||||||
|
server.succeed(f"""
|
||||||
|
systemd-run --unit=jellyfin-qbittorrent-monitor-test \\
|
||||||
|
--setenv=JELLYFIN_URL=http://localhost:8096 \\
|
||||||
|
--setenv=QBITTORRENT_URL=http://localhost:8080 \\
|
||||||
|
--setenv=CHECK_INTERVAL=2 \\
|
||||||
|
{python_path} {monitor_path}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Wait for monitor to start
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# TEST 2: Verify monitor runs and keeps normal limits with no streaming
|
||||||
|
qbt_result = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||||
|
assert qbt_result.strip() == "0", "Should maintain normal speed limits with no streaming"
|
||||||
|
|
||||||
|
# TEST 3: Simulate external streaming and verify throttling
|
||||||
|
print("\\nSimulating external streaming session...")
|
||||||
|
server.succeed("touch /tmp/jellyfin_streaming") # Signal mock to return streaming session
|
||||||
|
|
||||||
|
# Wait for monitor to detect streaming and apply throttling (includes hysteresis delay)
|
||||||
|
time.sleep(15)
|
||||||
|
|
||||||
|
qbt_result_streaming = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||||
|
|
||||||
|
# Check if throttling was enabled
|
||||||
|
assert qbt_result_streaming.strip() == "1", "External streaming should enable qBittorrent throttling"
|
||||||
|
|
||||||
|
# TEST 4: Stop streaming and verify throttling is removed
|
||||||
|
print("\\nStopping streaming session...")
|
||||||
|
server.succeed("rm -f /tmp/jellyfin_streaming") # Signal mock to return no sessions
|
||||||
|
|
||||||
|
# Wait for monitor to detect no streaming and remove throttling (includes hysteresis delay)
|
||||||
|
time.sleep(65)
|
||||||
|
|
||||||
|
qbt_result_no_streaming = server.succeed("curl -s http://localhost:8080/api/v2/transfer/speedLimitsMode")
|
||||||
|
assert qbt_result_no_streaming.strip() == "0", "No streaming should disable qBittorrent throttling"
|
||||||
|
'';
|
||||||
|
}
|
||||||
@ -11,4 +11,5 @@ in
|
|||||||
zfsTest = handleTest ./zfs.nix;
|
zfsTest = handleTest ./zfs.nix;
|
||||||
testTest = handleTest ./testTest.nix;
|
testTest = handleTest ./testTest.nix;
|
||||||
minecraftTest = handleTest ./minecraft.nix;
|
minecraftTest = handleTest ./minecraft.nix;
|
||||||
|
jellyfinQbittorrentMonitorTest = handleTest ./jellyfin-qbittorrent-monitor.nix;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user