about summary refs log tree commit diff
diff options
context:
space:
mode:
authoraszlig <aszlig@redmoonstudios.org>2016-03-15 04:34:30 +0100
committeraszlig <aszlig@redmoonstudios.org>2016-03-15 04:55:48 +0100
commitb5ef6a6f32ebed51255918ed100c12e8dfa165c6 (patch)
treea8c0dbbd55a6e45bbe5e6ebdfb34d88e78968878
parent99101f270f20f38667b9cf568d84cd86cba23da4 (diff)
modules: Add new Starbound service and test
Very preliminary and doesn't have all the option descriptions right, nor
does it have convenience features such as setting allowAdminCommands
based on whether any users are defined with admin privileges.

Of course the latter needs to undergo the decision on how to handle RCON
connections, because the latter *might* need that option.

But apart from that single option, there are a lot more options we need
to flesh out.

Also, the test currently is very limited and only spins up a client,
connects to the server and does a movement (just walk to the right).

Needless to say, it's even quite fragile and relies on OCR to properly
detect the custom pixel fonts from Starbound. Which unfortunately fails
most of the time.

Signed-off-by: aszlig <aszlig@redmoonstudios.org>
-rw-r--r--modules/module-list.nix1
-rw-r--r--modules/services/starbound.nix318
-rw-r--r--tests/default.nix3
-rw-r--r--tests/games/starbound.nix117
4 files changed, 439 insertions, 0 deletions
diff --git a/modules/module-list.nix b/modules/module-list.nix
index c4a9e84a..9935021f 100644
--- a/modules/module-list.nix
+++ b/modules/module-list.nix
@@ -7,6 +7,7 @@
   ./profiles/tests.nix
   ./services/multipath-vpn.nix
   ./services/postfix
+  ./services/starbound.nix
   ./system/iso.nix
   ./user/aszlig/profiles/base.nix
   ./user/aszlig/profiles/workstation
diff --git a/modules/services/starbound.nix b/modules/services/starbound.nix
new file mode 100644
index 00000000..2f76eb84
--- /dev/null
+++ b/modules/services/starbound.nix
@@ -0,0 +1,318 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.vuizvui.services.starbound;
+
+  mkListenerOptions = what: defaultPort: {
+    bind = mkOption {
+      type = types.str;
+      default = "::";
+      description = ''
+        Host/IP address to listen for incoming connections to the ${what}.
+      '';
+    };
+
+    port = mkOption {
+      type = types.int;
+      default = defaultPort;
+      description = ''
+        Port to listen for incoming connections to the ${what}.
+      '';
+    };
+  };
+
+  serverConfig = {
+    allowAdminCommands = cfg.adminCommands.allow;
+    allowAdminCommandsFromAnyone = cfg.adminCommands.allowFromAnyone;
+
+    allowAnonymousConnections = cfg.anonymousConnections.allow;
+    anonymousConnectionsAreAdmin = cfg.anonymousConnections.adminPrivileges;
+
+    serverUsers = cfg.users;
+
+    inherit (cfg) checkAssetsDigest clearPlayerFiles clearUniverseFiles;
+    inherit (cfg) maxPlayers safeScripts serverName;
+    inherit (cfg) upnpPortForwarding;
+
+    gameServerBind = cfg.bind;
+    gameServerPort = cfg.port;
+
+    bannedIPs = cfg.bannedIPs;
+    bannedUuids = cfg.bannedUUIDs;
+
+    runRconServer = cfg.rconServer.enable;
+    rconServerBind = cfg.rconServer.bind;
+    rconServerPort = cfg.rconServer.port;
+    rconServerPassword = cfg.rconServer.password;
+    rconServerTimeout = cfg.rconServer.timeout;
+
+    runQueryServer = cfg.queryServer.enable;
+    queryServerBind = cfg.queryServer.bind;
+    queryServerPort = cfg.queryServer.port;
+  } // cfg.extraConfig;
+
+  bootConfig = pkgs.runCommand "sbboot.config" {
+    overrides = pkgs.writeText "sbboot.overrides" (builtins.toJSON {
+      logFileBackups = 0;
+      modSource = "";
+      storageDirectory = cfg.dataDir;
+      defaultConfiguration = serverConfig;
+    });
+  } ''
+    "${pkgs.jq}/bin/jq" -s '.[0] * .[1]' \
+      "${cfg.package}/etc/sbboot.config" "$overrides" \
+      > "$out"
+  '';
+
+  # Traverse a given path with ../ until we get to the root directory (/).
+  gotoRoot = p: concatStringsSep "/" (map (const "..") (splitString "/" p));
+
+in {
+  options.vuizvui.services.starbound = {
+    enable = mkEnableOption "Starbound game server";
+
+    adminCommands = {
+      allow = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to allow admin commands in general.
+        '';
+        # XXX: Make this dependant on whether an account is defined with enabled
+        # admin.
+      };
+
+      allowFromAnyone = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Allow anyone, even anonymous users to use admin commands.
+        '';
+        # XXX: Check whether this is true!
+      };
+    };
+
+    anonymousConnections = {
+      allow = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to allow anonymous connections to the server.
+
+          Set this to <value>false</value> and use <option>serverUsers</option>
+          to only allow specific accounts to connect.
+        '';
+      };
+
+      adminPrivileges = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether all anonymous connections have administrative privileges.
+        '';
+      };
+    };
+
+    users = mkOption {
+      type = types.attrsOf (types.submodule {
+        options.admin = mkOption {
+          type = types.bool;
+          default = false;
+          description = ''
+            Whether this user has admin privileges.
+          '';
+        };
+        options.password = mkOption {
+          type = types.str;
+          example = "supersecure";
+          description = ''
+            The password for the user.
+          '';
+        };
+      });
+    };
+
+    checkAssetsDigest = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Check whether the assets on the client match the ones from the server
+        and deny connection if they don't match.
+      '';
+    };
+
+    clearPlayerFiles = mkOption {
+      # XXX: Figure out the exact semantics of this.
+      type = types.bool;
+      default = false;
+      description = ''
+        Forces players to use new characters or to have no gear or tech.
+      '';
+    };
+
+    clearUniverseFiles = mkOption {
+      # XXX: Figure out the exact semantics of this.
+      type = types.bool;
+      default = false;
+      description = ''
+        Forces player characters to use fresh universe data and navigation maps.
+      '';
+    };
+
+    bannedIPs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        IP addresses disallowed for connection to the server.
+      '';
+    };
+
+    bannedUUIDs = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        User IDs disallowed for connection to the server.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.path;
+      default = "/var/lib/starbound";
+      description = ''
+        The directory where Starbound stores its universe/player files.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.vuizvui.games.steam.starbound;
+      description = ''
+        The starbound package to use for running this game server.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.attrs;
+      default = {};
+      description = ''
+        Extra configuration options to add to the server config.
+      '';
+    };
+
+    rconServer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run an RCON server which allows to run administrative
+          commands on this game server instance.
+
+          See the <link xlink:href="${
+            "https://developer.valvesoftware.com/wiki/Source_RCON_Protocol"
+          }">RCON protocol documentation</link> for more information about this.
+        '';
+      };
+
+      password = mkOption {
+        type = types.str;
+        default = "";
+        description = ''
+          The password needed to authorize with the RCON server.
+        '';
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 1000;
+        # XXX: Find out what this timeout is for and whether it's in seconds.
+        description = ''
+          After how many seconds the RCON server drops the connection.
+        '';
+      };
+    } // mkListenerOptions "RCON server" 21026;
+
+    queryServer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to run a query server that shows information such as currently
+          connected players.
+        '';
+      };
+    } // mkListenerOptions "query server" 21025;
+
+    safeScripts = mkOption {
+      type = types.bool;
+      default = true;
+      # XXX: The description is just a guess and we need to find out what this
+      # really does.
+      description = ''
+        This is to make sure scripts can't call unsafe functions.
+      '';
+    };
+
+    serverName = mkOption {
+      type = types.str;
+      default = "A Starbound Server";
+      example = "My shiny Starbound Server";
+      description = ''
+        A short description or name of the Starbound server to run.
+      '';
+    };
+
+    upnpPortForwarding = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''
+        Whether to use UPnP to forward ports from NAT gateways.
+      '';
+    };
+
+    maxPlayers = mkOption {
+      type = types.int;
+      default = 8;
+      description = ''
+        Maximum amount of players to allow concurrently.
+      '';
+    };
+  } // mkListenerOptions "game server" 21025;
+
+  config = mkIf cfg.enable {
+    users.groups.starbound = {
+      gid = config.ids.gids.starbound;
+    };
+
+    users.users.starbound = {
+      uid = config.ids.uids.starbound;
+      description = "Starbound Game Server User";
+      group = "starbound";
+      home = cfg.dataDir;
+      createHome = true;
+    };
+
+    systemd.services.starbound = {
+      description = "Starbound Server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" "fs.target" ];
+
+      serviceConfig = {
+        User = "starbound";
+        Group = "starbound";
+        PrivateTmp = true;
+
+        KillSignal = "SIGINT";
+
+        ExecStart = toString [
+          "${cfg.package}/bin/starbound-server"
+          "-bootconfig \"${bootConfig}\""
+          # Workaround to disable logging to file
+          "-logfile \"${gotoRoot cfg.dataDir}/dev/null\""
+          "-verbose"
+        ];
+      };
+    };
+  };
+}
diff --git a/tests/default.nix b/tests/default.nix
index eb4f3b81..54b130a3 100644
--- a/tests/default.nix
+++ b/tests/default.nix
@@ -9,6 +9,9 @@ in {
   aszlig = {
     i3 = callTest ./aszlig/i3.nix;
   };
+  games = {
+    starbound = callTest ./games/starbound.nix;
+  };
   richi235 = {
     # Currently broken
     #multipath-vpn = callTest ./richi235/multipath-vpn.nix;
diff --git a/tests/games/starbound.nix b/tests/games/starbound.nix
new file mode 100644
index 00000000..a2c8ba64
--- /dev/null
+++ b/tests/games/starbound.nix
@@ -0,0 +1,117 @@
+{ pkgs, ... }:
+
+let
+  xdo = { name, description, xdoScript }: let
+    xdoFile = pkgs.writeText "${name}.xdo" ''
+      search --onlyvisible --class starbound
+      windowfocus --sync
+      windowactivate --sync
+      ${xdoScript}
+    '';
+    escapeScreenshot = pkgs.lib.replaceStrings ["-"] ["_"];
+  in ''
+    $client->nest("${description}", sub {
+      $client->screenshot("before_${escapeScreenshot name}");
+      $client->succeed("${pkgs.xdotool}/bin/xdotool '${xdoFile}'");
+    });
+  '';
+
+  clickAt = name: x: y: xdo {
+    name = "click-${name}";
+    description = "clicking on ${name} (coords ${toString x} ${toString y})";
+    xdoScript = ''
+      mousemove --window %1 --sync ${toString x} ${toString y}
+      click --repeat 10 1
+    '';
+  };
+
+  typeText = name: text: xdo {
+    name = "type-${name}";
+    description = "typing `${text}' into Starbound";
+    xdoScript = ''
+      type --delay 200 '${text}'
+    '';
+  };
+
+in {
+  name = "starbound";
+
+  enableOCR = true;
+
+  nodes = {
+    server = {
+      vuizvui.services.starbound = {
+        enable = true;
+        # Use a different dataDir than the default to make
+        # sure everything is still working.
+        dataDir = "/var/lib/starbound-test";
+        users.alice.password = "secret";
+      };
+      virtualisation.memorySize = 1024;
+      networking.interfaces.eth1.ipAddress = "192.168.0.1";
+      networking.interfaces.eth1.prefixLength = 24;
+      networking.firewall.enable = false;
+    };
+
+    client = { pkgs, ... }: {
+      imports = [
+        "${import ../../nixpkgs-path.nix}/nixos/tests/common/x11.nix"
+      ];
+      virtualisation.memorySize = 2047;
+      environment.systemPackages = [ pkgs.vuizvui.games.steam.starbound ];
+      networking.interfaces.eth1.ipAddress = "192.168.0.2";
+      networking.interfaces.eth1.prefixLength = 24;
+      networking.firewall.enable = false;
+    };
+  };
+
+  testScript = ''
+    $server->waitForUnit("starbound.service");
+
+    $client->nest("waiting for client to start up", sub {
+      $client->waitForX;
+      $client->succeed("starbound >&2 &");
+      $client->waitForText(qr/options/i);
+    });
+
+    ${clickAt "multiplayer" 100 460}
+    $client->waitForText(qr/select/i);
+    ${clickAt "new-character" 460 170}
+    $client->waitForText(qr/species/i);
+    ${clickAt "create-character" 600 525}
+    $client->waitForText(qr/select/i);
+    ${clickAt "use-character" 460 170}
+    $client->waitForText(qr/ser[vu]er/i);
+
+    ${clickAt "server-address" 460 272}
+    ${typeText "server-address" "192.168.0.1"}
+
+    ${clickAt "server-account" 490 304}
+    ${typeText "server-account" "alice"}
+
+    ${clickAt "server-password" 490 336}
+    ${typeText "server-password" "secret"}
+
+    ${clickAt "join-server" 495 370}
+
+    $client->waitForText(qr/q[uv]est/i);
+    ${xdo {
+      name = "close-quest-dialog";
+      description = "closing the quest dialog window";
+      xdoScript = ''
+        key Escape
+      '';
+    }}
+    ${xdo {
+      name = "move-right";
+      description = "moving to the right of the ship";
+      xdoScript = ''
+        keydown d
+        sleep 10
+        keyup d
+      '';
+    }}
+
+    $client->screenshot("client");
+  '';
+}