diff --git a/README.rst b/README.rst index ee86364..35cb4f7 100644 --- a/README.rst +++ b/README.rst @@ -15,9 +15,11 @@ and on top of that: - Managing game servers using `Linux Gameservers`_: - - Downloads newest version during first boot to ensure best possible game - support. - - Wrapper for `LinuxGSM`_ with support for up to 100 games. + - Installs a verified game catalog and LinuxGSM stable release. + - Wrapper for `LinuxGSM`_ with support for more than 100 game servers. + - ``turnkey-gameserver-update`` checks or applies verified catalog and + LinuxGSM updates. Individual game servers retain their LinuxGSM update + command. - Fully automatic or interactive game server selection: @@ -31,7 +33,7 @@ and on top of that: Credentials *(passwords set at first boot)* ------------------------------------------- -- Webmin, SSH, Shellinabox: username **root** +- Webmin, SSH: username **root** - Game server: username **gameuser** .. _TurnKey GNU/Linux: https://www.turnkeylinux.org/ diff --git a/changelog b/changelog index 0bf46ba..1a183a3 100644 --- a/changelog +++ b/changelog @@ -1,3 +1,17 @@ +turnkey-gameserver-19.0 (1) turnkey; urgency=low + + * Port the appliance to Debian 13 (Trixie). + + * Pin the official game catalog wrapper and LinuxGSM v26.2.0 bootstrap with + verified source records. + + * Add a supervised updater for the wrapper master and LinuxGSM stable tags. + + * Preserve interactive and unattended game selection, Configuration Console + management, non-root game processes, and systemd service lifecycle. + + -- TurnKey Linux release engineering Tue, 25 Aug 2026 22:30:00 +0000 + turnkey-gameserver-18.1 (1) turnkey; urgency=low * v18.1 rebuild - includes latest Debian & TurnKey packages. diff --git a/conf.d/downloads b/conf.d/downloads new file mode 100755 index 0000000..2d1ad5c --- /dev/null +++ b/conf.d/downloads @@ -0,0 +1,27 @@ +#!/bin/bash -e + +set -o pipefail +set -x + +SRC=/usr/local/src +WRAPPER_COMMIT=d2017be9f56db0da2a1c45651e656f47bc7ce42a +WRAPPER_ARCHIVE=linux-gameservers-$WRAPPER_COMMIT.zip +WRAPPER_SHA256=302b859632e34715f88b6f3455b7925efe822fb0b6b4a04514af12c1143cb983 +LINUXGSM_VERSION=v26.2.0 +LINUXGSM_BOOTSTRAP=linuxgsm-$LINUXGSM_VERSION.sh +LINUXGSM_SHA256=0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd + +proxy_args=() +if [ -n "${FAB_HTTP_PROXY:-}" ]; then + proxy_args=(--proxy "$FAB_HTTP_PROXY") +fi + +curl -LfsS "${proxy_args[@]}" \ + "https://github.com/jesinmat/linux-gameservers/archive/$WRAPPER_COMMIT.zip" \ + -o "$SRC/$WRAPPER_ARCHIVE" +curl -LfsS "${proxy_args[@]}" \ + "https://raw.githubusercontent.com/GameServerManagers/LinuxGSM/$LINUXGSM_VERSION/linuxgsm.sh" \ + -o "$SRC/$LINUXGSM_BOOTSTRAP" + +printf '%s %s\n' "$WRAPPER_SHA256" "$SRC/$WRAPPER_ARCHIVE" | sha256sum -c - +printf '%s %s\n' "$LINUXGSM_SHA256" "$SRC/$LINUXGSM_BOOTSTRAP" | sha256sum -c - diff --git a/conf.d/main b/conf.d/main index 5c5b78f..09ea4e1 100755 --- a/conf.d/main +++ b/conf.d/main @@ -1,37 +1,80 @@ -#!/bin/sh -ex +#!/bin/bash -e -GAMEUSER="gameuser" -GAME_REPO_DIR="/root/gameservers" -GAME_REPO_URL="https://github.com/jesinmat/linux-gameservers.git" +set -o pipefail +set -x -# Add i386 arch (common for lots of games) and install lib32gcc1 -dpkg --add-architecture i386 -dir=/etc/apt/sources.list.d -for list in $dir/sources.list $dir/security.sources.list; do - sed -i "\|turnkeylinux.org| s|\[|\[arch=amd64, |" $list +GAMEUSER=gameuser +GAME_REPO_DIR=/root/gameservers +SRC=/usr/local/src +WRAPPER_COMMIT=d2017be9f56db0da2a1c45651e656f47bc7ce42a +WRAPPER_ARCHIVE=linux-gameservers-$WRAPPER_COMMIT.zip +WRAPPER_SHA256=302b859632e34715f88b6f3455b7925efe822fb0b6b4a04514af12c1143cb983 +LINUXGSM_VERSION=v26.2.0 +LINUXGSM_TAG_COMMIT=bded3376bc44d20b89fcc6c32f23a66347fffec6 +LINUXGSM_BOOTSTRAP=linuxgsm-$LINUXGSM_VERSION.sh +LINUXGSM_SHA256=0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd + +# Older game servers still need i386 libraries. Restrict TurnKey package +# sources to amd64 before enabling Debian multiarch. +for source_file in /etc/apt/sources.list.d/*; do + [ -f "$source_file" ] || continue + grep -q 'turnkeylinux.org' "$source_file" || continue + if grep -q '^Types:' "$source_file"; then + if grep -q '^Architectures:' "$source_file"; then + sed -i 's/^Architectures:.*/Architectures: amd64/' "$source_file" + else + sed -i '/^Types:/a Architectures: amd64' "$source_file" + fi + else + sed -i -E '/turnkeylinux\.org/ { /\[[^]]*arch=/! s|^deb[[:space:]]+|deb [arch=amd64] |; }' "$source_file" + fi done +dpkg --add-architecture i386 apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y lib32gcc-s1 -# Create GAMEUSER as (limited) sudo user (initially commented out - running -# gameserver-init will enable it; then disable it afterwards). useradd -m -s /bin/bash "$GAMEUSER" -match="# Cmnd alias specification" -line1="Cmnd_Alias APT_GET = /usr/bin/apt-get" -line2="Cmnd_Alias TRUE = /usr/bin/true" -line3="#$GAMEUSER ALL=(ALL) NOPASSWD: APT_GET, TRUE" -for line in "$line1" "$line2"; do - sed -i "\|$match| a $line" /etc/sudoers -done -echo >> /etc/sudoers -echo "$line3" >> /etc/sudoers +cat > /etc/sudoers.d/gameserver-install < /etc/ssh/sshd_config.d/turnkey-gameserver.conf + +unzip -q "$SRC/$WRAPPER_ARCHIVE" -d /root +mv "/root/linux-gameservers-$WRAPPER_COMMIT" "$GAME_REPO_DIR" +rm -f "$SRC/$WRAPPER_ARCHIVE" + +install -d -m 0755 /usr/local/share/turnkey-gameserver +install -m 0755 "$SRC/$LINUXGSM_BOOTSTRAP" \ + /usr/local/share/turnkey-gameserver/linuxgsm.sh +rm -f "$SRC/$LINUXGSM_BOOTSTRAP" + +# Keep the wrapper on Debian package names and the verified LinuxGSM release. +sed -i 's/bsdmainutils/bsdextrautils/' "$GAME_REPO_DIR/auto_install.sh" +sed -i '\|run_as_user "wget -O .*https://linuxgsm.sh"|c\ install -o "$GAMEUSER" -g "$GAMEUSER" -m 0755 /usr/local/share/turnkey-gameserver/linuxgsm.sh "$LGSM_PATH"' \ + "$GAME_REPO_DIR/auto_install.sh" +grep -q 'install -o "$GAMEUSER".*turnkey-gameserver/linuxgsm.sh' \ + "$GAME_REPO_DIR/auto_install.sh" -# disable SSH login for GAMEUSER -echo "DenyUsers $GAMEUSER" >> /etc/ssh/sshd_config +# Mumble was removed from LinuxGSM and can no longer be installed. +rm -rf "$GAME_REPO_DIR/games/mumble" -git clone --depth=1 "$GAME_REPO_URL" "$GAME_REPO_DIR" +cat > /usr/local/share/turnkey-gameserver/source <`_ list is missing, you need to update the gameserver list. There are two methods to update the gameserver list, through the Configuration Console or updating the repo from the CLI. +The Configuration Console and CLI use the same verified update channel for the +game catalog and LinuxGSM bootstrap. @@ -81,26 +81,16 @@ Method 1: Method 2: ^^^^^^^^^^^ -#. SSH into the appliance and stop the service:: - - ~# systemctl stop gameserver - -#. Change to gameservers directory:: - - ~# cd /root/gameservers/ +#. SSH into the appliance and check the official channels:: -#. Pull down the latest from the git repo:: + ~# turnkey-gameserver-update --check - ~/gameservers# git pull origin master +#. Apply the verified catalog and LinuxGSM release:: -#. Start the gameserver service:: - - ~/gameservers# systemctl start gameserver - -Done! + ~# turnkey-gameserver-update --apply Update game server version ----------------------------- +-------------------------- If there is an update to the game after deploying your server, you may need to update the server to allow clients with the newer version to connect. Like with updating the game server list, this can either be done through the GUI or manually. Here are instructions for the two methods: Method 1: @@ -127,21 +117,14 @@ Method 2: ~# systemctl stop gameserver -#. Change to gameservers directory:: - - ~# cd /root/gameservers/ - -#. Update your game server (replace ```` with the Code from step 1):: - - ~# ./auto_install.sh -g -u gameuser -p /home/gameuser/gameserver - - e.g. for a Satisfactory server (code ``sf``) +#. Check and update your game server (replace ```` with its code):: - ``~# ./auto_install.sh -g sf -u gameuser -p /home/gameuser/gameserver`` + ~# sudo -H -u gameuser /home/gameuser/gameserver/server check-update + ~# sudo -H -u gameuser /home/gameuser/gameserver/server update #. Start the game server back up:: - ~# ./sfserver start + ~# systemctl start gameserver The server should now be updated to the latest version of the game. diff --git a/docs/v19.0-testing.md b/docs/v19.0-testing.md new file mode 100644 index 0000000..b91a4f6 --- /dev/null +++ b/docs/v19.0-testing.md @@ -0,0 +1,81 @@ +# GameServer v19 migration evidence + +## Source and state boundaries + +Debian supplies Nginx, the 32-bit compatibility library, and all management +dependencies. The game catalog wrapper and LinuxGSM are application sources +that Debian does not package for this appliance contract. + +The build uses these exact official upstream sources: + +- `jesinmat/linux-gameservers` master commit + `d2017be9f56db0da2a1c45651e656f47bc7ce42a` +- Wrapper archive SHA256 + `302b859632e34715f88b6f3455b7925efe822fb0b6b4a04514af12c1143cb983` +- LinuxGSM `v26.2.0` tag commit + `bded3376bc44d20b89fcc6c32f23a66347fffec6` +- LinuxGSM bootstrap SHA256 + `0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd` + +LinuxGSM v26.2.0 includes Debian 13 dependency metadata and explicit handling +for game servers whose vendor binaries do not support newer distributions. +LinuxGSM accepts private vulnerability reports through its GitHub security +advisory form. The wrapper has no separate published security process, so its +official repository issue and maintainer channels are the available route. + +The default source record binds both upstreams. Selecting a custom wrapper +repository at first boot changes that record to the custom channel, and the +official updater then stops with an explicit custom-source boundary instead of +overwriting it. + +## Acceptance crosswalk + +| Appliance contract | `tests/v19.sh` evidence | +| --- | --- | +| Management UI | Loads the Nginx GameServer landing page, its Webmin link, the selector command, and the Configuration Console update action. | +| Supported server list | Runs the installed wrapper list, requires at least 100 entries, checks a known lightweight definition, and rejects the removed Mumble entry. | +| Catalog and LinuxGSM update | Resolves the official wrapper master and latest stable LinuxGSM tag, verifies exact candidate artifacts, and exercises the apply dry run. | +| Service lifecycle | Starts and stops the systemd unit against a disposable non-root fixture and observes both actions. | +| Non-root server boundary | Runs the fixture through the same `runuser` start and stop scripts used by installed servers. | +| Automatic and interactive selection | Runs `gameserver-init` with the catalog's lightweight TeamSpeak target, requires LinuxGSM installation completion, and verifies the server through LinuxGSM's monitor command. | +| TurnKey platform surfaces | Core supplies SSH, Webmin, TLS, backups, and first-boot integration. The appliance test covers GameServer-specific behavior. | + +The runtime test emits exactly the seven Wave 2 result keys: package source, +installed version, runtime checks, updater command, updater result, updater +channel, and integrity evidence. + +## Candidate status + +Candidate `a57d42a3f90562d241ec9f85aa00d2856eae3e0c` passed the exact container +acceptance command: + +```sh +/sandboxed-git/turnkey/tools/test-v19-appliance gameserver \ + --source /home/agent/.local/worktrees/turnkey-apps/gameserver/wish-gameserver-v19-trixie +``` + +Run `20260826t130005z-2603-3322` built and imported the Trixie rootfs, reached +multi-user state with inithooks complete, and passed every application check in +the crosswalk. The retained report is +`/home/agent/.local/state/turnkey-v19-harness/runs/gameserver/20260826t130005z-2603-3322/report.txt` +with SHA256 +`70bc728761c5d0515e5ff95467ba06feda0bfc09e14d672b8a5b3c871c8579a9`. + +Product-fix accounting is 2 of 3. The first loop made installer failures +propagate through the logging pipeline. The second removed the Mumble choice +after exact acceptance proved that LinuxGSM v26.2.0 no longer supports its +server identifier. Test-only HTTP acceptance corrections did not consume a +product loop. + +## Deferred issues + +- MEDIUM: The upstream wrapper has 120 definitions, but some lag the current + LinuxGSM catalog. The known broken Mumble choice is excluded. The updater + checks both channels independently, but other additions and removals still + need matching wrapper catalog changes before the selector reflects them. +- MEDIUM: Acceptance installs the lightweight TeamSpeak target through the real + catalog and LinuxGSM path. Other game downloads differ substantially in size, + licensing, authentication, and network behavior, so this does not prove every + vendor-specific installer. +- LOW: The test checks the real landing page and Configuration Console action + boundary without automating the terminal dialog renderer. diff --git a/overlay/usr/lib/confconsole/plugins.d/Game_Server/update_list.py b/overlay/usr/lib/confconsole/plugins.d/Game_Server/update_list.py index 649798f..0f1ebf6 100755 --- a/overlay/usr/lib/confconsole/plugins.d/Game_Server/update_list.py +++ b/overlay/usr/lib/confconsole/plugins.d/Game_Server/update_list.py @@ -1,51 +1,20 @@ -'''Update LinuxGameservers management software''' -import os -import subprocess - -GAME_REPO_DIR="/root/gameservers" -GAME_REPO_URL="https://github.com/jesinmat/linux-gameservers.git" - -def is_update_available(): - ret = subprocess.run( - ['git', 'ls-remote', 'origin', 'refs/heads/master'], - capture_output=True, text=True) - if ret.returncode != 0: - return ('error', ret.stderr) - - # get first line of output, split by spaces (should be a commit id of - # remote head) - remote_head = ret.stdout.splitlines()[0].strip().split()[0] +"""Update the verified game-server catalog and LinuxGSM bootstrap.""" - ret = subprocess.run(['git', 'rev-parse', 'HEAD'], - capture_output=True, text=True) - if ret.returncode != 0: - return ('error', ret.stderr) +import subprocess - local_head = ret.stdout.splitlines()[0].strip() - return ('ok', local_head != remote_head) def run(): - curdir = os.getcwd() - os.chdir(GAME_REPO_DIR) - - ok, data = is_update_available() - if ok == 'error': - console.msgbox('Error', - 'An error occured checking for updates:\n', - data) - os.chdir(curdir) + console.infobox('Checking the official game-server update channels...') + result = subprocess.run( + ['/usr/local/sbin/turnkey-gameserver-update', '--apply'], + capture_output=True, + text=True, + ) + if result.returncode != 0: + console.msgbox( + 'Update Error', + 'The game-server catalog update failed:\n' + result.stderr, + ) return - if data: - console.infobox('Updates found, performing updates...') - ret = subprocess.run(['git', 'pull'], capture_output=True, text=True) - - if ret.returncode != 0: - console.msgbox('Error', - 'An error occured during update:\n', - ret.stderr) - else: - console.msgbox('Update', 'Update success') - else: - console.msgbox('Updates', 'No updates found') - os.chdir(curdir) + console.msgbox('Update', result.stdout) diff --git a/overlay/usr/lib/inithooks/bin/gameserver.py b/overlay/usr/lib/inithooks/bin/gameserver.py index b54d0ee..0d037d4 100755 --- a/overlay/usr/lib/inithooks/bin/gameserver.py +++ b/overlay/usr/lib/inithooks/bin/gameserver.py @@ -9,9 +9,16 @@ import os import sys import getopt +import shutil import subprocess +import tempfile from libinithooks.dialog_wrapper import Dialog +DEFAULT_GAMESERVER_REPO = 'https://github.com/jesinmat/linux-gameservers.git' +DEFAULT_GAMESERVER_BRANCH = 'master' +GAME_REPO_DIR = '/root/gameservers' +SOURCE_RECORD = '/usr/local/share/turnkey-gameserver/source' + def usage(s=None): if s: print("Error:", s, file=sys.stderr) @@ -26,9 +33,6 @@ def main(): except getopt.GetoptError as e: usage(e) - default_gameserver_repo = 'https://github.com/jesinmat/linux-gameservers.git' - default_gameserver_branch = 'master' - gameserver_repo = "" gameserver_branch = "" for opt, val in opts: @@ -50,48 +54,52 @@ def main(): ok, gameserver_repo = dialog.inputbox( 'TKL Gameserver', 'Choose gameserver repo url', - default_gameserver_repo) + DEFAULT_GAMESERVER_REPO) if not ok: - gameserver_repo = default_gameserver_repo + gameserver_repo = DEFAULT_GAMESERVER_REPO if not gameserver_branch: ok, gameserver_branch = dialog.inputbox( 'TKL Gameserver', 'Choose gameserver branch', - default_gameserver_branch) + DEFAULT_GAMESERVER_BRANCH) if not ok: - gameserver_branch = default_gameserver_branch + gameserver_branch = DEFAULT_GAMESERVER_BRANCH else: - gameserver_repo = default_gameserver_repo - gameserver_branch = default_gameserver_branch + gameserver_repo = DEFAULT_GAMESERVER_REPO + gameserver_branch = DEFAULT_GAMESERVER_BRANCH - needs_pull = False - old_dir = os.getcwd() - if gameserver_repo != default_gameserver_repo: - os.chdir('/root/gameservers') - subprocess.run([ - 'git', 'remote', 'set-url', 'origin', - gameserver_repo - ]) - needs_pull = True - if gameserver_branch != default_gameserver_branch: - os.chdir('/root/gameservers') - subprocess.run([ - 'git', 'fetch' - ]) - subprocess.run([ - 'git', 'checkout', '--track', - f'origin/{gameserver_branch}', - ]) - needs_pull = True + if (gameserver_repo, gameserver_branch) == ( + DEFAULT_GAMESERVER_REPO, DEFAULT_GAMESERVER_BRANCH): + return - if needs_pull: - os.chdir('/root/gameservers') + temp_dir = tempfile.mkdtemp(prefix='.gameservers-', dir='/root') + candidate_dir = os.path.join(temp_dir, 'repo') + try: subprocess.run([ - 'git', 'pull' - ]) - - os.chdir(old_dir) + 'git', 'clone', '--depth=1', '--branch', gameserver_branch, + gameserver_repo, candidate_dir, + ], check=True) + if not os.path.isfile(os.path.join(candidate_dir, 'auto_install.sh')): + raise RuntimeError('custom repository has no auto_install.sh') + if not os.path.isdir(os.path.join(candidate_dir, 'games')): + raise RuntimeError('custom repository has no games directory') + commit = subprocess.run([ + 'git', '-C', candidate_dir, 'rev-parse', 'HEAD', + ], check=True, capture_output=True, text=True).stdout.strip() + + shutil.rmtree(GAME_REPO_DIR) + os.replace(candidate_dir, GAME_REPO_DIR) + with open(SOURCE_RECORD, 'w', encoding='utf-8') as source_record: + source_record.write( + 'wrapper_channel=custom git repository\n' + f'wrapper_repository={gameserver_repo}\n' + f'wrapper_ref={gameserver_branch}\n' + f'wrapper_commit={commit}\n' + 'linuxgsm_channel=custom wrapper policy\n' + ) + finally: + shutil.rmtree(temp_dir, ignore_errors=True) if __name__ == '__main__': main() diff --git a/overlay/usr/lib/inithooks/firstboot.d/40gameserver b/overlay/usr/lib/inithooks/firstboot.d/40gameserver index 7476fef..6c0e111 100755 --- a/overlay/usr/lib/inithooks/firstboot.d/40gameserver +++ b/overlay/usr/lib/inithooks/firstboot.d/40gameserver @@ -3,10 +3,10 @@ . /etc/default/inithooks -[ -e $INITHOOKS_CONF ] && . $INITHOOKS_CONF +[ -e "$INITHOOKS_CONF" ] && . "$INITHOOKS_CONF" GAMEUSER=${GAMEUSER:-gameuser} -$INITHOOKS_PATH/bin/setpass.py $GAMEUSER --pass="$APP_PASS" +$INITHOOKS_PATH/bin/setpass.py "$GAMEUSER" --pass="$APP_PASS" unset skip_init # if sec updates set (or turnkey-init-fence is active), then we must be running @@ -16,6 +16,8 @@ if [[ -n "$SEC_UPDATES" ]] || systemctl -q is-active turnkey-init-fence ; then APP_GAMESERVER_BRANCH="${APP_GAMESERVER_BRANCH:-master}" [[ -n "$GAME" ]] || skip_init=true fi -$INITHOOKS_PATH/bin/gameserver.py --gameserver-repo="$APP_GAMESERVER_REPO" --gameserver-branch="$APP_GAMESERVER_BRANCH" +$INITHOOKS_PATH/bin/gameserver.py \ + --gameserver-repo="$APP_GAMESERVER_REPO" \ + --gameserver-branch="$APP_GAMESERVER_BRANCH" [[ -n "$skip_init" ]] || /usr/local/bin/gameserver-init diff --git a/overlay/usr/local/bin/gameserver-init b/overlay/usr/local/bin/gameserver-init index 86647b2..fff83aa 100755 --- a/overlay/usr/local/bin/gameserver-init +++ b/overlay/usr/local/bin/gameserver-init @@ -1,13 +1,16 @@ #!/bin/bash -e +set -o pipefail + GAMEUSER="gameuser" LOGFILE="/var/log/gameserver/install.log" GAME_REPO_DIR="/root/gameservers" +SUDOERS_FILE="/etc/sudoers.d/gameserver-install" clean_exit() { exit_code=$1 shift - sed -i "\|^$GAMEUSER| s|^|#|" /etc/sudoers + sed -i "\|^$GAMEUSER| s|^|#|" "$SUDOERS_FILE" if [[ $exit_code -ne 0 ]]; then rm -f /etc/gameserver/gameserver echo @@ -48,7 +51,7 @@ install_game() { if [ ! -f "/etc/gameserver/installation.done" ]; then conf=/etc/gameserver/gameserver [[ -f "$conf" ]] || clean_exit 1 "Conf file ($conf) not found." - . $conf + . "$conf" exit_code=0 cd "$GAME_REPO_DIR" chmod +x "auto_install.sh" @@ -70,7 +73,8 @@ install_game() { install_exit_code=0 trap 'clean_exit $?' INT TERM ERR -sed -i "\|$GAMEUSER| s|^\s*#||" /etc/sudoers +sed -i "\|$GAMEUSER| s|^[[:space:]]*#||" "$SUDOERS_FILE" +visudo -cf "$SUDOERS_FILE" select_game install_game 2>&1 | tee -a "$LOGFILE" || install_exit_code=$? clean_exit $install_exit_code diff --git a/overlay/usr/local/bin/gameserver-start b/overlay/usr/local/bin/gameserver-start index 4fbd1a8..c35e6da 100755 --- a/overlay/usr/local/bin/gameserver-start +++ b/overlay/usr/local/bin/gameserver-start @@ -5,5 +5,6 @@ fatal() { echo "FATAL: $@"; exit 1; } conf=/etc/gameserver/gameserver [[ -f "$conf" ]] || fatal "Conf file ($conf) not found." -. $conf +. "$conf" +[[ "$GAME" =~ ^[a-z0-9_-]+$ ]] || fatal "Invalid game code in $conf." runuser -l "gameuser" -c "~/gameserver/${GAME}server start" diff --git a/overlay/usr/local/bin/gameserver-stop b/overlay/usr/local/bin/gameserver-stop index 0194248..cc8b4fb 100755 --- a/overlay/usr/local/bin/gameserver-stop +++ b/overlay/usr/local/bin/gameserver-stop @@ -5,5 +5,6 @@ fatal() { echo "FATAL: $@"; exit 1; } conf=/etc/gameserver/gameserver [[ -f "$conf" ]] || fatal "Conf file ($conf) not found." -. $conf +. "$conf" +[[ "$GAME" =~ ^[a-z0-9_-]+$ ]] || fatal "Invalid game code in $conf." runuser -l "gameuser" -c "~/gameserver/${GAME}server stop" diff --git a/overlay/usr/local/sbin/turnkey-gameserver-update b/overlay/usr/local/sbin/turnkey-gameserver-update new file mode 100755 index 0000000..efbf1ee --- /dev/null +++ b/overlay/usr/local/sbin/turnkey-gameserver-update @@ -0,0 +1,157 @@ +#!/bin/bash -e + +set -o pipefail + +WRAPPER_REPO=https://github.com/jesinmat/linux-gameservers.git +WRAPPER_DIR=/root/gameservers +LINUXGSM_REPO=https://github.com/GameServerManagers/LinuxGSM.git +SHARE_DIR=/usr/local/share/turnkey-gameserver +SOURCE_RECORD=$SHARE_DIR/source +TEMP_DIR= + +usage() { + echo "usage: turnkey-gameserver-update --check | --apply [--dry-run]" >&2 + exit 2 +} + +fail() { + echo "gameserver update: $*" >&2 + exit 1 +} + +record_value() { + key=$1 + value=$(sed -n "s/^${key}=//p" "$SOURCE_RECORD") + [ -n "$value" ] || fail "missing source record key: $key" + printf '%s\n' "$value" +} + +cleanup() { + if [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ]; then + rm -rf "$TEMP_DIR" + fi +} +trap cleanup EXIT HUP INT TERM + +prepare_wrapper() { + wrapper_root=$1 + [ -f "$wrapper_root/auto_install.sh" ] || fail "wrapper archive has no auto_install.sh" + [ -d "$wrapper_root/games" ] || fail "wrapper archive has no games directory" + sed -i 's/bsdmainutils/bsdextrautils/' "$wrapper_root/auto_install.sh" + sed -i '\|run_as_user "wget -O .*https://linuxgsm.sh"|c\ install -o "$GAMEUSER" -g "$GAMEUSER" -m 0755 /usr/local/share/turnkey-gameserver/linuxgsm.sh "$LGSM_PATH"' \ + "$wrapper_root/auto_install.sh" + grep -q 'install -o "$GAMEUSER".*turnkey-gameserver/linuxgsm.sh' \ + "$wrapper_root/auto_install.sh" || fail "wrapper bootstrap patch did not apply" + rm -rf "$wrapper_root/games/mumble" +} + +fetch_candidate() { + wrapper_candidate_commit=$(git ls-remote "$WRAPPER_REPO" refs/heads/master | awk 'NR == 1 {print $1}') + printf '%s\n' "$wrapper_candidate_commit" | grep -Eq '^[0-9a-f]{40}$' || + fail "unable to resolve the official wrapper master commit" + + linuxgsm_refs=$(git ls-remote --refs --tags "$LINUXGSM_REPO" 'refs/tags/v*') + linuxgsm_candidate_version=$(printf '%s\n' "$linuxgsm_refs" | + sed -n 's|^[^[:space:]]*[[:space:]]*refs/tags/||p' | + grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1) + [ -n "$linuxgsm_candidate_version" ] || fail "no official LinuxGSM stable tag found" + linuxgsm_candidate_commit=$(printf '%s\n' "$linuxgsm_refs" | + awk -v ref="refs/tags/$linuxgsm_candidate_version" '$2 == ref {print $1}') + printf '%s\n' "$linuxgsm_candidate_commit" | grep -Eq '^[0-9a-f]{40}$' || + fail "unable to resolve the official LinuxGSM tag commit" + + TEMP_DIR=$(mktemp -d /var/tmp/turnkey-gameserver-update.XXXXXX) + wrapper_archive=$TEMP_DIR/wrapper.zip + linuxgsm_bootstrap=$TEMP_DIR/linuxgsm.sh + curl -LfsS \ + "https://github.com/jesinmat/linux-gameservers/archive/$wrapper_candidate_commit.zip" \ + -o "$wrapper_archive" + curl -LfsS \ + "https://raw.githubusercontent.com/GameServerManagers/LinuxGSM/$linuxgsm_candidate_version/linuxgsm.sh" \ + -o "$linuxgsm_bootstrap" + wrapper_candidate_sha256=$(sha256sum "$wrapper_archive" | awk '{print $1}') + linuxgsm_candidate_sha256=$(sha256sum "$linuxgsm_bootstrap" | awk '{print $1}') + + wrapper_candidate_root=$TEMP_DIR/linux-gameservers-$wrapper_candidate_commit + unzip -q "$wrapper_archive" -d "$TEMP_DIR" + prepare_wrapper "$wrapper_candidate_root" + grep -qx "version=\"$linuxgsm_candidate_version\"" "$linuxgsm_bootstrap" || + fail "LinuxGSM bootstrap version does not match its tag" +} + +mode= +dry_run=false +while [ "$#" -gt 0 ]; do + case "$1" in + --check) [ -z "$mode" ] || usage; mode=check ;; + --apply) [ -z "$mode" ] || usage; mode=apply ;; + --dry-run) dry_run=true ;; + *) usage ;; + esac + shift +done +[ -n "$mode" ] || usage +[ "$mode" = apply ] || [ "$dry_run" = false ] || usage +[ -f "$SOURCE_RECORD" ] || fail "missing source record" + +wrapper_channel=$(record_value wrapper_channel) +[ "$wrapper_channel" = "official jesinmat/linux-gameservers master" ] || + fail "custom wrapper configured; update it through its selected repository" +wrapper_installed_commit=$(record_value wrapper_commit) +wrapper_installed_sha256=$(record_value wrapper_archive_sha256) +linuxgsm_installed_version=$(record_value linuxgsm_version) +linuxgsm_installed_commit=$(record_value linuxgsm_tag_commit) +linuxgsm_installed_sha256=$(record_value linuxgsm_bootstrap_sha256) + +fetch_candidate + +if [ "$wrapper_candidate_commit" = "$wrapper_installed_commit" ]; then + [ "$wrapper_candidate_sha256" = "$wrapper_installed_sha256" ] || + fail "official wrapper archive changed for the installed commit" +fi +if [ "$linuxgsm_candidate_version" = "$linuxgsm_installed_version" ]; then + [ "$linuxgsm_candidate_commit" = "$linuxgsm_installed_commit" ] || + fail "official LinuxGSM tag changed for the installed version" + [ "$linuxgsm_candidate_sha256" = "$linuxgsm_installed_sha256" ] || + fail "official LinuxGSM bootstrap changed for the installed version" +fi + +status=up-to-date +if [ "$wrapper_candidate_commit" != "$wrapper_installed_commit" ] || + dpkg --compare-versions "${linuxgsm_installed_version#v}" lt "${linuxgsm_candidate_version#v}"; then + status=update-available +fi + +printf 'wrapper_installed_commit=%s\n' "$wrapper_installed_commit" +printf 'wrapper_candidate_commit=%s\n' "$wrapper_candidate_commit" +printf 'wrapper_candidate_archive_sha256=%s\n' "$wrapper_candidate_sha256" +printf 'linuxgsm_installed=%s\n' "$linuxgsm_installed_version" +printf 'linuxgsm_candidate=%s\n' "$linuxgsm_candidate_version" +printf 'linuxgsm_candidate_commit=%s\n' "$linuxgsm_candidate_commit" +printf 'linuxgsm_candidate_bootstrap_sha256=%s\n' "$linuxgsm_candidate_sha256" +printf 'status=%s\n' "$status" + +[ "$mode" = apply ] || exit 0 +if [ "$dry_run" = true ]; then + printf 'apply=dry-run verified exact candidates\n' + exit 0 +fi +[ "$(id -u)" -eq 0 ] || fail "apply requires root" +if [ "$status" = up-to-date ]; then + printf 'apply=no-op\n' + exit 0 +fi + +rsync -a --delete "$wrapper_candidate_root/" "$WRAPPER_DIR/" +install -m 0755 "$linuxgsm_bootstrap" "$SHARE_DIR/linuxgsm.sh" +cat > "$TEMP_DIR/source" <&2 + exit 1 +} + +record_value() { + key=$1 + value=$(sed -n "s/^${key}=//p" "$SOURCE_RECORD") + [ -n "$value" ] || fail "missing source record key: $key" + printf '%s\n' "$value" +} + +[ -f "$SOURCE_RECORD" ] || fail "missing source record" +[ "$(record_value wrapper_commit)" = d2017be9f56db0da2a1c45651e656f47bc7ce42a ] || + fail "unexpected wrapper commit" +[ "$(record_value wrapper_archive_sha256)" = 302b859632e34715f88b6f3455b7925efe822fb0b6b4a04514af12c1143cb983 ] || + fail "unexpected wrapper archive digest" +[ "$(record_value linuxgsm_version)" = v26.2.0 ] || fail "unexpected LinuxGSM version" +[ "$(record_value linuxgsm_tag_commit)" = bded3376bc44d20b89fcc6c32f23a66347fffec6 ] || + fail "unexpected LinuxGSM tag commit" +[ "$(record_value linuxgsm_bootstrap_sha256)" = 0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd ] || + fail "unexpected LinuxGSM bootstrap digest" +printf '%s %s\n' \ + 0a17b88b4d6a272ce8494d55fc0c2748f3187057c15b801d71991428aa8f79bd \ + "$LINUXGSM_BOOTSTRAP" | sha256sum -c - >/dev/null +grep -qx 'version="v26.2.0"' "$LINUXGSM_BOOTSTRAP" || fail "LinuxGSM runtime version differs" + +server_list=$(cd "$WRAPPER_DIR" && ./auto_install.sh --list) +server_count=$(printf '%s\n' "$server_list" | grep -Ec '^\| [a-z0-9_-]+[[:space:]]+\|') +[ "$server_count" -ge 100 ] || fail "supported server catalog is unexpectedly small" +printf '%s\n' "$server_list" | grep -Eq '^\| ts3[[:space:]]+\| Teamspeak 3' || + fail "lightweight TeamSpeak server definition is missing" +if printf '%s\n' "$server_list" | grep -Eq '^\| mumble[[:space:]]+\|'; then + fail "catalog offers the LinuxGSM-unsupported Mumble server" +fi + +nginx -t >/dev/null 2>&1 +http_redirect=$(curl -sS -o /dev/null -w '%{http_code} %{redirect_url}' http://127.0.0.1/) +[ "$http_redirect" = '307 https://127.0.0.1/' ] || + fail "HTTP did not redirect to the HTTPS management page" +landing_page=$(curl -kfsS https://127.0.0.1/) +grep -q '

TurnKey GameServer

' <<<"$landing_page" || + fail "game-server management landing page did not render" +grep -q 'Webmin' <<<"$landing_page" || fail "management link is missing" +[ -x /usr/local/bin/gameserver-init ] || fail "game-server selector is missing" +[ -f /usr/lib/confconsole/plugins.d/Game_Server/update_list.py ] || + fail "catalog update action is missing from Configuration Console" + +update_check=$(turnkey-gameserver-update --check) +printf '%s\n' "$update_check" | grep -qx \ + 'wrapper_candidate_commit=d2017be9f56db0da2a1c45651e656f47bc7ce42a' || + fail "wrapper update candidate is not the verified commit" +printf '%s\n' "$update_check" | grep -qx 'linuxgsm_candidate=v26.2.0' || + fail "LinuxGSM update candidate is not the verified release" +printf '%s\n' "$update_check" | grep -qx \ + 'linuxgsm_candidate_commit=bded3376bc44d20b89fcc6c32f23a66347fffec6' || + fail "LinuxGSM candidate commit mismatch" +printf '%s\n' "$update_check" | grep -qx 'status=up-to-date' || + fail "updater did not report a stable installed state" +update_apply=$(turnkey-gameserver-update --apply --dry-run) +printf '%s\n' "$update_apply" | grep -qx 'apply=dry-run verified exact candidates' || + fail "updater apply plan was not verified" + +[ ! -e /etc/gameserver/installation.done ] || + fail "fixture requires a fresh appliance without an installed game" +fixture_root=/run/gameserver-v19-fixture +fixture_script=/home/gameuser/gameserver/fixtureserver +cleanup() { + systemctl stop gameserver >/dev/null 2>&1 || true + rm -f /etc/gameserver/gameserver "$fixture_script" + rm -rf "$fixture_root" +} +trap cleanup EXIT HUP INT TERM + +install -d -o gameuser -g gameuser -m 0755 "$fixture_root" +install -d -o gameuser -g gameuser -m 0755 /home/gameuser/gameserver +cat > "$fixture_script" <<'EOF' +#!/bin/bash -e + +case "$1" in + start) touch /run/gameserver-v19-fixture/running ;; + stop) rm -f /run/gameserver-v19-fixture/running ;; + *) exit 2 ;; +esac +EOF +chown gameuser:gameuser "$fixture_script" +chmod 0755 "$fixture_script" +install -d -m 0755 /etc/gameserver +cat > /etc/gameserver/gameserver <<'EOF' +GAME="fixture" +GAME_LONG_NAME="Wave 2 lightweight lifecycle fixture" +EOF + +systemctl daemon-reload +systemctl start gameserver +systemctl -q is-active gameserver || fail "game-server service did not become active" +[ -f "$fixture_root/running" ] || fail "game-server start action did not reach the fixture" +systemctl stop gameserver +[ ! -e "$fixture_root/running" ] || fail "game-server stop action did not reach the fixture" +cleanup +trap - EXIT HUP INT TERM + +GAME=ts3 /usr/local/bin/gameserver-init +[ -e /etc/gameserver/installation.done ] || + fail "TeamSpeak catalog installation did not complete" +grep -qx 'GAME="ts3"' /etc/gameserver/gameserver || + fail "TeamSpeak catalog selection was not retained" +[ -x /home/gameuser/gameserver/ts3server ] || + fail "LinuxGSM TeamSpeak entry point was not installed" +systemctl -q is-active gameserver || fail "installed TeamSpeak server is not active" +runuser -l gameuser -c '~/gameserver/ts3server monitor' >/dev/null || + fail "LinuxGSM did not report the installed TeamSpeak server running" + +if [ -n "${TKL_TEST_RESULT:-}" ]; then + cat > "$TKL_TEST_RESULT" <