diff --git a/nixos/modules/programs/atop.nix b/nixos/modules/programs/atop.nix index 7ef8d687ca17..47597a0a4d4b 100644 --- a/nixos/modules/programs/atop.nix +++ b/nixos/modules/programs/atop.nix @@ -1,6 +1,6 @@ # Global configuration for atop. -{ config, lib, ... }: +{ config, lib, pkgs, ... }: with lib; @@ -12,11 +12,83 @@ in options = { - programs.atop = { + programs.atop = rec { + enable = mkEnableOption "Atop"; + + package = mkOption { + type = types.package; + default = pkgs.atop; + description = '' + Which package to use for Atop. + ''; + }; + + netatop = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to install and enable the netatop kernel module. + Note: this sets the kernel taint flag "O" for loading out-of-tree modules. + ''; + }; + package = mkOption { + type = types.package; + default = config.boot.kernelPackages.netatop; + description = '' + Which package to use for netatop. + ''; + }; + }; + + atopgpu.enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to install and enable the atopgpud daemon to get information about + NVIDIA gpus. + ''; + }; + + setuidWrapper.enable = mkOption { + type = types.bool; + default = false; + description = '' + Whether to install a setuid wrapper for Atop. This is required to use some of + the features as non-root user (e.g.: ipc information, netatop, atopgpu). + Atop tries to drop the root privileges shortly after starting. + ''; + }; + + atopService.enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable the atop service responsible for storing statistics for + long-term analysis. + ''; + }; + atopRotateTimer.enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable the atop-rotate timer, which restarts the atop service + daily to make sure the data files are rotate. + ''; + }; + atopacctService.enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable the atopacct service which manages process accounting. + This allows Atop to gather data about processes that disappeared in between + two refresh intervals. + ''; + }; settings = mkOption { type = types.attrs; - default = {}; + default = { }; example = { flags = "a1f"; interval = 5; @@ -25,12 +97,50 @@ in Parameters to be written to /etc/atoprc. ''; }; - }; }; - config = mkIf (cfg.settings != {}) { - environment.etc.atoprc.text = - concatStrings (mapAttrsToList (n: v: "${n} ${toString v}\n") cfg.settings); - }; + config = mkIf cfg.enable ( + let + atop = + if cfg.atopgpu.enable then + (cfg.package.override { withAtopgpu = true; }) + else + cfg.package; + in + { + environment.etc = mkIf (cfg.settings != { }) { + atoprc.text = concatStrings + (mapAttrsToList + (n: v: '' + ${n} ${toString v} + '') + cfg.settings); + }; + environment.systemPackages = [ atop (lib.mkIf cfg.netatop.enable cfg.netatop.package) ]; + boot.extraModulePackages = [ (lib.mkIf cfg.netatop.enable cfg.netatop.package) ]; + systemd = + let + mkSystemd = type: cond: name: restartTriggers: { + ${name} = lib.mkIf cond { + inherit restartTriggers; + wantedBy = [ (if type == "services" then "multi-user.target" else if type == "timers" then "timers.target" else null) ]; + }; + }; + mkService = mkSystemd "services"; + mkTimer = mkSystemd "timers"; + in + { + packages = [ atop (lib.mkIf cfg.netatop.enable cfg.netatop.package) ]; + services = + mkService cfg.atopService.enable "atop" [ atop ] + // mkService cfg.atopacctService.enable "atopacct" [ atop ] + // mkService cfg.netatop.enable "netatop" [ cfg.netatop.package ] + // mkService cfg.atopgpu.enable "atopgpu" [ atop ]; + timers = mkTimer cfg.atopRotateTimer.enable "atop-rotate" [ atop ]; + }; + security.wrappers = + lib.mkIf cfg.setuidWrapper.enable { atop = { source = "${atop}/bin/atop"; }; }; + } + ); } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 2c2ee95788db..f0b050829234 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -29,6 +29,7 @@ in ammonite = handleTest ./ammonite.nix {}; apparmor = handleTest ./apparmor.nix {}; atd = handleTest ./atd.nix {}; + atop = handleTest ./atop.nix {}; avahi = handleTest ./avahi.nix {}; avahi-with-resolved = handleTest ./avahi.nix { networkd = true; }; awscli = handleTest ./awscli.nix { }; diff --git a/nixos/tests/atop.nix b/nixos/tests/atop.nix new file mode 100644 index 000000000000..a2565e610ae1 --- /dev/null +++ b/nixos/tests/atop.nix @@ -0,0 +1,213 @@ +{ system ? builtins.currentSystem +, config ? { } +, pkgs ? import ../.. { inherit system config; } +}: + +with import ../lib/testing-python.nix { inherit system pkgs; }; +with pkgs.lib; + +let assertions = rec { + path = program: path: '' + with subtest("The path of ${program} should be ${path}"): + p = machine.succeed("type -p \"${program}\" | head -c -1") + assert p == "${path}", f"${program} is {p}, expected ${path}" + ''; + unit = name: state: '' + with subtest("Unit ${name} should be ${state}"): + machine.require_unit_state("${name}", "${state}") + ''; + version = '' + import re + + with subtest("binary should report the correct version"): + pkgver = "${pkgs.atop.version}" + ver = re.sub(r'(?s)^Version: (\d\.\d\.\d).*', r'\1', machine.succeed("atop -V")) + assert ver == pkgver, f"Version is `{ver}`, expected `{pkgver}`" + ''; + atoprc = contents: + if builtins.stringLength contents > 0 then '' + with subtest("/etc/atoprc should have the correct contents"): + f = machine.succeed("cat /etc/atoprc") + assert f == "${contents}", f"/etc/atoprc contents: '{f}', expected '${contents}'" + '' else '' + with subtest("/etc/atoprc should not be present"): + machine.succeed("test ! -e /etc/atoprc") + ''; + wrapper = present: + if present then path "atop" "/run/wrappers/bin/atop" + '' + with subtest("Wrapper should be setuid root"): + stat = machine.succeed("stat --printf '%a %u' /run/wrappers/bin/atop") + assert stat == "4511 0", f"Wrapper stat is {stat}, expected '4511 0'" + '' + else path "atop" "/run/current-system/sw/bin/atop"; + atopService = present: + if present then + unit "atop.service" "active" + + '' + with subtest("atop.service should have written some data to /var/log/atop"): + files = int(machine.succeed("ls -1 /var/log/atop | wc -l")) + assert files > 0, "Expected at least 1 data file" + '' else unit "atop.service" "inactive"; + atopRotateTimer = present: + unit "atop-rotate.timer" (if present then "active" else "inactive"); + atopacctService = present: + if present then + unit "atopacct.service" "active" + + '' + with subtest("atopacct.service should enable process accounting"): + machine.succeed("test -f /run/pacct_source") + + with subtest("atopacct.service should write data to /run/pacct_shadow.d"): + files = int(machine.succeed("ls -1 /run/pacct_shadow.d | wc -l")) + assert files >= 1, "Expected at least 1 pacct_shadow.d file" + '' else unit "atopacct.service" "inactive"; + netatop = present: + if present then + unit "netatop.service" "active" + + '' + with subtest("The netatop kernel module should be loaded"): + out = machine.succeed("modprobe -n -v netatop") + assert out == "", f"Module should be loaded already, but modprobe would have done {out}." + '' else '' + with subtest("The netatop kernel module should be absent"): + machine.fail("modprobe -n -v netatop") + ''; + atopgpu = present: + if present then + (unit "atopgpu.service" "active") + (path "atopgpud" "/run/current-system/sw/bin/atopgpud") + else (unit "atopgpu.service" "inactive") + '' + with subtest("atopgpud should not be present"): + machine.fail("type -p atopgpud") + ''; +}; +in +{ + name = "atop"; + + justThePackage = makeTest { + name = "atop-justThePackage"; + machine = { + environment.systemPackages = [ pkgs.atop ]; + }; + testScript = with assertions; builtins.concatStringsSep "\n" [ + version + (atoprc "") + (wrapper false) + (atopService false) + (atopRotateTimer false) + (atopacctService false) + (netatop false) + (atopgpu false) + ]; + }; + defaults = makeTest { + name = "atop-defaults"; + machine = { + programs.atop = { + enable = true; + }; + }; + testScript = with assertions; builtins.concatStringsSep "\n" [ + version + (atoprc "") + (wrapper false) + (atopService true) + (atopRotateTimer true) + (atopacctService true) + (netatop false) + (atopgpu false) + ]; + }; + minimal = makeTest { + name = "atop-minimal"; + machine = { + programs.atop = { + enable = true; + atopService.enable = false; + atopRotateTimer.enable = false; + atopacctService.enable = false; + }; + }; + testScript = with assertions; builtins.concatStringsSep "\n" [ + version + (atoprc "") + (wrapper false) + (atopService false) + (atopRotateTimer false) + (atopacctService false) + (netatop false) + (atopgpu false) + ]; + }; + netatop = makeTest { + name = "atop-netatop"; + machine = { + programs.atop = { + enable = true; + netatop.enable = true; + }; + }; + testScript = with assertions; builtins.concatStringsSep "\n" [ + version + (atoprc "") + (wrapper false) + (atopService true) + (atopRotateTimer true) + (atopacctService true) + (netatop true) + (atopgpu false) + ]; + }; + atopgpu = makeTest { + name = "atop-atopgpu"; + machine = { + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (getName pkg) [ + "cudatoolkit" + ]; + + programs.atop = { + enable = true; + atopgpu.enable = true; + }; + }; + testScript = with assertions; builtins.concatStringsSep "\n" [ + version + (atoprc "") + (wrapper false) + (atopService true) + (atopRotateTimer true) + (atopacctService true) + (netatop false) + (atopgpu true) + ]; + }; + everything = makeTest { + name = "atop-everthing"; + machine = { + nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (getName pkg) [ + "cudatoolkit" + ]; + + programs.atop = { + enable = true; + settings = { + flags = "faf1"; + interval = 2; + }; + setuidWrapper.enable = true; + netatop.enable = true; + atopgpu.enable = true; + }; + }; + testScript = with assertions; builtins.concatStringsSep "\n" [ + version + (atoprc "flags faf1\\ninterval 2\\n") + (wrapper true) + (atopService true) + (atopRotateTimer true) + (atopacctService true) + (netatop true) + (atopgpu true) + ]; + }; +} diff --git a/pkgs/os-specific/linux/atop/atop.service.patch b/pkgs/os-specific/linux/atop/atop.service.patch new file mode 100644 index 000000000000..3ef59e60cbc0 --- /dev/null +++ b/pkgs/os-specific/linux/atop/atop.service.patch @@ -0,0 +1,10 @@ +--- a/atop.service ++++ b/atop.service +@@ -9,5 +9,6 @@ + Environment=LOGPATH=/var/log/atop +-EnvironmentFile=/etc/default/atop ++EnvironmentFile=-/etc/default/atop + ExecStartPre=/bin/sh -c 'test -n "$LOGINTERVAL" -a "$LOGINTERVAL" -eq "$LOGINTERVAL"' + ExecStartPre=/bin/sh -c 'test -n "$LOGGENERATIONS" -a "$LOGGENERATIONS" -eq "$LOGGENERATIONS"' ++ExecStartPre=/bin/sh -c 'mkdir -p "${LOGPATH}"' + ExecStart=/bin/sh -c 'exec @out@/bin/atop ${LOGOPTS} -w "${LOGPATH}/atop_$(date +%%Y%%m%%d)" ${LOGINTERVAL}' diff --git a/pkgs/os-specific/linux/atop/atopacct.service.patch b/pkgs/os-specific/linux/atop/atopacct.service.patch new file mode 100644 index 000000000000..9f2cd8f2e9ca --- /dev/null +++ b/pkgs/os-specific/linux/atop/atopacct.service.patch @@ -0,0 +1,7 @@ +--- a/atopacct.service ++++ b/atopacct.service +@@ -9,3 +9,3 @@ + Type=forking +-PIDFile=/var/run/atopacctd.pid ++PIDFile=/run/atopacctd.pid + ExecStart=@out@/bin/atopacctd diff --git a/pkgs/os-specific/linux/atop/default.nix b/pkgs/os-specific/linux/atop/default.nix index e1b64c0a4b5c..50a3e3e63168 100644 --- a/pkgs/os-specific/linux/atop/default.nix +++ b/pkgs/os-specific/linux/atop/default.nix @@ -1,4 +1,14 @@ -{lib, stdenv, fetchurl, zlib, ncurses}: +{ lib +, stdenv +, fetchurl +, zlib +, ncurses +, findutils +, systemd +, python3 +# makes the package unfree via pynvml +, withAtopgpu ? false +}: stdenv.mkDerivation rec { pname = "atop"; @@ -9,31 +19,52 @@ stdenv.mkDerivation rec { sha256 = "nsLKOlcWkvfvqglfmaUQZDK8txzCLNbElZfvBIEFj3I="; }; - buildInputs = [zlib ncurses]; + nativeBuildInputs = lib.optionals withAtopgpu [ python3.pkgs.wrapPython ]; + buildInputs = [ zlib ncurses ] ++ lib.optionals withAtopgpu [ python3 ]; + pythonPath = lib.optionals withAtopgpu [ python3.pkgs.pynvml ]; makeFlags = [ - "SCRPATH=$out/etc/atop" - "LOGPATH=/var/log/atop" - "INIPATH=$out/etc/rc.d/init.d" - "SYSDPATH=$out/lib/systemd/system" - "CRNPATH=$out/etc/cron.d" - "DEFPATH=$out/etc/default" - "ROTPATH=$out/etc/logrotate.d" + "DESTDIR=$(out)" + "BINPATH=/bin" + "SBINPATH=/bin" + "MAN1PATH=/share/man/man1" + "MAN5PATH=/share/man/man5" + "MAN8PATH=/share/man/man8" + "SYSDPATH=/lib/systemd/system" + "PMPATHD=/lib/systemd/system-sleep" + ]; + + patches = [ + # Fix paths in atop.service, atop-rotate.service, atopgpu.service, atopacct.service, + # and atop-pm.sh + ./fix-paths.patch + # Don't fail on missing /etc/default/atop, make sure /var/log/atop exists pre-start + ./atop.service.patch + # Specify PIDFile in /run, not /var/run to silence systemd warning + ./atopacct.service.patch ]; preConfigure = '' - sed -e "s@/usr/@$out/@g" -i $(find . -type f ) - sed -e "/mkdir.*LOGPATH/s@mkdir@echo missing dir @" -i Makefile - sed -e "/touch.*LOGPATH/s@touch@echo should have created @" -i Makefile - sed -e 's/chown/true/g' -i Makefile - sed -e '/chkconfig/d' -i Makefile - sed -e 's/chmod 04711/chmod 0711/g' -i Makefile + for f in *.{sh,service}; do + findutils=${findutils} systemd=${systemd} substituteAllInPlace "$f" + done + + substituteInPlace Makefile --replace 'chown' 'true' + substituteInPlace Makefile --replace 'chmod 04711' 'chmod 0711' ''; installTargets = [ "systemdinstall" ]; preInstall = '' - mkdir -p "$out"/{bin,sbin} + mkdir -p $out/bin ''; + postInstall = '' + # remove extra files we don't need + rm -r $out/{var,etc} $out/bin/atop{sar,}-${version} + '' + (if withAtopgpu then '' + wrapPythonPrograms + '' else '' + rm $out/lib/systemd/system/atopgpu.service $out/bin/atopgpud $out/share/man/man8/atopgpud.8 + ''); meta = with lib; { platforms = platforms.linux; diff --git a/pkgs/os-specific/linux/atop/fix-paths.patch b/pkgs/os-specific/linux/atop/fix-paths.patch new file mode 100644 index 000000000000..e6cd631d3c11 --- /dev/null +++ b/pkgs/os-specific/linux/atop/fix-paths.patch @@ -0,0 +1,48 @@ +--- a/atop.service ++++ b/atop.service +@@ -12,4 +12,4 @@ + ExecStartPre=/bin/sh -c 'test -n "$LOGGENERATIONS" -a "$LOGGENERATIONS" -eq "$LOGGENERATIONS"' +-ExecStart=/bin/sh -c 'exec /usr/bin/atop ${LOGOPTS} -w "${LOGPATH}/atop_$(date +%%Y%%m%%d)" ${LOGINTERVAL}' +-ExecStartPost=/usr/bin/find "${LOGPATH}" -name "atop_*" -mtime +${LOGGENERATIONS} -exec rm -v {} \; ++ExecStart=/bin/sh -c 'exec @out@/bin/atop ${LOGOPTS} -w "${LOGPATH}/atop_$(date +%%Y%%m%%d)" ${LOGINTERVAL}' ++ExecStartPost=@findutils@/bin/find "${LOGPATH}" -name "atop_*" -mtime +${LOGGENERATIONS} -exec rm -v {} \; + KillSignal=SIGUSR2 + +--- a/atop-rotate.service ++++ b/atop-rotate.service +@@ -4,3 +4,3 @@ + [Service] + Type=oneshot +-ExecStart=/usr/bin/systemctl try-restart atop.service ++ExecStart=@systemd@/bin/systemctl try-restart atop.service + +--- a/atopgpu.service ++++ b/atopgpu.service +@@ -6,5 +6,5 @@ + + [Service] +-ExecStart=/usr/sbin/atopgpud ++ExecStart=@out@/bin/atopgpud + Type=oneshot + RemainAfterExit=yes + +--- a/atopacct.service ++++ b/atopacct.service +@@ -10,3 +10,3 @@ + PIDFile=/var/run/atopacctd.pid +-ExecStart=/usr/sbin/atopacctd ++ExecStart=@out@/bin/atopacctd + +--- a/atop-pm.sh ++++ b/atop-pm.sh +@@ -2,8 +2,8 @@ + + case "$1" in +- pre) /usr/bin/systemctl stop atop ++ pre) @systemd@/bin/systemctl stop atop + exit 0 + ;; +- post) /usr/bin/systemctl start atop ++ post) @systemd@/bin/systemctl start atop + exit 0 + ;; diff --git a/pkgs/os-specific/linux/netatop/default.nix b/pkgs/os-specific/linux/netatop/default.nix index fb0a4eb71887..28f989929a4c 100644 --- a/pkgs/os-specific/linux/netatop/default.nix +++ b/pkgs/os-specific/linux/netatop/default.nix @@ -1,4 +1,4 @@ -{ lib, stdenv, fetchurl, kernel, zlib }: +{ lib, stdenv, fetchurl, kernel, kmod, zlib }: let version = "3.1"; @@ -12,10 +12,16 @@ stdenv.mkDerivation { sha256 = "0qjw8glfdmngfvbn1w63q128vxdz2jlabw13y140ga9i5ibl6vvk"; }; - buildInputs = [ zlib ]; + buildInputs = [ kmod zlib ]; hardeningDisable = [ "pic" ]; + patches = [ + # fix paths in netatop.service + ./fix-paths.patch + # Specify PIDFile in /run, not /var/run to silence systemd warning + ./netatop.service.patch + ]; preConfigure = '' patchShebangs mkversion sed -i -e 's,^KERNDIR.*,KERNDIR=${kernel.dev}/lib/modules/${kernel.modDirVersion}/build,' \ @@ -24,12 +30,14 @@ stdenv.mkDerivation { -e s,/usr,$out, \ -e /init.d/d \ -e /depmod/d \ - -e /netatop.service/d \ + -e s,/lib/systemd,$out/lib/systemd, \ Makefile + + kmod=${kmod} substituteAllInPlace netatop.service ''; preInstall = '' - mkdir -p $out/bin $out/sbin $out/share/man/man{4,8} + mkdir -p $out/lib/systemd/system $out/bin $out/sbin $out/share/man/man{4,8} mkdir -p $out/lib/modules/${kernel.modDirVersion}/extra ''; @@ -38,6 +46,6 @@ stdenv.mkDerivation { homepage = "https://www.atoptool.nl/downloadnetatop.php"; license = lib.licenses.gpl2; platforms = lib.platforms.linux; - maintainers = with lib.maintainers; [viric]; + maintainers = with lib.maintainers; [ viric ]; }; } diff --git a/pkgs/os-specific/linux/netatop/fix-paths.patch b/pkgs/os-specific/linux/netatop/fix-paths.patch new file mode 100644 index 000000000000..0e71c4efdd31 --- /dev/null +++ b/pkgs/os-specific/linux/netatop/fix-paths.patch @@ -0,0 +1,11 @@ +--- a/netatop.service ++++ b/netatop.service +@@ -8,5 +8,5 @@ + Type=oneshot +-ExecStartPre=/sbin/modprobe netatop +-ExecStart=/usr/sbin/netatopd +-ExecStopPost=/sbin/rmmod netatop ++ExecStartPre=@kmod@/bin/modprobe netatop ++ExecStart=@out@/bin/netatopd ++ExecStopPost=@kmod@/bin/rmmod netatop + PIDFile=/var/run/netatop.pid diff --git a/pkgs/os-specific/linux/netatop/netatop.service.patch b/pkgs/os-specific/linux/netatop/netatop.service.patch new file mode 100644 index 000000000000..c7c798ee06bc --- /dev/null +++ b/pkgs/os-specific/linux/netatop/netatop.service.patch @@ -0,0 +1,7 @@ +--- a/netatop.service ++++ b/netatop.service +@@ -11,3 +11,3 @@ + ExecStopPost=@kmod@/bin/rmmod netatop +-PIDFile=/var/run/netatop.pid ++PIDFile=/run/netatop.pid + RemainAfterExit=yes