From 9508002438eb2b843baa092127df693b6d05b6a8 Mon Sep 17 00:00:00 2001 From: "Burfeind, Jan-Niklas" Date: Thu, 12 Mar 2026 18:11:31 +0100 Subject: [PATCH] nixos/pdudaemon: init module based on the example in share/ in the project repo. --- nixos/modules/module-list.nix | 1 + nixos/modules/services/hardware/pdudaemon.nix | 146 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/pdudaemon.nix | 50 ++++++ 4 files changed, 198 insertions(+) create mode 100644 nixos/modules/services/hardware/pdudaemon.nix create mode 100644 nixos/tests/pdudaemon.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 54ebc0b65767..238d7d74a954 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -686,6 +686,7 @@ ./services/hardware/nvidia-optimus.nix ./services/hardware/openrgb.nix ./services/hardware/pcscd.nix + ./services/hardware/pdudaemon.nix ./services/hardware/pid-fan-controller.nix ./services/hardware/pommed.nix ./services/hardware/power-profiles-daemon.nix diff --git a/nixos/modules/services/hardware/pdudaemon.nix b/nixos/modules/services/hardware/pdudaemon.nix new file mode 100644 index 000000000000..3f400848436e --- /dev/null +++ b/nixos/modules/services/hardware/pdudaemon.nix @@ -0,0 +1,146 @@ +{ + config, + pkgs, + lib, + ... +}: + +let + cfg = config.services.pdudaemon; + configFile = pkgs.writeText "pdudaemon.conf" ( + lib.generators.toJSON { } { + daemon = { + hostname = cfg.bindAddress; + port = cfg.port; + logging_level = cfg.logLevel; + listener = cfg.listener; + }; + pdus = cfg.pdus; + } + ); +in +{ + meta = { + maintainers = with lib.maintainers; [ + aiyion + emantor + ]; + }; + + options = { + services.pdudaemon = { + enable = lib.mkEnableOption "PDUDaemon"; + + package = lib.mkPackageOption pkgs "pdudaemon" { }; + + bindAddress = lib.mkOption { + default = "0.0.0.0"; + type = lib.types.str; + description = "Bind address for the PDUDaemon."; + }; + + port = lib.mkOption { + default = 16421; + type = lib.types.port; + description = "Port to bind to."; + }; + + openFirewall = lib.mkOption { + default = false; + type = lib.types.bool; + description = '' + Whether to automatically open the PDUDaemon listen port in the firewall. + ''; + }; + + listener = lib.mkOption { + default = "http"; + type = lib.types.enum [ + "http" + "tcp" + ]; + description = "Which kind of listener to provide."; + }; + + logLevel = lib.mkOption { + default = "error"; + type = lib.types.enum [ + "debug" + "info" + "warning" + "error" + ]; + description = "PDUDaemon log level."; + }; + + pdus = lib.mkOption { + type = with lib.types; attrsOf anything; + default = { }; + description = '' + Structural pdus section of PDUDaemon's pdudaemon.conf. + Refer to + for more examples. + ''; + example = lib.literalExpression '' + { + cbs350-poe-switch = { + driver = "snmpv1"; + community = "private"; + oid = ".1.3.6.1.2.1.105.1.1.1.3.1.*; + onsetting = 1; + offsetting = 2; + }; + energenie = { + driver = "EG-PMS"; + device = "aa:bb:cc:xx:yy"; + }; + local = { + driver = "localcmdline"; + }; + }; + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [ cfg.port ]; + + systemd.services.pdudaemon = { + after = [ "network-online.target" ]; + description = "Control and Queueing daemon for PDUs"; + serviceConfig = { + ExecStart = "${lib.getExe cfg.package} --conf ${configFile}"; + Type = "simple"; + DynamicUser = "yes"; + StateDirectory = "pdudaemon"; + ProtectHome = true; + Restart = "on-failure"; + CapabilityBoundingSet = ""; + PrivateDevices = true; + ProtectClock = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + SystemCallArchitectures = "native"; + MemoryDenyWriteExecute = true; + RestrictNamespaces = true; + ProtectHostname = true; + LockPersonality = true; + ProtectKernelTunables = true; + RestrictRealtime = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + PrivateUsers = true; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + ]; + }; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a797d5466611..f958d2876438 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1259,6 +1259,7 @@ in inherit runTest; }; pdns-recursor = runTest ./pdns-recursor.nix; + pdudaemon = runTest ./pdudaemon.nix; peerflix = runTest ./peerflix.nix; peering-manager = runTest ./web-apps/peering-manager.nix; peertube = handleTestOn [ "x86_64-linux" ] ./web-apps/peertube.nix { }; diff --git a/nixos/tests/pdudaemon.nix b/nixos/tests/pdudaemon.nix new file mode 100644 index 000000000000..71fb798f886e --- /dev/null +++ b/nixos/tests/pdudaemon.nix @@ -0,0 +1,50 @@ +{ pkgs, ... }: +{ + name = "PDUDaemon"; + meta.maintainers = with pkgs.lib.maintainers; [ + aiyion + emantor + ]; + + nodes.pdudaemonhost = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.curl ]; + services.pdudaemon.enable = true; + services.pdudaemon.openFirewall = true; + services.pdudaemon.pdus = { + testpduhost = { + driver = "localcmdline"; + cmd_on = "echo '%s on' >> /tmp/pdu"; + cmd_off = "echo '%s off' >> /tmp/pdu"; + }; + }; + }; + + nodes.clienthost = + { pkgs, ... }: + { + environment.systemPackages = [ pkgs.curl ]; + }; + + testScript = + { nodes, ... }: + #python + '' + with subtest("Wait for pdudaemon startup"): + pdudaemonhost.start() + pdudaemonhost.wait_for_unit("pdudaemon.service") + pdudaemonhost.wait_for_open_port(16421) + print(pdudaemonhost.succeed("curl 'http://localhost:16421/power/control/on?hostname=testpduhost&port=1'")) + + with subtest("Connect from client"): + clienthost.start() + clienthost.wait_until_succeeds("curl 'http://pdudaemonhost:16421/power/control/off?hostname=testpduhost&port=1'") + + with subtest("Check systemd hardening does not degrade unnoticed"): + exact_threshold = 15 + service_name = "pdudaemon" + pdudaemonhost.fail(f"systemd-analyze security {service_name}.service --threshold={exact_threshold-1}") + pdudaemonhost.succeed(f"systemd-analyze security {service_name}.service --threshold={exact_threshold}") + ''; +}