From 4bbe09068aefe0381e611e1928f08881bd94b05b Mon Sep 17 00:00:00 2001 From: Ivan Trubach Date: Fri, 7 Apr 2023 09:34:25 +0300 Subject: [PATCH] nixos/pufferpanel: init --- .../manual/release-notes/rl-2305.section.md | 2 + nixos/modules/module-list.nix | 1 + nixos/modules/services/misc/pufferpanel.nix | 176 ++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/pufferpanel.nix | 74 ++++++++ 5 files changed, 254 insertions(+) create mode 100644 nixos/modules/services/misc/pufferpanel.nix create mode 100644 nixos/tests/pufferpanel.nix diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index c2c9f2d1a251..e68b5dd027b7 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -81,6 +81,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [ulogd](https://www.netfilter.org/projects/ulogd/index.html), a userspace logging daemon for netfilter/iptables related logging. Available as [services.ulogd](options.html#opt-services.ulogd.enable). +- [PufferPanel](https://pufferpanel.com), game server management panel designed to be easy to use. Available as [services.pufferpanel](#opt-services.pufferpanel.enable). + - [jellyseerr](https://github.com/Fallenbagel/jellyseerr), a web-based requests manager for Jellyfin, forked from Overseerr. Available as [services.jellyseerr](#opt-services.jellyseerr.enable). - [photoprism](https://photoprism.app/), a AI-Powered Photos App for the Decentralized Web. Available as [services.photoprism](options.html#opt-services.photoprism.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index cc464c18ca55..901530098eb9 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -667,6 +667,7 @@ ./services/misc/polaris.nix ./services/misc/portunus.nix ./services/misc/prowlarr.nix + ./services/misc/pufferpanel.nix ./services/misc/pykms.nix ./services/misc/radarr.nix ./services/misc/readarr.nix diff --git a/nixos/modules/services/misc/pufferpanel.nix b/nixos/modules/services/misc/pufferpanel.nix new file mode 100644 index 000000000000..78ec35646907 --- /dev/null +++ b/nixos/modules/services/misc/pufferpanel.nix @@ -0,0 +1,176 @@ +{ config, pkgs, lib, ... }: +let + cfg = config.services.pufferpanel; +in +{ + options.services.pufferpanel = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable PufferPanel game management server. + + Note that [PufferPanel templates] and binaries downloaded by PufferPanel + expect [FHS environment]. It is possible to set {option}`package` option + to use PufferPanel wrapper with FHS environment. For example, to use + `Download Game from Steam` and `Download Java` template operations: + ```Nix + { lib, pkgs, ... }: { + services.pufferpanel = { + enable = true; + extraPackages = with pkgs; [ bash curl gawk gnutar gzip ]; + package = pkgs.buildFHSUserEnv { + name = "pufferpanel-fhs"; + runScript = lib.getExe pkgs.pufferpanel; + targetPkgs = pkgs': with pkgs'; [ icu openssl zlib ]; + }; + }; + } + ``` + + [PufferPanel templates]: https://github.com/PufferPanel/templates + [FHS environment]: https://wikipedia.org/wiki/Filesystem_Hierarchy_Standard + ''; + }; + + package = lib.mkPackageOptionMD pkgs "pufferpanel" { }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "podman" ]; + description = lib.mdDoc '' + Additional groups for the systemd service. + ''; + }; + + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + example = lib.literalExpression "[ pkgs.jre ]"; + description = lib.mdDoc '' + Packages to add to the PATH environment variable. Both the {file}`bin` + and {file}`sbin` subdirectories of each package are added. + ''; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = lib.literalExpression '' + { + PUFFER_WEB_HOST = ":8080"; + PUFFER_DAEMON_SFTP_HOST = ":5657"; + PUFFER_DAEMON_CONSOLE_BUFFER = "1000"; + PUFFER_DAEMON_CONSOLE_FORWARD = "true"; + PUFFER_PANEL_REGISTRATIONENABLED = "false"; + } + ''; + description = lib.mdDoc '' + Environment variables to set for the service. Secrets should be + specified using {option}`environmentFile`. + + Refer to the [PufferPanel source code][] for the list of available + configuration options. Variable name is an upper-cased configuration + entry name with underscores instead of dots, prefixed with `PUFFER_`. + For example, `panel.settings.companyName` entry can be set using + {env}`PUFFER_PANEL_SETTINGS_COMPANYNAME`. + + When running with panel enabled (configured with `PUFFER_PANEL_ENABLE` + environment variable), it is recommended disable registration using + `PUFFER_PANEL_REGISTRATIONENABLED` environment variable (registration is + enabled by default). To create the initial administrator user, run + {command}`pufferpanel --workDir /var/lib/pufferpanel user add --admin`. + + Some options override corresponding settings set via web interface (e.g. + `PUFFER_PANEL_REGISTRATIONENABLED`). Those options can be temporarily + toggled or set in settings but do not persist between restarts. + + [PufferPanel source code]: https://github.com/PufferPanel/PufferPanel/blob/master/config/entries.go + ''; + }; + + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = lib.mdDoc '' + File to load environment variables from. Loaded variables override + values set in {option}`environment`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.pufferpanel = { + description = "PufferPanel game management server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + path = cfg.extraPackages; + environment = cfg.environment; + + # Note that we export environment variables for service directories if the + # value is not set. An empty environment variable is considered to be set. + # E.g. + # export PUFFER_LOGS=${PUFFER_LOGS-$LOGS_DIRECTORY} + # would set PUFFER_LOGS to $LOGS_DIRECTORY if PUFFER_LOGS environment + # variable is not defined. + script = '' + ${lib.concatLines (lib.mapAttrsToList (name: value: '' + export ${name}="''${${name}-${value}}" + '') { + PUFFER_LOGS = "$LOGS_DIRECTORY"; + PUFFER_DAEMON_DATA_CACHE = "$CACHE_DIRECTORY"; + PUFFER_DAEMON_DATA_SERVERS = "$STATE_DIRECTORY/servers"; + PUFFER_DAEMON_DATA_BINARIES = "$STATE_DIRECTORY/binaries"; + })} + exec ${lib.getExe cfg.package} run --workDir "$STATE_DIRECTORY" + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + UMask = "0077"; + + SupplementaryGroups = cfg.extraGroups; + + StateDirectory = "pufferpanel"; + StateDirectoryMode = "0700"; + CacheDirectory = "pufferpanel"; + CacheDirectoryMode = "0700"; + LogsDirectory = "pufferpanel"; + LogsDirectoryMode = "0700"; + + EnvironmentFile = cfg.environmentFile; + + # Command "pufferpanel shutdown --pid $MAINPID" sends SIGTERM (code 15) + # to the main process and waits for termination. This is essentially + # KillMode=mixed we are using here. See + # https://freedesktop.org/software/systemd/man/systemd.kill.html#KillMode= + KillMode = "mixed"; + + DynamicUser = true; + ProtectHome = true; + ProtectProc = "invisible"; + ProtectClock = true; + ProtectHostname = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + PrivateUsers = true; + PrivateDevices = true; + RestrictRealtime = true; + RestrictNamespaces = [ "user" "mnt" ]; # allow buildFHSUserEnv + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + LockPersonality = true; + DeviceAllow = [ "" ]; + DevicePolicy = "closed"; + CapabilityBoundingSet = [ "" ]; + }; + }; + }; + + meta.maintainers = [ lib.maintainers.tie ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0783f3bf68e2..d1b75997a833 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -587,6 +587,7 @@ in { pt2-clone = handleTest ./pt2-clone.nix {}; pykms = handleTest ./pykms.nix {}; public-inbox = handleTest ./public-inbox.nix {}; + pufferpanel = handleTest ./pufferpanel.nix {}; pulseaudio = discoverTests (import ./pulseaudio.nix); qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {}; qemu-vm-restrictnetwork = handleTest ./qemu-vm-restrictnetwork.nix {}; diff --git a/nixos/tests/pufferpanel.nix b/nixos/tests/pufferpanel.nix new file mode 100644 index 000000000000..e7b09c13f90b --- /dev/null +++ b/nixos/tests/pufferpanel.nix @@ -0,0 +1,74 @@ +import ./make-test-python.nix ({ lib, ... }: { + name = "pufferpanel"; + meta.maintainers = [ lib.maintainers.tie ]; + + nodes.machine = { pkgs, ... }: { + environment.systemPackages = [ pkgs.pufferpanel ]; + services.pufferpanel = { + enable = true; + extraPackages = [ pkgs.netcat ]; + environment = { + PUFFER_PANEL_REGISTRATIONENABLED = "false"; + PUFFER_PANEL_SETTINGS_COMPANYNAME = "NixOS"; + }; + }; + }; + + testScript = '' + import shlex + import json + + curl = "curl --fail-with-body --silent" + baseURL = "http://localhost:8080" + adminName = "admin" + adminEmail = "admin@nixos.org" + adminPass = "admin" + adminCreds = json.dumps({ + "email": adminEmail, + "password": adminPass, + }) + stopCode = 9 # SIGKILL + serverPort = 1337 + serverDefinition = json.dumps({ + "name": "netcat", + "node": 0, + "users": [ + adminName, + ], + "type": "netcat", + "run": { + "stopCode": stopCode, + "command": f"nc -l {serverPort}", + }, + "environment": { + "type": "standard", + }, + }) + + start_all() + + machine.wait_for_unit("pufferpanel.service") + machine.wait_for_open_port(5657) # SFTP + machine.wait_for_open_port(8080) # HTTP + + # Note that PufferPanel does not initialize database unless necessary. + # /api/config endpoint creates database file and triggers migrations. + # On success, we run a command to create administrator user that we use to + # interact with HTTP API. + resp = json.loads(machine.succeed(f"{curl} {baseURL}/api/config")) + assert resp["branding"]["name"] == "NixOS", "Invalid company name in configuration" + assert resp["registrationEnabled"] == False, "Expected registration to be disabled" + + machine.succeed(f"pufferpanel --workDir /var/lib/pufferpanel user add --admin --name {adminName} --email {adminEmail} --password {adminPass}") + + resp = json.loads(machine.succeed(f"{curl} -d '{adminCreds}' {baseURL}/auth/login")) + assert "servers.admin" in resp["scopes"], "User is not administrator" + token = resp["session"] + authHeader = shlex.quote(f"Authorization: Bearer {token}") + + resp = json.loads(machine.succeed(f"{curl} -H {authHeader} -H 'Content-Type: application/json' -d '{serverDefinition}' {baseURL}/api/servers")) + serverID = resp["id"] + machine.succeed(f"{curl} -X POST -H {authHeader} {baseURL}/proxy/daemon/server/{serverID}/start") + machine.wait_for_open_port(serverPort) + ''; +})