diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 5293bce9b46f79..10b7dd378de1c0 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -429,6 +429,10 @@ - `services.netbird.tunnels` was renamed to [`services.netbird.clients`](#opt-services.netbird.clients), hardened (using dedicated less-privileged users) and significantly extended. +- The `octoprint` service has gained an `enableRaspberryPi` option, which will + be disabled for state versions following 25.05. Users running on Raspberry Pi + should enable the option to restore full functionality. + ## Other Notable Changes {#sec-release-25.05-notable-changes} diff --git a/nixos/modules/services/misc/octoprint.nix b/nixos/modules/services/misc/octoprint.nix index 193e4222a37e7e..443f29d310abdf 100644 --- a/nixos/modules/services/misc/octoprint.nix +++ b/nixos/modules/services/misc/octoprint.nix @@ -6,17 +6,22 @@ }: let - cfg = config.services.octoprint; + inherit (lib) + literalExpression + mkDefault + mkEnableOption + mkOption + mkRenamedOptionModule + optional + types + versionOlder + ; - baseConfig = { - plugins.curalegacy.cura_engine = "${pkgs.curaengine_stable}/bin/CuraEngine"; - server.port = cfg.port; - webcam.ffmpeg = "${pkgs.ffmpeg.bin}/bin/ffmpeg"; - } // lib.optionalAttrs (cfg.host != null) { server.host = cfg.host; }; + cfg = config.services.octoprint; - fullConfig = lib.recursiveUpdate cfg.extraConfig baseConfig; + formatType = pkgs.formats.json { }; - cfgUpdate = pkgs.writeText "octoprint-config.yaml" (builtins.toJSON fullConfig); + configFile = formatType.generate "octoprint-config.yaml" cfg.settings; pluginsEnv = package.python.withPackages (ps: [ ps.octoprint ] ++ (cfg.plugins ps)); @@ -72,18 +77,32 @@ in description = "State directory of the daemon."; }; - plugins = lib.mkOption { - type = lib.types.functionTo (lib.types.listOf lib.types.package); - default = plugins: [ ]; - defaultText = lib.literalExpression "plugins: []"; - example = lib.literalExpression "plugins: with plugins; [ themeify stlviewer ]"; + plugins = mkOption { + type = types.functionTo (types.listOf types.package); + default = _plugins: [ ]; + defaultText = literalExpression "plugins: []"; + example = literalExpression "plugins: with plugins; [ themeify stlviewer ]"; description = "Additional plugins to be used. Available plugins are passed through the plugins input."; }; - extraConfig = lib.mkOption { - type = lib.types.attrs; + settings = mkOption { default = { }; - description = "Extra options which are added to OctoPrint's YAML configuration file."; + description = '' + The octoprint settings, for definitions see the upstream [documentation](https://docs.octoprint.org). + Will override any existing settings. + ''; + type = types.submodule { + freeformType = formatType.type; + config = { + plugins.curalegacy.cura_engine = mkDefault "${pkgs.curaengine_stable}/bin/CuraEngine"; + server.host = cfg.host; + server.port = cfg.port; + webcam.ffmpeg = mkDefault "${pkgs.ffmpeg.bin}/bin/ffmpeg"; + }; + }; + }; + enableRaspberryPi = mkEnableOption "RaspberryPi specific hardware access rules" // { + default = versionOlder config.system.stateVersion "25.05"; }; }; @@ -91,6 +110,20 @@ in }; ##### implementation + imports = [ + (mkRenamedOptionModule + [ + "services" + "octoprint" + "extraConfig" + ] + [ + "services" + "octoprint" + "settings" + ] + ) + ]; config = lib.mkIf cfg.enable { @@ -105,12 +138,13 @@ in octoprint.gid = config.ids.gids.octoprint; }; - systemd.tmpfiles.rules = [ - "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -" - # this will allow octoprint access to raspberry specific hardware to check for throttling - # read-only will not work: "VCHI initialization failed" error - "a /dev/vchiq - - - - u:octoprint:rw" - ]; + systemd.tmpfiles.rules = + [ "d '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -" ] + ++ optional cfg.enableRaspberryPi + # this will allow octoprint access to raspberry specific hardware to check for throttling + # read-only will not work: "VCHI initialization failed" error + # FIXME: this should probably be a udev rule + "a /dev/vchiq - - - - u:octoprint:rw"; systemd.services.octoprint = { description = "OctoPrint, web interface for 3D printers"; @@ -120,10 +154,10 @@ in preStart = '' if [ -e "${cfg.stateDir}/config.yaml" ]; then - ${pkgs.yaml-merge}/bin/yaml-merge "${cfg.stateDir}/config.yaml" "${cfgUpdate}" > "${cfg.stateDir}/config.yaml.tmp" + ${pkgs.yaml-merge}/bin/yaml-merge "${cfg.stateDir}/config.yaml" "${configFile}" > "${cfg.stateDir}/config.yaml.tmp" mv "${cfg.stateDir}/config.yaml.tmp" "${cfg.stateDir}/config.yaml" else - cp "${cfgUpdate}" "${cfg.stateDir}/config.yaml" + cp "${configFile}" "${cfg.stateDir}/config.yaml" chmod 600 "${cfg.stateDir}/config.yaml" fi ''; @@ -135,9 +169,41 @@ in SupplementaryGroups = [ "dialout" ]; + + # Hardening + CapabilityBoundingSet = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "@pkey" + ]; + ReadWritePaths = [ cfg.stateDir ]; + UMask = "0077"; + }; }; networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; }; + meta.maintainers = with lib.maintainers; [ patrickdag ]; } diff --git a/nixos/tests/octoprint.nix b/nixos/tests/octoprint.nix index 9473797d504754..4499e5e67674a7 100644 --- a/nixos/tests/octoprint.nix +++ b/nixos/tests/octoprint.nix @@ -35,33 +35,40 @@ import ./make-test-python.nix ( }; testScript = '' - import json + import json - @polling_condition - def octoprint_running(): - machine.succeed("pgrep octoprint") + @polling_condition + def octoprint_running(): + machine.succeed("pgrep octoprint") - with subtest("Wait for octoprint service to start"): - machine.wait_for_unit("octoprint.service") - machine.wait_until_succeeds("pgrep octoprint") + with subtest("Wait for octoprint service to start"): + machine.wait_for_unit("octoprint.service") + machine.wait_until_succeeds("pgrep octoprint") - with subtest("Wait for final boot"): - # this appears whe octoprint is almost finished starting - machine.wait_for_file("/var/lib/octoprint/uploads") + with subtest("Wait for final boot"): + # this appears whe octoprint is almost finished starting + machine.wait_for_file("/var/lib/octoprint/uploads") - # octoprint takes some time to start. This makes sure we'll retry just in case it takes longer - # retry-all-errors in necessary, since octoprint will report a 404 error when not yet ready - curl_cmd = "curl --retry-all-errors --connect-timeout 5 --max-time 10 --retry 5 --retry-delay 0 \ - --retry-max-time 40 -X GET --header 'X-API-Key: ${apikey}' " + # octoprint takes some time to start. This makes sure we'll retry just in case it takes longer + # retry-all-errors in necessary, since octoprint will report a 404 error when not yet ready + curl_cmd = "curl --retry-all-errors --connect-timeout 5 --max-time 10 --retry 5 --retry-delay 0 \ + --retry-max-time 40 -X GET --header 'X-API-Key: ${apikey}' " # used to fail early, in case octoprint first starts and then crashes with octoprint_running: # type: ignore[union-attr] with subtest("Check for web interface"): - machine.wait_until_succeeds("curl -s localhost:5000") + machine.wait_until_succeeds("curl -s -4 localhost:5000") + machine.wait_until_succeeds("curl -s -6 localhost:5000") - with subtest("Check API"): - version = json.loads(machine.succeed(curl_cmd + "localhost:5000/api/version")) - server = json.loads(machine.succeed(curl_cmd + "localhost:5000/api/server")) + with subtest("Check API IPv4"): + version = json.loads(machine.succeed(curl_cmd + "-4 localhost:5000/api/version")) + server = json.loads(machine.succeed(curl_cmd + "-4 localhost:5000/api/server")) + assert version["server"] == str("${pkgs.octoprint.version}") + assert server["safemode"] == None + + with subtest("Check API IPv6"): + version = json.loads(machine.succeed(curl_cmd + "-6 localhost:5000/api/version")) + server = json.loads(machine.succeed(curl_cmd + "-6 localhost:5000/api/server")) assert version["server"] == str("${pkgs.octoprint.version}") assert server["safemode"] == None ''; diff --git a/pkgs/by-name/oc/octoprint/ffmpeg-path.patch b/pkgs/by-name/oc/octoprint/ffmpeg-path.patch deleted file mode 100644 index 2e7c7dbe06428f..00000000000000 --- a/pkgs/by-name/oc/octoprint/ffmpeg-path.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/src/octoprint/server/api/settings.py b/src/octoprint/server/api/settings.py -index c3e6cea10..ced2f8fa0 100644 ---- a/src/octoprint/server/api/settings.py -+++ b/src/octoprint/server/api/settings.py -@@ -130,7 +130,7 @@ data["webcam"] = { - "webcamEnabled": s.getBoolean(["webcam", "webcamEnabled"]), - "snapshotTimeout": s.getInt(["webcam", "snapshotTimeout"]), - "timelapseEnabled": s.getBoolean(["webcam", "timelapseEnabled"]), -- "ffmpegPath": s.get(["webcam", "ffmpeg"]), -+ "ffmpegPath": "@ffmpeg@", - "ffmpegCommandline": s.get(["webcam", "ffmpegCommandline"]), - "bitrate": s.get(["webcam", "bitrate"]), - "ffmpegThreads": s.get(["webcam", "ffmpegThreads"]), -@@ -548,8 +548,6 @@ def _saveSettings(data): - ["webcam", "snapshotSslValidation"], - data["webcam"]["snapshotSslValidation"], - ) -- if "ffmpegPath" in data["webcam"]: -- s.set(["webcam", "ffmpeg"], data["webcam"]["ffmpegPath"]) - if "ffmpegCommandline" in data["webcam"]: - commandline = data["webcam"]["ffmpegCommandline"] - if not all( diff --git a/pkgs/by-name/oc/octoprint/package.nix b/pkgs/by-name/oc/octoprint/package.nix index 4a9d9b5d02b7a4..5eef44e0b375c2 100644 --- a/pkgs/by-name/oc/octoprint/package.nix +++ b/pkgs/by-name/oc/octoprint/package.nix @@ -1,5 +1,4 @@ { - pkgs, stdenv, callPackage, lib, @@ -197,11 +196,6 @@ let (replaceVars ./pip-path.patch { pip = "${self.pip}/bin/pip"; }) - - # hardcore path to ffmpeg and hide related settings - (replaceVars ./ffmpeg-path.patch { - ffmpeg = "${pkgs.ffmpeg}/bin/ffmpeg"; - }) ]; postPatch = @@ -265,6 +259,7 @@ let gebner WhittlesJr gador + patrickdag ]; }; };