lol

nixos/qbittorrent: init (#287923)

authored by

André Silva and committed by
GitHub
fcf647a8 618d6a53

+437 -1
+2
nixos/doc/manual/release-notes/rl-2511.section.md
··· 52 52 53 53 - [Newt](https://github.com/fosrl/newt), a fully user space WireGuard tunnel client and TCP/UDP proxy, designed to securely expose private resources controlled by Pangolin. Available as [services.newt](options.html#opt-services.newt.enable). 54 54 55 + - [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). 56 + 55 57 - [Szurubooru](https://github.com/rr-/szurubooru), an image board engine inspired by services such as Danbooru, dedicated for small and medium communities. Available as [services.szurubooru](#opt-services.szurubooru.enable). 56 58 57 59 - The [Neat IP Address Planner](https://spritelink.github.io/NIPAP/) (NIPAP) can now be enabled through [services.nipap.enable](#opt-services.nipap.enable).
+1
nixos/modules/module-list.nix
··· 1502 1502 ./services/torrent/magnetico.nix 1503 1503 ./services/torrent/opentracker.nix 1504 1504 ./services/torrent/peerflix.nix 1505 + ./services/torrent/qbittorrent.nix 1505 1506 ./services/torrent/rtorrent.nix 1506 1507 ./services/torrent/torrentstream.nix 1507 1508 ./services/torrent/transmission.nix
+238
nixos/modules/services/torrent/qbittorrent.nix
··· 1 + { 2 + config, 3 + pkgs, 4 + lib, 5 + utils, 6 + ... 7 + }: 8 + let 9 + cfg = config.services.qbittorrent; 10 + inherit (builtins) concatStringsSep isAttrs isString; 11 + inherit (lib) 12 + literalExpression 13 + getExe 14 + mkEnableOption 15 + mkOption 16 + mkPackageOption 17 + mkIf 18 + maintainers 19 + escape 20 + collect 21 + mapAttrsRecursive 22 + optionals 23 + ; 24 + inherit (lib.types) 25 + str 26 + port 27 + path 28 + nullOr 29 + listOf 30 + attrsOf 31 + anything 32 + submodule 33 + ; 34 + inherit (lib.generators) toINI mkKeyValueDefault mkValueStringDefault; 35 + gendeepINI = toINI { 36 + mkKeyValue = 37 + let 38 + sep = "="; 39 + in 40 + k: v: 41 + if isAttrs v then 42 + concatStringsSep "\n" ( 43 + collect isString ( 44 + mapAttrsRecursive ( 45 + path: value: 46 + "${escape [ sep ] (concatStringsSep "\\" ([ k ] ++ path))}${sep}${mkValueStringDefault { } value}" 47 + ) v 48 + ) 49 + ) 50 + else 51 + mkKeyValueDefault { } sep k v; 52 + }; 53 + configFile = pkgs.writeText "qBittorrent.conf" (gendeepINI cfg.serverConfig); 54 + in 55 + { 56 + options.services.qbittorrent = { 57 + enable = mkEnableOption "qbittorrent, BitTorrent client"; 58 + 59 + package = mkPackageOption pkgs "qbittorrent-nox" { }; 60 + 61 + user = mkOption { 62 + type = str; 63 + default = "qbittorrent"; 64 + description = "User account under which qbittorrent runs."; 65 + }; 66 + 67 + group = mkOption { 68 + type = str; 69 + default = "qbittorrent"; 70 + description = "Group under which qbittorrent runs."; 71 + }; 72 + 73 + profileDir = mkOption { 74 + type = path; 75 + default = "/var/lib/qBittorrent/"; 76 + description = "the path passed to qbittorrent via --profile."; 77 + }; 78 + 79 + openFirewall = mkEnableOption "opening both the webuiPort and torrentPort over TCP in the firewall"; 80 + 81 + webuiPort = mkOption { 82 + default = 8080; 83 + type = nullOr port; 84 + description = "the port passed to qbittorrent via `--webui-port`"; 85 + }; 86 + 87 + torrentingPort = mkOption { 88 + default = null; 89 + type = nullOr port; 90 + description = "the port passed to qbittorrent via `--torrenting-port`"; 91 + }; 92 + 93 + serverConfig = mkOption { 94 + default = { }; 95 + type = submodule { 96 + freeformType = attrsOf (attrsOf anything); 97 + }; 98 + description = '' 99 + Free-form settings mapped to the `qBittorrent.conf` file in the profile. 100 + Refer to [Explanation-of-Options-in-qBittorrent](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent). 101 + 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. 102 + Alternatively you can run qBittorrent independently first and use its webUI to generate the format. 103 + 104 + Optionally an alternative webUI can be easily set. VueTorrent for example: 105 + ```nix 106 + { 107 + Preferences = { 108 + WebUI = { 109 + AlternativeUIEnabled = true; 110 + RootFolder = "''${pkgs.vuetorrent}/share/vuetorrent"; 111 + }; 112 + }; 113 + } 114 + ]; 115 + ``` 116 + ''; 117 + example = literalExpression '' 118 + { 119 + LegalNotice.Accepted = true; 120 + Preferences = { 121 + WebUI = { 122 + Username = "user"; 123 + Password_PBKDF2 = "generated ByteArray."; 124 + }; 125 + General.Locale = "en"; 126 + }; 127 + } 128 + ''; 129 + }; 130 + 131 + extraArgs = mkOption { 132 + type = listOf str; 133 + default = [ ]; 134 + description = '' 135 + 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. 136 + ''; 137 + example = [ 138 + "--confirm-legal-notice" 139 + ]; 140 + }; 141 + }; 142 + config = mkIf cfg.enable { 143 + systemd = { 144 + tmpfiles.settings = { 145 + qbittorrent = { 146 + "${cfg.profileDir}/qBittorrent/"."d" = { 147 + mode = "755"; 148 + inherit (cfg) user group; 149 + }; 150 + "${cfg.profileDir}/qBittorrent/config/"."d" = { 151 + mode = "755"; 152 + inherit (cfg) user group; 153 + }; 154 + "${cfg.profileDir}/qBittorrent/config/qBittorrent.conf"."L+" = mkIf (cfg.serverConfig != { }) { 155 + mode = "1400"; 156 + inherit (cfg) user group; 157 + argument = "${configFile}"; 158 + }; 159 + }; 160 + }; 161 + services.qbittorrent = { 162 + description = "qbittorrent BitTorrent client"; 163 + wants = [ "network-online.target" ]; 164 + after = [ 165 + "local-fs.target" 166 + "network-online.target" 167 + "nss-lookup.target" 168 + ]; 169 + wantedBy = [ "multi-user.target" ]; 170 + restartTriggers = optionals (cfg.serverConfig != { }) [ configFile ]; 171 + 172 + serviceConfig = { 173 + Type = "simple"; 174 + User = cfg.user; 175 + Group = cfg.group; 176 + ExecStart = utils.escapeSystemdExecArgs ( 177 + [ 178 + (getExe cfg.package) 179 + "--profile=${cfg.profileDir}" 180 + ] 181 + ++ optionals (cfg.webuiPort != null) [ "--webui-port=${toString cfg.webuiPort}" ] 182 + ++ optionals (cfg.torrentingPort != null) [ "--torrenting-port=${toString cfg.torrentingPort}" ] 183 + ++ cfg.extraArgs 184 + ); 185 + TimeoutStopSec = 1800; 186 + 187 + # https://github.com/qbittorrent/qBittorrent/pull/6806#discussion_r121478661 188 + PrivateTmp = false; 189 + 190 + PrivateNetwork = false; 191 + RemoveIPC = true; 192 + NoNewPrivileges = true; 193 + PrivateDevices = true; 194 + PrivateUsers = true; 195 + ProtectHome = "yes"; 196 + ProtectProc = "invisible"; 197 + ProcSubset = "pid"; 198 + ProtectSystem = "full"; 199 + ProtectClock = true; 200 + ProtectHostname = true; 201 + ProtectKernelLogs = true; 202 + ProtectKernelModules = true; 203 + ProtectKernelTunables = true; 204 + ProtectControlGroups = true; 205 + RestrictAddressFamilies = [ 206 + "AF_INET" 207 + "AF_INET6" 208 + "AF_NETLINK" 209 + ]; 210 + RestrictNamespaces = true; 211 + RestrictRealtime = true; 212 + RestrictSUIDSGID = true; 213 + LockPersonality = true; 214 + MemoryDenyWriteExecute = true; 215 + SystemCallArchitectures = "native"; 216 + CapabilityBoundingSet = ""; 217 + SystemCallFilter = [ "@system-service" ]; 218 + }; 219 + }; 220 + }; 221 + 222 + users = { 223 + users = mkIf (cfg.user == "qbittorrent") { 224 + qbittorrent = { 225 + inherit (cfg) group; 226 + isSystemUser = true; 227 + }; 228 + }; 229 + groups = mkIf (cfg.group == "qbittorrent") { qbittorrent = { }; }; 230 + }; 231 + 232 + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall ( 233 + optionals (cfg.webuiPort != null) [ cfg.webuiPort ] 234 + ++ optionals (cfg.torrentingPort != null) [ cfg.torrentingPort ] 235 + ); 236 + }; 237 + meta.maintainers = with maintainers; [ fsnkty ]; 238 + }
+1
nixos/tests/all-tests.nix
··· 1222 1222 public-inbox = runTest ./public-inbox.nix; 1223 1223 pufferpanel = runTest ./pufferpanel.nix; 1224 1224 pulseaudio = discoverTests (import ./pulseaudio.nix); 1225 + qbittorrent = runTest ./qbittorrent.nix; 1225 1226 qboot = handleTestOn [ "x86_64-linux" "i686-linux" ] ./qboot.nix { }; 1226 1227 qemu-vm-restrictnetwork = handleTest ./qemu-vm-restrictnetwork.nix { }; 1227 1228 qemu-vm-volatile-root = runTest ./qemu-vm-volatile-root.nix;
+190
nixos/tests/qbittorrent.nix
··· 1 + { pkgs, lib, ... }: 2 + { 3 + name = "qbittorrent"; 4 + 5 + meta = with pkgs.lib.maintainers; { 6 + maintainers = [ fsnkty ]; 7 + }; 8 + 9 + nodes = { 10 + simple = { 11 + services.qbittorrent.enable = true; 12 + 13 + specialisation.portChange.configuration = { 14 + services.qbittorrent = { 15 + enable = true; 16 + webuiPort = 5555; 17 + torrentingPort = 44444; 18 + }; 19 + }; 20 + 21 + specialisation.openPorts.configuration = { 22 + services.qbittorrent = { 23 + enable = true; 24 + openFirewall = true; 25 + webuiPort = 8080; 26 + torrentingPort = 55555; 27 + }; 28 + }; 29 + 30 + specialisation.serverConfig.configuration = { 31 + services.qbittorrent = { 32 + enable = true; 33 + webuiPort = null; 34 + serverConfig.Preferences.WebUI.Port = "8181"; 35 + }; 36 + }; 37 + }; 38 + # Seperate vm because it's not possible to reboot into a specialisation with 39 + # switch-to-configuration: https://github.com/NixOS/nixpkgs/issues/82851 40 + # For one of the test we check if manual changes are overridden during 41 + # reboot, therefore it's necessary to reboot into a declarative setup. 42 + declarative = { 43 + services.qbittorrent = { 44 + enable = true; 45 + webuiPort = null; 46 + serverConfig = { 47 + Preferences = { 48 + WebUI = { 49 + Username = "user"; 50 + # Default password: adminadmin 51 + Password_PBKDF2 = "@ByteArray(6DIf26VOpTCYbgNiO6DAFQ==:e6241eaAWGzRotQZvVA5/up9fj5wwSAThLgXI2lVMsYTu1StUgX9MgmElU3Sa/M8fs+zqwZv9URiUOObjqJGNw==)"; 52 + Port = lib.mkDefault "8181"; 53 + }; 54 + }; 55 + }; 56 + }; 57 + 58 + specialisation.serverConfigChange.configuration = { 59 + services.qbittorrent = { 60 + enable = true; 61 + webuiPort = null; 62 + serverConfig.Preferences.WebUI.Port = "7171"; 63 + }; 64 + }; 65 + }; 66 + }; 67 + 68 + testScript = 69 + { nodes, ... }: 70 + let 71 + simpleSpecPath = "${nodes.simple.system.build.toplevel}/specialisation"; 72 + declarativeSpecPath = "${nodes.declarative.system.build.toplevel}/specialisation"; 73 + portChange = "${simpleSpecPath}/portChange"; 74 + openPorts = "${simpleSpecPath}/openPorts"; 75 + serverConfig = "${simpleSpecPath}/serverConfig"; 76 + serverConfigChange = "${declarativeSpecPath}/serverConfigChange"; 77 + in 78 + '' 79 + simple.start(allow_reboot=True) 80 + declarative.start(allow_reboot=True) 81 + 82 + 83 + def test_webui(machine, port): 84 + machine.wait_for_unit("qbittorrent.service") 85 + machine.wait_for_open_port(port) 86 + machine.wait_until_succeeds(f"curl --fail http://localhost:{port}") 87 + 88 + 89 + # To simulate an interactive change in the settings 90 + def setPreferences_api(machine, port, post_creds, post_data): 91 + qb_url = f"http://localhost:{port}" 92 + api_url = f"{qb_url}/api/v2" 93 + cookie_path = "/tmp/qbittorrent.cookie" 94 + 95 + machine.succeed( 96 + f'curl --header "Referer: {qb_url}" \ 97 + --data "{post_creds}" {api_url}/auth/login \ 98 + -c {cookie_path}' 99 + ) 100 + machine.succeed( 101 + f'curl --header "Referer: {qb_url}" \ 102 + --data "{post_data}" {api_url}/app/setPreferences \ 103 + -b {cookie_path}' 104 + ) 105 + 106 + 107 + # A randomly generated password is printed in the service log when no 108 + # password it set 109 + def get_temp_pass(machine): 110 + _, password = machine.execute( 111 + "journalctl -u qbittorrent.service |\ 112 + grep 'The WebUI administrator password was not set.' |\ 113 + awk '{ print $NF }' | tr -d '\n'" 114 + ) 115 + return password 116 + 117 + 118 + # Non declarative tests 119 + 120 + with subtest("webui works with all default settings"): 121 + test_webui(simple, 8080) 122 + 123 + with subtest("check if manual changes in settings are saved correctly"): 124 + temp_pass = get_temp_pass(simple) 125 + 126 + ## Change some settings 127 + api_post = [r"json={\"listen_port\": 33333}", r"json={\"web_ui_port\": 9090}"] 128 + for x in api_post: 129 + setPreferences_api( 130 + machine=simple, 131 + port=8080, 132 + post_creds=f"username=admin&password={temp_pass}", 133 + post_data=x, 134 + ) 135 + 136 + simple.wait_for_open_port(33333) 137 + test_webui(simple, 9090) 138 + 139 + ## Test which settings are reset 140 + ## As webuiPort is passed as an cli it should reset after reboot 141 + ## As torrentingPort is not passed as an cli it should not reset after 142 + ## reboot 143 + simple.reboot() 144 + test_webui(simple, 8080) 145 + simple.wait_for_open_port(33333) 146 + 147 + with subtest("ports are changed on config change"): 148 + simple.succeed("${portChange}/bin/switch-to-configuration test") 149 + test_webui(simple, 5555) 150 + simple.wait_for_open_port(44444) 151 + 152 + with subtest("firewall is opened correctly"): 153 + simple.succeed("${openPorts}/bin/switch-to-configuration test") 154 + test_webui(simple, 8080) 155 + declarative.wait_until_succeeds("curl --fail http://simple:8080") 156 + declarative.wait_for_open_port(55555, "simple") 157 + 158 + with subtest("switching from simple to declarative works"): 159 + simple.succeed("${serverConfig}/bin/switch-to-configuration test") 160 + test_webui(simple, 8181) 161 + 162 + 163 + # Declarative tests 164 + 165 + with subtest("serverConfig is applied correctly"): 166 + test_webui(declarative, 8181) 167 + 168 + with subtest("manual changes are overridden during reboot"): 169 + ## Change some settings 170 + setPreferences_api( 171 + machine=declarative, 172 + port=8181, # as set through serverConfig 173 + post_creds="username=user&password=adminadmin", 174 + post_data=r"json={\"web_ui_port\": 9191}", 175 + ) 176 + 177 + test_webui(declarative, 9191) 178 + 179 + ## Test which settings are reset 180 + ## The generated qBittorrent.conf is, apparently, reapplied after reboot. 181 + ## Because the port is set in `serverConfig` this overrides the manually 182 + ## set port. 183 + declarative.reboot() 184 + test_webui(declarative, 8181) 185 + 186 + with subtest("changes in serverConfig are applied correctly"): 187 + declarative.succeed("${serverConfigChange}/bin/switch-to-configuration test") 188 + test_webui(declarative, 7171) 189 + ''; 190 + }
+5 -1
pkgs/by-name/qb/qbittorrent/package.nix
··· 16 16 webuiSupport ? true, 17 17 wrapGAppsHook3, 18 18 zlib, 19 + nixosTests, 19 20 }: 20 21 21 22 stdenv.mkDerivation (finalAttrs: { ··· 74 75 qtWrapperArgs+=("''${gappsWrapperArgs[@]}") 75 76 ''; 76 77 77 - passthru.updateScript = nix-update-script { extraArgs = [ "--version-regex=release-(.*)" ]; }; 78 + passthru = { 79 + updateScript = nix-update-script { extraArgs = [ "--version-regex=release-(.*)" ]; }; 80 + tests.testService = nixosTests.qbittorrent; 81 + }; 78 82 79 83 meta = { 80 84 description = "Featureful free software BitTorrent client";