From 1dc5eb13b0fe25ce66928869821997d8202d240d Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sat, 25 Nov 2023 13:06:11 -0800 Subject: nixos/armagetronad: add module with tests --- nixos/modules/services/games/armagetronad.nix | 221 ++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 nixos/modules/services/games/armagetronad.nix (limited to 'nixos/modules/services') diff --git a/nixos/modules/services/games/armagetronad.nix b/nixos/modules/services/games/armagetronad.nix new file mode 100644 index 000000000000..64b8cb23057e --- /dev/null +++ b/nixos/modules/services/games/armagetronad.nix @@ -0,0 +1,221 @@ +{ config, lib, pkgs, ... }: +let + inherit (lib) mkEnableOption mkIf mkOption mkMerge literalExpression; + inherit (lib) mapAttrsToList filterAttrs unique recursiveUpdate types; + + mkValueStringArmagetron = with lib; v: + if isInt v then toString v + else if isFloat v then toString v + else if isString v then v + else if true == v then "1" + else if false == v then "0" + else if null == v then "" + else throw "unsupported type: ${builtins.typeOf v}: ${(lib.generators.toPretty {} v)}"; + + settingsFormat = pkgs.formats.keyValue { + mkKeyValue = lib.generators.mkKeyValueDefault + { + mkValueString = mkValueStringArmagetron; + } " "; + listsAsDuplicateKeys = true; + }; + + cfg = config.services.armagetronad; + enabledServers = lib.filterAttrs (n: v: v.enable) cfg.servers; + nameToId = serverName: "armagetronad-${serverName}"; +in +{ + options = { + services.armagetronad = { + servers = mkOption { + description = lib.mdDoc "Armagetron server definitions."; + default = { }; + type = types.attrsOf (types.submodule { + options = { + enable = mkEnableOption (lib.mdDoc "armagetronad"); + package = lib.mkPackageOptionMD pkgs "armagetronad-dedicated" { + example = '' + pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated + ''; + extraDescription = '' + Ensure that you use a derivation whose evaluation contains the path `bin/armagetronad-dedicated`. + ''; + }; + host = mkOption { + type = types.str; + default = "0.0.0.0"; + description = lib.mdDoc "Host to listen on. Used for SERVER_IP."; + }; + port = mkOption { + type = types.port; + default = 4534; + description = lib.mdDoc "Port to listen on. Used for SERVER_PORT."; + }; + dns = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc "DNS address to use for this server. Optional."; + }; + openFirewall = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Set to true to open a UDP port for Armagetron Advanced."; + }; + name = mkOption { + type = types.str; + description = "The name of this server."; + }; + settings = mkOption { + type = settingsFormat.type; + default = { }; + description = lib.mdDoc '' + Armagetron Advanced server rules configuration. Refer to: + + or `armagetronad-dedicated --doc` for a list. + + This attrset is used to populate `settings_custom.cfg`; see: + + ''; + example = literalExpression '' + { + CYCLE_RUBBER = 40; + } + ''; + }; + roundSettings = mkOption { + type = settingsFormat.type; + default = { }; + description = lib.mdDoc '' + Armagetron Advanced server per-round configuration. Refer to: + + or `armagetronad-dedicated --doc` for a list. + + This attrset is used to populate `everytime.cfg`; see: + + ''; + example = literalExpression '' + { + SAY = [ + "Hosted on NixOS" + "https://nixos.org" + "iD Tech High Rubber rul3z!! Happy New Year 2008!!1" + ]; + } + ''; + }; + }; + }); + }; + }; + }; + + config = mkIf (enabledServers != { }) { + systemd.services = mkMerge (mapAttrsToList + (serverName: serverCfg: + let + serverId = nameToId serverName; + serverInfo = ( + { + SERVER_IP = serverCfg.host; + SERVER_PORT = serverCfg.port; + SERVER_NAME = serverCfg.name; + } // ( + if serverCfg.dns != null then { SERVER_DNS = serverCfg.dns; } + else { } + ) + ); + customSettings = serverCfg.settings; + everytimeSettings = serverCfg.roundSettings; + + serverInfoCfg = settingsFormat.generate "server_info.${serverName}.cfg" serverInfo; + customSettingsCfg = settingsFormat.generate "settings_custom.${serverName}.cfg" customSettings; + everytimeSettingsCfg = settingsFormat.generate "everytime.${serverName}.cfg" everytimeSettings; + in + { + "armagetronad@${serverName}" = { + description = "Armagetron Advanced Dedicated Server for ${serverName}"; + wants = [ "basic.target" ]; + after = [ "basic.target" "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = + let + stateDirectory = "armagetronad/${serverName}"; + serverRoot = "/var/lib/${stateDirectory}"; + preStart = pkgs.writeShellScript "armagetronad-${serverName}-prestart.sh" '' + owner="${serverId}:${serverId}" + + # Create the config directories. + for dirname in data settings var resource; do + dir="${serverRoot}/$dirname" + mkdir -p "$dir" + chmod u+rwx,g+rx,o-rwx "$dir" + chown "$owner" "$dir" + done + + # Link in the config files if present and non-trivial. + ln -sf ${serverInfoCfg} "${serverRoot}/settings/server_info.cfg" + ln -sf ${customSettingsCfg} "${serverRoot}/settings/settings_custom.cfg" + ln -sf ${everytimeSettingsCfg} "${serverRoot}/settings/everytime.cfg" + + # Create an input file for sending commands to the server. + input="${serverRoot}/input" + truncate -s0 "$input" + chmod u+rw,g+r,o-rwx "$input" + chown "$owner" "$input" + ''; + in + { + Type = "simple"; + StateDirectory = stateDirectory; + ExecStartPre = preStart; + ExecStart = "${serverCfg.package}/bin/armagetronad-dedicated --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource"; + Restart = "on-failure"; + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + User = serverId; + Group = serverId; + }; + }; + }) + enabledServers + ); + + networking.firewall.allowedUDPPorts = + unique (mapAttrsToList (serverName: serverCfg: serverCfg.port) (filterAttrs (serverName: serverCfg: serverCfg.openFirewall) enabledServers)); + + users.users = mkMerge (mapAttrsToList + (serverName: serverCfg: + { + ${nameToId serverName} = { + group = nameToId serverName; + description = "Armagetron Advanced dedicated user for server ${serverName}"; + isSystemUser = true; + }; + }) + enabledServers + ); + + users.groups = mkMerge (mapAttrsToList + (serverName: serverCfg: + { + ${nameToId serverName} = { }; + }) + enabledServers + ); + }; +} -- cgit 1.4.1 From a5c305d170faad291c94f3e47b1b9dc445e4dc2a Mon Sep 17 00:00:00 2001 From: Morgan Jones Date: Sun, 11 Feb 2024 23:09:59 -0800 Subject: nixos/armagetronad: address code review feedback --- nixos/modules/services/games/armagetronad.nix | 119 ++++++++++++++++++-------- nixos/tests/armagetronad.nix | 20 ++--- 2 files changed, 93 insertions(+), 46 deletions(-) (limited to 'nixos/modules/services') diff --git a/nixos/modules/services/games/armagetronad.nix b/nixos/modules/services/games/armagetronad.nix index 64b8cb23057e..f79818e0e53b 100644 --- a/nixos/modules/services/games/armagetronad.nix +++ b/nixos/modules/services/games/armagetronad.nix @@ -23,6 +23,8 @@ let cfg = config.services.armagetronad; enabledServers = lib.filterAttrs (n: v: v.enable) cfg.servers; nameToId = serverName: "armagetronad-${serverName}"; + getStateDirectory = serverName: "armagetronad/${serverName}"; + getServerRoot = serverName: "/var/lib/${getStateDirectory serverName}"; in { options = { @@ -33,38 +35,45 @@ in type = types.attrsOf (types.submodule { options = { enable = mkEnableOption (lib.mdDoc "armagetronad"); + package = lib.mkPackageOptionMD pkgs "armagetronad-dedicated" { example = '' pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated ''; extraDescription = '' - Ensure that you use a derivation whose evaluation contains the path `bin/armagetronad-dedicated`. + Ensure that you use a derivation which contains the path `bin/armagetronad-dedicated`. ''; }; + host = mkOption { type = types.str; default = "0.0.0.0"; description = lib.mdDoc "Host to listen on. Used for SERVER_IP."; }; + port = mkOption { type = types.port; default = 4534; description = lib.mdDoc "Port to listen on. Used for SERVER_PORT."; }; + dns = mkOption { type = types.nullOr types.str; default = null; description = lib.mdDoc "DNS address to use for this server. Optional."; }; + openFirewall = mkOption { type = types.bool; default = true; - description = lib.mdDoc "Set to true to open a UDP port for Armagetron Advanced."; + description = lib.mdDoc "Set to true to open the configured UDP port for Armagetron Advanced."; }; + name = mkOption { type = types.str; description = "The name of this server."; }; + settings = mkOption { type = settingsFormat.type; default = { }; @@ -82,6 +91,7 @@ in } ''; }; + roundSettings = mkOption { type = settingsFormat.type; default = { }; @@ -110,19 +120,17 @@ in }; config = mkIf (enabledServers != { }) { - systemd.services = mkMerge (mapAttrsToList + systemd.tmpfiles.settings = mkMerge (mapAttrsToList (serverName: serverCfg: let serverId = nameToId serverName; + serverRoot = getServerRoot serverName; serverInfo = ( { SERVER_IP = serverCfg.host; SERVER_PORT = serverCfg.port; SERVER_NAME = serverCfg.name; - } // ( - if serverCfg.dns != null then { SERVER_DNS = serverCfg.dns; } - else { } - ) + } // (lib.optionalAttrs (serverCfg.dns != null) { SERVER_DNS = serverCfg.dns; }) ); customSettings = serverCfg.settings; everytimeSettings = serverCfg.roundSettings; @@ -132,43 +140,82 @@ in everytimeSettingsCfg = settingsFormat.generate "everytime.${serverName}.cfg" everytimeSettings; in { - "armagetronad@${serverName}" = { + "10-armagetronad-${serverId}" = { + "${serverRoot}/data" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/settings" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/var" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/resource" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/input" = { + "f+" = { + group = serverId; + user = serverId; + mode = "0640"; + }; + }; + "${serverRoot}/settings/server_info.cfg" = { + "L+" = { + argument = "${serverInfoCfg}"; + }; + }; + "${serverRoot}/settings/settings_custom.cfg" = { + "L+" = { + argument = "${customSettingsCfg}"; + }; + }; + "${serverRoot}/settings/everytime.cfg" = { + "L+" = { + argument = "${everytimeSettingsCfg}"; + }; + }; + }; + } + ) + enabledServers + ); + + systemd.services = mkMerge (mapAttrsToList + (serverName: serverCfg: + let + serverId = nameToId serverName; + in + { + "armagetronad-${serverName}" = { description = "Armagetron Advanced Dedicated Server for ${serverName}"; wants = [ "basic.target" ]; - after = [ "basic.target" "network.target" ]; + after = [ "basic.target" "network.target" "multi-user.target" ]; wantedBy = [ "multi-user.target" ]; serviceConfig = let - stateDirectory = "armagetronad/${serverName}"; - serverRoot = "/var/lib/${stateDirectory}"; - preStart = pkgs.writeShellScript "armagetronad-${serverName}-prestart.sh" '' - owner="${serverId}:${serverId}" - - # Create the config directories. - for dirname in data settings var resource; do - dir="${serverRoot}/$dirname" - mkdir -p "$dir" - chmod u+rwx,g+rx,o-rwx "$dir" - chown "$owner" "$dir" - done - - # Link in the config files if present and non-trivial. - ln -sf ${serverInfoCfg} "${serverRoot}/settings/server_info.cfg" - ln -sf ${customSettingsCfg} "${serverRoot}/settings/settings_custom.cfg" - ln -sf ${everytimeSettingsCfg} "${serverRoot}/settings/everytime.cfg" - - # Create an input file for sending commands to the server. - input="${serverRoot}/input" - truncate -s0 "$input" - chmod u+rw,g+r,o-rwx "$input" - chown "$owner" "$input" - ''; + serverRoot = getServerRoot serverName; in { Type = "simple"; - StateDirectory = stateDirectory; - ExecStartPre = preStart; - ExecStart = "${serverCfg.package}/bin/armagetronad-dedicated --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource"; + StateDirectory = getStateDirectory serverName; + ExecStart = "${lib.getExe serverCfg.package} --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource"; Restart = "on-failure"; CapabilityBoundingSet = ""; LockPersonality = true; diff --git a/nixos/tests/armagetronad.nix b/nixos/tests/armagetronad.nix index be1a9bb4e92c..ff2841dedd21 100644 --- a/nixos/tests/armagetronad.nix +++ b/nixos/tests/armagetronad.nix @@ -138,7 +138,7 @@ in { # Wait for the servers to come up. start_all() for srv in servers: - srv.node.wait_for_unit(f"armagetronad@{srv.name}") + srv.node.wait_for_unit(f"armagetronad-{srv.name}") srv.node.wait_until_succeeds(f"ss --numeric --udp --listening | grep -q {srv.port}") # Make sure console commands work through the named pipe we created. @@ -150,10 +150,10 @@ in { f"echo 'say Testing again!' >> /var/lib/armagetronad/{srv.name}/input" ) srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: Testing!'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: Testing!'" ) srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: Testing again!'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: Testing again!'" ) """ @@ -220,18 +220,18 @@ in { # Wait for clients to connect for client in clients: srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q '{client.name}.*entered the game'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q '{client.name}.*entered the game'" ) # Wait for the match to start srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: {srv.welcome}'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: {srv.welcome}'" ) srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Admin: https://nixos.org'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Admin: https://nixos.org'" ) srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q 'Go (round 1 of 10)'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q 'Go (round 1 of 10)'" ) # Wait a bit @@ -245,7 +245,7 @@ in { # Wait for coredump. srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q '{attacker.name} core dumped {victim.name}'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q '{attacker.name} core dumped {victim.name}'" ) screenshot_idx = take_screenshots(screenshot_idx) @@ -254,7 +254,7 @@ in { client.send('esc') client.send_on('Menu', 'up', 'up', 'ret') srv.node.wait_until_succeeds( - f"journalctl -u armagetronad@{srv.name} -e | grep -q '{client.name}.*left the game'" + f"journalctl -u armagetronad-{srv.name} -e | grep -q '{client.name}.*left the game'" ) # Next server. @@ -264,7 +264,7 @@ in { # Stop the servers for srv in servers: srv.node.succeed( - f"systemctl stop armagetronad@{srv.name}" + f"systemctl stop armagetronad-{srv.name}" ) srv.node.wait_until_fails(f"ss --numeric --udp --listening | grep -q {srv.port}") ''; -- cgit 1.4.1