mirror of
https://github.com/CHN-beta/nixpkgs.git
synced 2026-01-12 02:40:31 +08:00
nixos/qbittorrent: init service module
nixos/qbittorrent: add default serverConfig & fix test
Migrate to runTest
Replace lib.optional with lib.optionals
nixos/qbittorrent: update release notes to 2511
(cherry picked from commit 84d174e312)
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
|
||||
- [go-httpbin](https://github.com/mccutchen/go-httpbin), a reasonably complete and well-tested golang port of httpbin, with zero dependencies outside the go stdlib. Available as [services.go-httpbin](#opt-services.go-httpbin.enable).
|
||||
|
||||
- [qBittorrent](https://www.qbittorrent.org/), is a bittorrent client programmed in C++ / Qt that uses libtorrent by Arvid Norberg. Available as [services.qbittorrent](#opt-services.qbittorrent.enable).
|
||||
|
||||
## Backward Incompatibilities {#sec-release-25.11-incompatibilities}
|
||||
|
||||
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
||||
|
||||
@@ -1486,6 +1486,7 @@
|
||||
./services/torrent/magnetico.nix
|
||||
./services/torrent/opentracker.nix
|
||||
./services/torrent/peerflix.nix
|
||||
./services/torrent/qbittorrent.nix
|
||||
./services/torrent/rtorrent.nix
|
||||
./services/torrent/torrentstream.nix
|
||||
./services/torrent/transmission.nix
|
||||
|
||||
238
nixos/modules/services/torrent/qbittorrent.nix
Normal file
238
nixos/modules/services/torrent/qbittorrent.nix
Normal file
@@ -0,0 +1,238 @@
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
utils,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.services.qbittorrent;
|
||||
inherit (builtins) concatStringsSep isAttrs isString;
|
||||
inherit (lib)
|
||||
literalExpression
|
||||
getExe
|
||||
mkEnableOption
|
||||
mkOption
|
||||
mkPackageOption
|
||||
mkIf
|
||||
maintainers
|
||||
escape
|
||||
collect
|
||||
mapAttrsRecursive
|
||||
optionals
|
||||
;
|
||||
inherit (lib.types)
|
||||
str
|
||||
port
|
||||
path
|
||||
nullOr
|
||||
listOf
|
||||
attrsOf
|
||||
anything
|
||||
submodule
|
||||
;
|
||||
inherit (lib.generators) toINI mkKeyValueDefault mkValueStringDefault;
|
||||
gendeepINI = toINI {
|
||||
mkKeyValue =
|
||||
let
|
||||
sep = "=";
|
||||
in
|
||||
k: v:
|
||||
if isAttrs v then
|
||||
concatStringsSep "\n" (
|
||||
collect isString (
|
||||
mapAttrsRecursive (
|
||||
path: value:
|
||||
"${escape [ sep ] (concatStringsSep "\\" ([ k ] ++ path))}${sep}${mkValueStringDefault { } value}"
|
||||
) v
|
||||
)
|
||||
)
|
||||
else
|
||||
mkKeyValueDefault { } sep k v;
|
||||
};
|
||||
configFile = pkgs.writeText "qBittorrent.conf" (gendeepINI cfg.serverConfig);
|
||||
in
|
||||
{
|
||||
options.services.qbittorrent = {
|
||||
enable = mkEnableOption "qbittorrent, BitTorrent client";
|
||||
|
||||
package = mkPackageOption pkgs "qbittorrent-nox" { };
|
||||
|
||||
user = mkOption {
|
||||
type = str;
|
||||
default = "qbittorrent";
|
||||
description = "User account under which qbittorrent runs.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = str;
|
||||
default = "qbittorrent";
|
||||
description = "Group under which qbittorrent runs.";
|
||||
};
|
||||
|
||||
profileDir = mkOption {
|
||||
type = path;
|
||||
default = "/var/lib/qBittorrent/";
|
||||
description = "the path passed to qbittorrent via --profile.";
|
||||
};
|
||||
|
||||
openFirewall = mkEnableOption "opening both the webuiPort and torrentPort over TCP in the firewall";
|
||||
|
||||
webuiPort = mkOption {
|
||||
default = 8080;
|
||||
type = nullOr port;
|
||||
description = "the port passed to qbittorrent via `--webui-port`";
|
||||
};
|
||||
|
||||
torrentingPort = mkOption {
|
||||
default = null;
|
||||
type = nullOr port;
|
||||
description = "the port passed to qbittorrent via `--torrenting-port`";
|
||||
};
|
||||
|
||||
serverConfig = mkOption {
|
||||
default = { };
|
||||
type = submodule {
|
||||
freeformType = attrsOf (attrsOf anything);
|
||||
};
|
||||
description = ''
|
||||
Free-form settings mapped to the `qBittorrent.conf` file in the profile.
|
||||
Refer to [Explanation-of-Options-in-qBittorrent](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent).
|
||||
The Password_PBKDF2 format is oddly unique, you will likely want to use [this tool](https://codeberg.org/feathecutie/qbittorrent_password) to generate the format.
|
||||
Alternatively you can run qBittorrent independently first and use its webUI to generate the format.
|
||||
|
||||
Optionally an alternative webUI can be easily set. VueTorrent for example:
|
||||
```nix
|
||||
{
|
||||
Preferences = {
|
||||
WebUI = {
|
||||
AlternativeUIEnabled = true;
|
||||
RootFolder = "''${pkgs.vuetorrent}/share/vuetorrent";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
```
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
LegalNotice.Accepted = true;
|
||||
Preferences = {
|
||||
WebUI = {
|
||||
Username = "user";
|
||||
Password_PBKDF2 = "generated ByteArray.";
|
||||
};
|
||||
General.Locale = "en";
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
extraArgs = mkOption {
|
||||
type = listOf str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Extra arguments passed to qbittorrent. See `qbittorrent -h`, or the [source code](https://github.com/qbittorrent/qBittorrent/blob/master/src/app/cmdoptions.cpp), for the available arguments.
|
||||
'';
|
||||
example = [
|
||||
"--confirm-legal-notice"
|
||||
];
|
||||
};
|
||||
};
|
||||
config = mkIf cfg.enable {
|
||||
systemd = {
|
||||
tmpfiles.settings = {
|
||||
qbittorrent = {
|
||||
"${cfg.profileDir}/qBittorrent/"."d" = {
|
||||
mode = "755";
|
||||
inherit (cfg) user group;
|
||||
};
|
||||
"${cfg.profileDir}/qBittorrent/config/"."d" = {
|
||||
mode = "755";
|
||||
inherit (cfg) user group;
|
||||
};
|
||||
"${cfg.profileDir}/qBittorrent/config/qBittorrent.conf"."L+" = mkIf (cfg.serverConfig != { }) {
|
||||
mode = "1400";
|
||||
inherit (cfg) user group;
|
||||
argument = "${configFile}";
|
||||
};
|
||||
};
|
||||
};
|
||||
services.qbittorrent = {
|
||||
description = "qbittorrent BitTorrent client";
|
||||
wants = [ "network-online.target" ];
|
||||
after = [
|
||||
"local-fs.target"
|
||||
"network-online.target"
|
||||
"nss-lookup.target"
|
||||
];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
restartTriggers = optionals (cfg.serverConfig != { }) [ configFile ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = utils.escapeSystemdExecArgs (
|
||||
[
|
||||
(getExe cfg.package)
|
||||
"--profile=${cfg.profileDir}"
|
||||
]
|
||||
++ optionals (cfg.webuiPort != null) [ "--webui-port=${toString cfg.webuiPort}" ]
|
||||
++ optionals (cfg.torrentingPort != null) [ "--torrenting-port=${toString cfg.torrentingPort}" ]
|
||||
++ cfg.extraArgs
|
||||
);
|
||||
TimeoutStopSec = 1800;
|
||||
|
||||
# https://github.com/qbittorrent/qBittorrent/pull/6806#discussion_r121478661
|
||||
PrivateTmp = false;
|
||||
|
||||
PrivateNetwork = false;
|
||||
RemoveIPC = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateUsers = true;
|
||||
ProtectHome = "yes";
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
ProtectSystem = "full";
|
||||
ProtectClock = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectControlGroups = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_NETLINK"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
SystemCallArchitectures = "native";
|
||||
CapabilityBoundingSet = "";
|
||||
SystemCallFilter = [ "@system-service" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
users = {
|
||||
users = mkIf (cfg.user == "qbittorrent") {
|
||||
qbittorrent = {
|
||||
inherit (cfg) group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
};
|
||||
groups = mkIf (cfg.group == "qbittorrent") { qbittorrent = { }; };
|
||||
};
|
||||
|
||||
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall (
|
||||
optionals (cfg.webuiPort != null) [ cfg.webuiPort ]
|
||||
++ optionals (cfg.torrentingPort != null) [ cfg.torrentingPort ]
|
||||
);
|
||||
};
|
||||
meta.maintainers = with maintainers; [ fsnkty ];
|
||||
}
|
||||
@@ -1146,6 +1146,7 @@ in
|
||||
public-inbox = handleTest ./public-inbox.nix { };
|
||||
pufferpanel = handleTest ./pufferpanel.nix { };
|
||||
pulseaudio = discoverTests (import ./pulseaudio.nix);
|
||||
qbittorrent = runTest ./qbittorrent.nix;
|
||||
qboot = handleTestOn [ "x86_64-linux" "i686-linux" ] ./qboot.nix { };
|
||||
qemu-vm-restrictnetwork = handleTest ./qemu-vm-restrictnetwork.nix { };
|
||||
qemu-vm-volatile-root = runTest ./qemu-vm-volatile-root.nix;
|
||||
|
||||
190
nixos/tests/qbittorrent.nix
Normal file
190
nixos/tests/qbittorrent.nix
Normal file
@@ -0,0 +1,190 @@
|
||||
{ pkgs, lib, ... }:
|
||||
{
|
||||
name = "qbittorrent";
|
||||
|
||||
meta = with pkgs.lib.maintainers; {
|
||||
maintainers = [ fsnkty ];
|
||||
};
|
||||
|
||||
nodes = {
|
||||
simple = {
|
||||
services.qbittorrent.enable = true;
|
||||
|
||||
specialisation.portChange.configuration = {
|
||||
services.qbittorrent = {
|
||||
enable = true;
|
||||
webuiPort = 5555;
|
||||
torrentingPort = 44444;
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.openPorts.configuration = {
|
||||
services.qbittorrent = {
|
||||
enable = true;
|
||||
openFirewall = true;
|
||||
webuiPort = 8080;
|
||||
torrentingPort = 55555;
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.serverConfig.configuration = {
|
||||
services.qbittorrent = {
|
||||
enable = true;
|
||||
webuiPort = null;
|
||||
serverConfig.Preferences.WebUI.Port = "8181";
|
||||
};
|
||||
};
|
||||
};
|
||||
# Seperate vm because it's not possible to reboot into a specialisation with
|
||||
# switch-to-configuration: https://github.com/NixOS/nixpkgs/issues/82851
|
||||
# For one of the test we check if manual changes are overridden during
|
||||
# reboot, therefore it's necessary to reboot into a declarative setup.
|
||||
declarative = {
|
||||
services.qbittorrent = {
|
||||
enable = true;
|
||||
webuiPort = null;
|
||||
serverConfig = {
|
||||
Preferences = {
|
||||
WebUI = {
|
||||
Username = "user";
|
||||
# Default password: adminadmin
|
||||
Password_PBKDF2 = "@ByteArray(6DIf26VOpTCYbgNiO6DAFQ==:e6241eaAWGzRotQZvVA5/up9fj5wwSAThLgXI2lVMsYTu1StUgX9MgmElU3Sa/M8fs+zqwZv9URiUOObjqJGNw==)";
|
||||
Port = lib.mkDefault "8181";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
specialisation.serverConfigChange.configuration = {
|
||||
services.qbittorrent = {
|
||||
enable = true;
|
||||
webuiPort = null;
|
||||
serverConfig.Preferences.WebUI.Port = "7171";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
{ nodes, ... }:
|
||||
let
|
||||
simpleSpecPath = "${nodes.simple.system.build.toplevel}/specialisation";
|
||||
declarativeSpecPath = "${nodes.declarative.system.build.toplevel}/specialisation";
|
||||
portChange = "${simpleSpecPath}/portChange";
|
||||
openPorts = "${simpleSpecPath}/openPorts";
|
||||
serverConfig = "${simpleSpecPath}/serverConfig";
|
||||
serverConfigChange = "${declarativeSpecPath}/serverConfigChange";
|
||||
in
|
||||
''
|
||||
simple.start(allow_reboot=True)
|
||||
declarative.start(allow_reboot=True)
|
||||
|
||||
|
||||
def test_webui(machine, port):
|
||||
machine.wait_for_unit("qbittorrent.service")
|
||||
machine.wait_for_open_port(port)
|
||||
machine.wait_until_succeeds(f"curl --fail http://localhost:{port}")
|
||||
|
||||
|
||||
# To simulate an interactive change in the settings
|
||||
def setPreferences_api(machine, port, post_creds, post_data):
|
||||
qb_url = f"http://localhost:{port}"
|
||||
api_url = f"{qb_url}/api/v2"
|
||||
cookie_path = "/tmp/qbittorrent.cookie"
|
||||
|
||||
machine.succeed(
|
||||
f'curl --header "Referer: {qb_url}" \
|
||||
--data "{post_creds}" {api_url}/auth/login \
|
||||
-c {cookie_path}'
|
||||
)
|
||||
machine.succeed(
|
||||
f'curl --header "Referer: {qb_url}" \
|
||||
--data "{post_data}" {api_url}/app/setPreferences \
|
||||
-b {cookie_path}'
|
||||
)
|
||||
|
||||
|
||||
# A randomly generated password is printed in the service log when no
|
||||
# password it set
|
||||
def get_temp_pass(machine):
|
||||
_, password = machine.execute(
|
||||
"journalctl -u qbittorrent.service |\
|
||||
grep 'The WebUI administrator password was not set.' |\
|
||||
awk '{ print $NF }' | tr -d '\n'"
|
||||
)
|
||||
return password
|
||||
|
||||
|
||||
# Non declarative tests
|
||||
|
||||
with subtest("webui works with all default settings"):
|
||||
test_webui(simple, 8080)
|
||||
|
||||
with subtest("check if manual changes in settings are saved correctly"):
|
||||
temp_pass = get_temp_pass(simple)
|
||||
|
||||
## Change some settings
|
||||
api_post = [r"json={\"listen_port\": 33333}", r"json={\"web_ui_port\": 9090}"]
|
||||
for x in api_post:
|
||||
setPreferences_api(
|
||||
machine=simple,
|
||||
port=8080,
|
||||
post_creds=f"username=admin&password={temp_pass}",
|
||||
post_data=x,
|
||||
)
|
||||
|
||||
simple.wait_for_open_port(33333)
|
||||
test_webui(simple, 9090)
|
||||
|
||||
## Test which settings are reset
|
||||
## As webuiPort is passed as an cli it should reset after reboot
|
||||
## As torrentingPort is not passed as an cli it should not reset after
|
||||
## reboot
|
||||
simple.reboot()
|
||||
test_webui(simple, 8080)
|
||||
simple.wait_for_open_port(33333)
|
||||
|
||||
with subtest("ports are changed on config change"):
|
||||
simple.succeed("${portChange}/bin/switch-to-configuration test")
|
||||
test_webui(simple, 5555)
|
||||
simple.wait_for_open_port(44444)
|
||||
|
||||
with subtest("firewall is opened correctly"):
|
||||
simple.succeed("${openPorts}/bin/switch-to-configuration test")
|
||||
test_webui(simple, 8080)
|
||||
declarative.wait_until_succeeds("curl --fail http://simple:8080")
|
||||
declarative.wait_for_open_port(55555, "simple")
|
||||
|
||||
with subtest("switching from simple to declarative works"):
|
||||
simple.succeed("${serverConfig}/bin/switch-to-configuration test")
|
||||
test_webui(simple, 8181)
|
||||
|
||||
|
||||
# Declarative tests
|
||||
|
||||
with subtest("serverConfig is applied correctly"):
|
||||
test_webui(declarative, 8181)
|
||||
|
||||
with subtest("manual changes are overridden during reboot"):
|
||||
## Change some settings
|
||||
setPreferences_api(
|
||||
machine=declarative,
|
||||
port=8181, # as set through serverConfig
|
||||
post_creds="username=user&password=adminadmin",
|
||||
post_data=r"json={\"web_ui_port\": 9191}",
|
||||
)
|
||||
|
||||
test_webui(declarative, 9191)
|
||||
|
||||
## Test which settings are reset
|
||||
## The generated qBittorrent.conf is, apparently, reapplied after reboot.
|
||||
## Because the port is set in `serverConfig` this overrides the manually
|
||||
## set port.
|
||||
declarative.reboot()
|
||||
test_webui(declarative, 8181)
|
||||
|
||||
with subtest("changes in serverConfig are applied correctly"):
|
||||
declarative.succeed("${serverConfigChange}/bin/switch-to-configuration test")
|
||||
test_webui(declarative, 7171)
|
||||
'';
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
webuiSupport ? true,
|
||||
wrapGAppsHook3,
|
||||
zlib,
|
||||
nixosTests,
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation (finalAttrs: {
|
||||
@@ -74,7 +75,10 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
qtWrapperArgs+=("''${gappsWrapperArgs[@]}")
|
||||
'';
|
||||
|
||||
passthru.updateScript = nix-update-script { extraArgs = [ "--version-regex=release-(.*)" ]; };
|
||||
passthru = {
|
||||
updateScript = nix-update-script { extraArgs = [ "--version-regex=release-(.*)" ]; };
|
||||
tests.testService = nixosTests.qbittorrent;
|
||||
};
|
||||
|
||||
meta = {
|
||||
description = "Featureful free software BitTorrent client";
|
||||
|
||||
Reference in New Issue
Block a user