v0.12.0 migrate sqlite3 to psql18, updated auth, deploy docs, account and replay updates
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
This commit is contained in:
1 parent
5d2be4abb2
commit
d0326b059e
66 files changed
+4016
-2433
No files matched your search
@@ -0,0 +1,18 @@
|
||||
# Deployment Scripts
|
||||
|
||||
Run in this order; each script's header documents its inputs. The full guide is
|
||||
[doc/deployment.md](../doc/deployment.md).
|
||||
|
||||
| Order | Where | Script | Purpose |
|
||||
|---|---|---|---|
|
||||
| 1 | Bastille host | `freebsd/host.sh <jail>` | `sysvshm=new` for PostgreSQL, jail restart |
|
||||
| 2 | Build machine | `make server-freebsd` | Static `bin/chess-server-freebsd-amd64` |
|
||||
| 3 | Jail (root) | `freebsd/setup-jail.sh` | PostgreSQL 18, database, account, service, backups, optional SQLite import |
|
||||
| 4 | Bastille host | `freebsd/nginx-chess.conf` | Location block for the host nginx |
|
||||
|
||||
Supporting files, installed by `setup-jail.sh`:
|
||||
|
||||
- `postgresql/setup.sql` — role, database, and schema (owner or split mode)
|
||||
- `postgresql/migrate-sqlite.sh` — one-time import of a v0.11 SQLite database
|
||||
- `freebsd/rc.d/chess_server` — `daemon(8)` service script
|
||||
- `freebsd/chess-backup.sh` — nightly `pg_dump` with 14-day retention
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
# Nightly logical backup of the chess database. setup-jail.sh installs this as
|
||||
# /usr/local/sbin/chess-backup and schedules it as the postgres user through
|
||||
# /usr/local/etc/cron.d/chess-backup.
|
||||
#
|
||||
# CHESS_BACKUP_DIR destination (default: /var/db/postgres/backups)
|
||||
# CHESS_BACKUP_KEEP days of dumps to keep (default: 14)
|
||||
#
|
||||
# Restore into a freshly provisioned database with:
|
||||
# su -m postgres -c 'pg_restore --no-owner --role=chess -d chess <dump>'
|
||||
# (use --role=chess_owner for the split-privilege setup).
|
||||
set -eu
|
||||
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
|
||||
|
||||
dir=${CHESS_BACKUP_DIR:-/var/db/postgres/backups}
|
||||
keep=${CHESS_BACKUP_KEEP:-14}
|
||||
umask 077
|
||||
mkdir -p "$dir"
|
||||
|
||||
target="$dir/chess-$(date -u +%Y%m%dT%H%M%SZ).dump"
|
||||
pg_dump --format=custom --dbname=chess --file="$target.partial"
|
||||
mv "$target.partial" "$target"
|
||||
find "$dir" -name 'chess-*.dump' -type f -mtime "+$keep" -delete
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
# Host-side preparation for the chess jail. Run as root on the Bastille host:
|
||||
#
|
||||
# sh deploy/freebsd/host.sh <jail-name>
|
||||
#
|
||||
# PostgreSQL allocates a small System V shared-memory segment. This gives the
|
||||
# jail its own SysV shared-memory namespace (isolated, unlike the older
|
||||
# allow.sysvipc) and restarts the jail so the parameter takes effect. The
|
||||
# restart stops every service in the jail, including a running chess-server.
|
||||
#
|
||||
# After setup-jail.sh has run inside the jail, add the location block from
|
||||
# deploy/freebsd/nginx-chess.conf to the host nginx configuration, then
|
||||
# nginx -t && service nginx reload
|
||||
set -eu
|
||||
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
|
||||
|
||||
[ $# -eq 1 ] || { echo "usage: $0 <jail-name>" >&2; exit 2; }
|
||||
jail=$1
|
||||
[ "$(id -u)" -eq 0 ] || { echo "host.sh: run as root" >&2; exit 1; }
|
||||
command -v bastille >/dev/null 2>&1 || { echo "host.sh: bastille not found" >&2; exit 1; }
|
||||
|
||||
bastille config "$jail" set sysvshm new
|
||||
bastille restart "$jail"
|
||||
echo "host.sh: $jail now has sysvshm=new; run deploy/freebsd/setup-jail.sh inside it"
|
||||
@@ -0,0 +1,19 @@
|
||||
# Host nginx: proxy /chess/ to the chess API in the jail. Include inside the
|
||||
# existing TLS server block, next to the security headers. Replace
|
||||
# JAIL_IP:API_PORT with the values given to setup-jail.sh.
|
||||
#
|
||||
# chess-server trusts X-Real-IP only from the address in -trusted-proxies
|
||||
# (TRUSTED_PROXIES in setup-jail.sh): the address this nginx uses to reach
|
||||
# the jail. Do not forward X-Forwarded-For for rate limiting; its first entry
|
||||
# is supplied by the client.
|
||||
location /chess/ {
|
||||
proxy_pass http://JAIL_IP:API_PORT/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# Long-polling waits up to 30 seconds for a game update.
|
||||
proxy_read_timeout 45s;
|
||||
# Bound request bodies; API payloads are small JSON documents.
|
||||
client_max_body_size 16k;
|
||||
}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PROVIDE: chess_server
|
||||
# REQUIRE: LOGIN postgresql
|
||||
# KEYWORD: shutdown
|
||||
#
|
||||
# rc.conf variables (setup-jail.sh sets them with sysrc):
|
||||
# chess_server_enable YES to start at boot
|
||||
# chess_server_flags chess-server arguments (-dsn, -jwt-secret-file, ...)
|
||||
# chess_server_env environment, including PATH=... for stockfish
|
||||
# chess_server_account unprivileged account (default: chess)
|
||||
# chess_server_binary default: /usr/local/chess/chess-server
|
||||
# chess_server_logfile default: /var/log/chess/chess-server.log
|
||||
#
|
||||
# daemon(8) starts as root, writes the supervisor pidfile, drops to the
|
||||
# account, restarts the server if it exits (-r, 5 s delay), and reopens the
|
||||
# log file on SIGHUP (-H) for newsyslog. Do not set chess_server_user: rc.subr
|
||||
# would switch users before daemon runs and daemon could not drop privileges.
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name=chess_server
|
||||
rcvar=chess_server_enable
|
||||
load_rc_config $name
|
||||
|
||||
: ${chess_server_enable:="NO"}
|
||||
: ${chess_server_account:="chess"}
|
||||
: ${chess_server_binary:="/usr/local/chess/chess-server"}
|
||||
: ${chess_server_logfile:="/var/log/chess/chess-server.log"}
|
||||
|
||||
pidfile="/var/run/${name}.pid"
|
||||
procname="daemon"
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-f -r -R 5 -H -P ${pidfile} -o ${chess_server_logfile} -u ${chess_server_account} ${chess_server_binary} ${chess_server_flags}"
|
||||
|
||||
run_rc_command "$1"
|
||||
@@ -0,0 +1,284 @@
|
||||
#!/bin/sh
|
||||
# Install or update chess-server with PostgreSQL 18 inside a FreeBSD jail.
|
||||
# Run as root inside the jail, from a copy of this repository:
|
||||
#
|
||||
# CHESS_BINARY=/tmp/chess-server-freebsd-amd64 \
|
||||
# API_HOST=10.17.89.10 TRUSTED_PROXIES=10.17.89.1 \
|
||||
# sh deploy/freebsd/setup-jail.sh
|
||||
#
|
||||
# Before the first run:
|
||||
# 1. On the host: sh deploy/freebsd/host.sh <jail> (sysvshm=new, restart)
|
||||
# 2. On a build machine: make server-freebsd, then copy
|
||||
# bin/chess-server-freebsd-amd64 into the jail
|
||||
# 3. For an existing SQLite deployment: note the database path; the old
|
||||
# service is stopped before the import
|
||||
#
|
||||
# Required:
|
||||
# CHESS_BINARY new chess-server binary (FreeBSD/amd64, static)
|
||||
# API_HOST address chess-server listens on (the jail IP)
|
||||
#
|
||||
# Optional:
|
||||
# API_PORT default 8080
|
||||
# TRUSTED_PROXIES host nginx address(es) as seen from the jail; without it
|
||||
# all proxied clients share one rate-limit bucket
|
||||
# CHESS_HOME default: the chess account's home, else /usr/local/chess
|
||||
# LOG_DIR default /var/log/chess
|
||||
# SQLITE_DB v0.11 SQLite database to import once into an empty schema
|
||||
# OLD_SERVICE rc.d name of the previous chess service to stop and
|
||||
# disable (for example chess); skipped when unset
|
||||
# SPLIT_PRIVILEGES yes: runtime role gets DML only, schema owned by
|
||||
# chess_owner (default no; see deploy/postgresql/setup.sql)
|
||||
# EXTRA_FLAGS additional chess-server flags (e.g. "-max-users 500")
|
||||
# PGDATA default /var/db/postgres/data18
|
||||
#
|
||||
# The script is idempotent: rerun it to upgrade the binary, change flags, or
|
||||
# repair configuration. It never drops data. Steps:
|
||||
# packages -> PostgreSQL init and hardening -> role/database/schema ->
|
||||
# chess account, binary, JWT key, log dir -> stop old service -> schema
|
||||
# migration -> optional SQLite import -> rc.d/rc.conf -> log rotation ->
|
||||
# nightly backup -> start and health check
|
||||
set -eu
|
||||
PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
|
||||
umask 022
|
||||
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
repo=$(cd "$here/../.." && pwd)
|
||||
|
||||
: "${CHESS_BINARY:?set CHESS_BINARY to the new chess-server binary}"
|
||||
: "${API_HOST:?set API_HOST to the address chess-server listens on}"
|
||||
API_PORT=${API_PORT:-8080}
|
||||
TRUSTED_PROXIES=${TRUSTED_PROXIES:-}
|
||||
LOG_DIR=${LOG_DIR:-/var/log/chess}
|
||||
SQLITE_DB=${SQLITE_DB:-}
|
||||
OLD_SERVICE=${OLD_SERVICE:-}
|
||||
SPLIT_PRIVILEGES=${SPLIT_PRIVILEGES:-no}
|
||||
EXTRA_FLAGS=${EXTRA_FLAGS:-}
|
||||
PGDATA=${PGDATA:-/var/db/postgres/data18}
|
||||
# Install locations; overridable only for testing the script.
|
||||
RC_DIR=${RC_DIR:-/usr/local/etc/rc.d}
|
||||
NEWSYSLOG_DIR=${NEWSYSLOG_DIR:-/usr/local/etc/newsyslog.conf.d}
|
||||
CRON_DIR=${CRON_DIR:-/usr/local/etc/cron.d}
|
||||
SBIN_DIR=${SBIN_DIR:-/usr/local/sbin}
|
||||
BACKUP_DIR=${BACKUP_DIR:-/var/db/postgres/backups}
|
||||
|
||||
# The database role is named after the OS account: peer authentication maps
|
||||
# one to the other, so no password exists anywhere.
|
||||
account=chess
|
||||
dsn='postgres:///chess?host=/tmp'
|
||||
|
||||
log() { printf '==> %s\n' "$*"; }
|
||||
die() { printf 'setup-jail: %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
# Run SQL from stdin as the postgres superuser against database $1.
|
||||
pg_sql() {
|
||||
su -m postgres -c "psql -X -A -t -q -v ON_ERROR_STOP=1 -d $1"
|
||||
}
|
||||
|
||||
# --- preflight ---------------------------------------------------------------
|
||||
[ "$(id -u)" -eq 0 ] || die "run as root"
|
||||
[ "$(uname -s)" = FreeBSD ] || die "this script targets FreeBSD"
|
||||
[ -f "$CHESS_BINARY" ] || die "CHESS_BINARY not found: $CHESS_BINARY"
|
||||
for f in "$repo/deploy/postgresql/setup.sql" "$repo/deploy/postgresql/migrate-sqlite.sh" \
|
||||
"$here/rc.d/chess_server" "$here/chess-backup.sh"; do
|
||||
[ -f "$f" ] || die "missing $f; run from a complete repository copy"
|
||||
done
|
||||
case $SPLIT_PRIVILEGES in yes | no) ;; *) die "SPLIT_PRIVILEGES must be yes or no" ;; esac
|
||||
if [ -n "$SQLITE_DB" ] && [ ! -r "$SQLITE_DB" ]; then
|
||||
die "SQLITE_DB not readable: $SQLITE_DB"
|
||||
fi
|
||||
[ -n "$TRUSTED_PROXIES" ] ||
|
||||
log "WARNING: TRUSTED_PROXIES unset; behind nginx every client shares one rate limit"
|
||||
|
||||
# --- packages ----------------------------------------------------------------
|
||||
packages="postgresql18-server postgresql18-client stockfish"
|
||||
[ -n "$SQLITE_DB" ] && packages="$packages sqlite3"
|
||||
log "installing packages: $packages"
|
||||
# shellcheck disable=SC2086 # word splitting intended
|
||||
pkg install -y $packages
|
||||
|
||||
# --- PostgreSQL ----------------------------------------------------------------
|
||||
sysrc -q postgresql_enable=YES >/dev/null
|
||||
sysrc -q postgresql_data="$PGDATA" >/dev/null
|
||||
sysrc -q postgresql_initdb_flags="--encoding=UTF8 --locale-provider=builtin --builtin-locale=C.UTF-8 --auth-local=peer --auth-host=reject" >/dev/null
|
||||
|
||||
if [ ! -f "$PGDATA/PG_VERSION" ]; then
|
||||
log "initializing PostgreSQL cluster in $PGDATA"
|
||||
if ! service postgresql initdb; then
|
||||
die "initdb failed. If it reports a shared memory error, run deploy/freebsd/host.sh <jail> on the host (sysvshm=new) and retry"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only the postgres superuser and the chess role, each over the Unix socket
|
||||
# with peer authentication. No host lines: TCP is disabled below.
|
||||
hba="$PGDATA/pg_hba.conf"
|
||||
if ! grep -q '^# Managed by chess setup-jail.sh' "$hba" 2>/dev/null; then
|
||||
[ -f "$hba.orig" ] || cp -p "$hba" "$hba.orig"
|
||||
log "writing $hba (original kept as pg_hba.conf.orig)"
|
||||
cat >"$hba" <<'HBA'
|
||||
# Managed by chess setup-jail.sh; edits are kept on rerun.
|
||||
# TYPE DATABASE USER METHOD
|
||||
local all postgres peer
|
||||
local chess chess peer
|
||||
HBA
|
||||
chown postgres:postgres "$hba"
|
||||
chmod 600 "$hba"
|
||||
fi
|
||||
|
||||
log "starting PostgreSQL"
|
||||
service postgresql status >/dev/null 2>&1 || service postgresql start
|
||||
# ALTER SYSTEM writes postgresql.auto.conf, leaving postgresql.conf untouched.
|
||||
pg_sql postgres <<'SQL'
|
||||
ALTER SYSTEM SET listen_addresses = '';
|
||||
ALTER SYSTEM SET unix_socket_directories = '/tmp';
|
||||
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
|
||||
SQL
|
||||
# listen_addresses and unix_socket_directories apply only after a restart.
|
||||
service postgresql restart
|
||||
|
||||
if [ -z "$(echo "SELECT 1 FROM pg_roles WHERE rolname = 'chess'" | pg_sql postgres)" ]; then
|
||||
log "provisioning role, database, and schema (split privileges: $SPLIT_PRIVILEGES)"
|
||||
split=false
|
||||
[ "$SPLIT_PRIVILEGES" = yes ] && split=true
|
||||
su -m postgres -c "psql -X -q -d postgres -v split=$split -f -" <"$repo/deploy/postgresql/setup.sql"
|
||||
else
|
||||
log "role chess exists; leaving database provisioning unchanged"
|
||||
fi
|
||||
|
||||
# --- chess account and files ---------------------------------------------------
|
||||
if ! id "$account" >/dev/null 2>&1; then
|
||||
CHESS_HOME=${CHESS_HOME:-/usr/local/chess}
|
||||
log "creating account $account (home $CHESS_HOME, no shell, no password)"
|
||||
pw useradd -n "$account" -c "Chess server" -d "$CHESS_HOME" -s /usr/sbin/nologin -w no
|
||||
else
|
||||
CHESS_HOME=${CHESS_HOME:-$(pw usershow "$account" | cut -d: -f9)}
|
||||
shell=$(pw usershow "$account" | cut -d: -f10)
|
||||
case $shell in
|
||||
*/nologin | */false) ;;
|
||||
*) log "WARNING: $account has login shell $shell; consider: pw usermod $account -s /usr/sbin/nologin" ;;
|
||||
esac
|
||||
fi
|
||||
[ -d "$CHESS_HOME" ] || install -d -o root -g "$account" -m 0750 "$CHESS_HOME"
|
||||
install -d -o "$account" -g "$account" -m 0750 "$LOG_DIR"
|
||||
|
||||
binary="$CHESS_HOME/chess-server"
|
||||
if [ -f "$binary" ] && ! cmp -s "$CHESS_BINARY" "$binary"; then
|
||||
log "keeping the previous binary as $binary.prev"
|
||||
cp -p "$binary" "$binary.prev"
|
||||
fi
|
||||
# Root-owned: the service account cannot replace its own executable.
|
||||
install -o root -g wheel -m 0555 "$CHESS_BINARY" "$binary"
|
||||
|
||||
key="$CHESS_HOME/jwt.key"
|
||||
if [ ! -f "$key" ]; then
|
||||
log "generating JWT signing key $key"
|
||||
(umask 077 && openssl rand -base64 48 >"$key")
|
||||
fi
|
||||
chown "$account:$account" "$key"
|
||||
chmod 600 "$key"
|
||||
|
||||
# --- stop the previous service ------------------------------------------------
|
||||
if [ -n "$OLD_SERVICE" ] && [ "$OLD_SERVICE" != chess_server ]; then
|
||||
log "stopping and disabling previous service $OLD_SERVICE"
|
||||
service "$OLD_SERVICE" onestop || true
|
||||
sysrc -q "${OLD_SERVICE}_enable=NO" >/dev/null || true
|
||||
fi
|
||||
service chess_server onestatus >/dev/null 2>&1 && service chess_server onestop
|
||||
|
||||
# --- schema ------------------------------------------------------------------
|
||||
log "creating or migrating the chess schema"
|
||||
if [ "$SPLIT_PRIVILEGES" = yes ]; then
|
||||
# The postgres account migrates as chess_owner. It may not be able to
|
||||
# traverse the chess home, so it runs a temporary copy of the binary.
|
||||
migrator=$(mktemp /tmp/chess-migrate.XXXXXX)
|
||||
install -o root -g wheel -m 0555 "$CHESS_BINARY" "$migrator"
|
||||
su -m postgres -c "$migrator db init -dsn \"dbname=chess user=postgres options='-c role=chess_owner -c search_path=chess'\"" ||
|
||||
{ rm -f "$migrator"; die "schema migration failed"; }
|
||||
rm -f "$migrator"
|
||||
else
|
||||
su -m "$account" -c "$binary db init -dsn '$dsn'"
|
||||
fi
|
||||
|
||||
# --- optional SQLite import ----------------------------------------------------
|
||||
if [ -n "$SQLITE_DB" ]; then
|
||||
if [ -n "$(echo 'SELECT 1 FROM chess.users UNION ALL SELECT 1 FROM chess.games LIMIT 1' | pg_sql chess)" ]; then
|
||||
log "chess schema already has data; skipping SQLite import"
|
||||
else
|
||||
log "importing $SQLITE_DB"
|
||||
work=$(mktemp -d /tmp/chess-import.XXXXXX)
|
||||
# Copy WAL companions too: after an unclean stop they hold recent writes.
|
||||
for suffix in "" -wal -shm; do
|
||||
if [ -f "$SQLITE_DB$suffix" ]; then
|
||||
cp "$SQLITE_DB$suffix" "$work/legacy.db$suffix"
|
||||
fi
|
||||
done
|
||||
chown -R postgres "$work"
|
||||
su -m postgres -c "cd '$work' && CHESS_SCHEMA=chess sh -s -- '$work/legacy.db' dbname=chess" \
|
||||
<"$repo/deploy/postgresql/migrate-sqlite.sh"
|
||||
rm -rf "$work"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- service configuration -------------------------------------------------------
|
||||
if [ -f "$RC_DIR/chess_server" ] && ! cmp -s "$here/rc.d/chess_server" "$RC_DIR/chess_server"; then
|
||||
cp -p "$RC_DIR/chess_server" "$RC_DIR/chess_server.orig"
|
||||
fi
|
||||
install -d "$RC_DIR"
|
||||
install -o root -g wheel -m 0555 "$here/rc.d/chess_server" "$RC_DIR/chess_server"
|
||||
|
||||
flags="-api-host $API_HOST -api-port $API_PORT -dsn $dsn -jwt-secret-file $key -log-level info"
|
||||
[ -n "$TRUSTED_PROXIES" ] && flags="$flags -trusted-proxies $TRUSTED_PROXIES"
|
||||
[ -n "$EXTRA_FLAGS" ] && flags="$flags $EXTRA_FLAGS"
|
||||
sysrc -q chess_server_enable=YES >/dev/null
|
||||
sysrc -q chess_server_account="$account" >/dev/null
|
||||
sysrc -q chess_server_binary="$binary" >/dev/null
|
||||
sysrc -q chess_server_logfile="$LOG_DIR/chess-server.log" >/dev/null
|
||||
sysrc -q chess_server_env="PATH=/usr/local/bin:/usr/bin:/bin" >/dev/null
|
||||
sysrc -q chess_server_flags="$flags" >/dev/null
|
||||
|
||||
# Rotate daily or at 1 MB, keep a week; SIGHUP makes daemon(8) reopen the log.
|
||||
install -d "$NEWSYSLOG_DIR"
|
||||
printf '%s\n' \
|
||||
"# Managed by chess setup-jail.sh" \
|
||||
"$LOG_DIR/chess-server.log $account:$account 640 7 1000 @T00 JC /var/run/chess_server.pid 1" \
|
||||
>"$NEWSYSLOG_DIR/chess_server.conf"
|
||||
|
||||
# Nightly pg_dump at 03:30, as postgres, keeping 14 days.
|
||||
install -d "$SBIN_DIR"
|
||||
install -o root -g wheel -m 0555 "$here/chess-backup.sh" "$SBIN_DIR/chess-backup"
|
||||
install -d -o postgres -g postgres -m 0700 "$BACKUP_DIR"
|
||||
install -d "$CRON_DIR"
|
||||
printf '%s\n' \
|
||||
"# Managed by chess setup-jail.sh" \
|
||||
"SHELL=/bin/sh" \
|
||||
"PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin" \
|
||||
"30 3 * * * postgres CHESS_BACKUP_DIR=$BACKUP_DIR $SBIN_DIR/chess-backup" \
|
||||
>"$CRON_DIR/chess-backup"
|
||||
|
||||
# --- start and verify -----------------------------------------------------------
|
||||
log "starting chess_server"
|
||||
service chess_server start
|
||||
health=""
|
||||
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do
|
||||
if health=$(fetch -q -T 2 -o - "http://$API_HOST:$API_PORT/health" 2>/dev/null); then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
case $health in
|
||||
*'"storage":"ok"'*) log "healthy: $health" ;;
|
||||
*) die "chess-server did not report healthy storage (got: ${health:-no response}); see $LOG_DIR/chess-server.log" ;;
|
||||
esac
|
||||
|
||||
cat <<EOF
|
||||
|
||||
chess-server is running at http://$API_HOST:$API_PORT with PostgreSQL storage.
|
||||
|
||||
Remaining host step: add the location block from deploy/freebsd/nginx-chess.conf
|
||||
(proxy_pass http://$API_HOST:$API_PORT/) to the host nginx, then
|
||||
nginx -t && service nginx reload
|
||||
|
||||
Administration inside the jail:
|
||||
su -m $account -c '$binary db user list -dsn "$dsn"'
|
||||
service chess_server status|restart
|
||||
tail -f $LOG_DIR/chess-server.log
|
||||
EOF
|
||||
Executable
+171
@@ -0,0 +1,171 @@
|
||||
#!/bin/sh
|
||||
# One-time import of a chess SQLite database (schema v2, chess <= v0.11) into
|
||||
# the PostgreSQL schema created by `chess-server db init`.
|
||||
#
|
||||
# Usage: migrate-sqlite.sh /path/to/chess.db [psql-conninfo]
|
||||
#
|
||||
# conninfo libpq connection string for the chess database; defaults to
|
||||
# $CHESS_DSN. Run as a role that can write the chess tables and
|
||||
# create temporary tables (the postgres superuser is simplest).
|
||||
# CHESS_SCHEMA schema holding the chess tables (default: chess)
|
||||
#
|
||||
# Stop the old server first so the SQLite WAL is checkpointed. The import runs
|
||||
# in one transaction and refuses a target that already holds users or games.
|
||||
#
|
||||
# - Every account is imported as a regular account; the old temporary/permanent
|
||||
# distinction and account expiry no longer exist.
|
||||
# - Expired sessions are dropped.
|
||||
# - Legacy games recorded before claim columns existed get claims backfilled
|
||||
# from player IDs that match an imported user, and every claim records the
|
||||
# claimant's username.
|
||||
# - Games without a claim are imported too; the server deletes them once they
|
||||
# have been idle for -anonymous-game-ttl (24 hours by default).
|
||||
set -eu
|
||||
|
||||
die() {
|
||||
echo "migrate-sqlite: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ $# -ge 1 ] && [ $# -le 2 ] || die "usage: $0 /path/to/chess.db [psql-conninfo]"
|
||||
sqlite_db=$1
|
||||
conninfo=${2:-${CHESS_DSN:-}}
|
||||
schema=${CHESS_SCHEMA:-chess}
|
||||
|
||||
for tool in sqlite3 psql; do
|
||||
command -v "$tool" >/dev/null 2>&1 || die "$tool is required"
|
||||
done
|
||||
[ -r "$sqlite_db" ] || die "cannot read $sqlite_db"
|
||||
[ -n "$conninfo" ] || die "no connection string: pass one or set CHESS_DSN"
|
||||
|
||||
version=$(sqlite3 -readonly "$sqlite_db" 'PRAGMA user_version;')
|
||||
[ "$version" = 2 ] || die "SQLite schema version is $version; start chess v0.11 on it once to upgrade to version 2"
|
||||
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT INT TERM
|
||||
|
||||
export_table() {
|
||||
sqlite3 -readonly -csv -nullvalue '\N' "$sqlite_db" "$2" >"$work/$1.csv"
|
||||
}
|
||||
export_table users 'SELECT user_id, username, email, password_hash, account_type,
|
||||
created_at, expires_at, last_login_at FROM users'
|
||||
export_table sessions 'SELECT session_id, user_id, created_at, expires_at FROM sessions'
|
||||
export_table games 'SELECT game_id, initial_fen,
|
||||
white_player_id, white_type, white_level, white_search_time, white_claimed_by,
|
||||
black_player_id, black_type, black_level, black_search_time, black_claimed_by,
|
||||
start_time_utc, result, end_time_utc FROM games'
|
||||
export_table moves 'SELECT game_id, move_number, move_uci, fen_after_move,
|
||||
player_color, move_time_utc FROM moves'
|
||||
|
||||
# psql's \copy paths are resolved relative to the working directory.
|
||||
cd "$work"
|
||||
psql -X -q -v ON_ERROR_STOP=1 -v schema="$schema" "$conninfo" <<'SQL'
|
||||
BEGIN;
|
||||
SET LOCAL search_path TO :"schema";
|
||||
-- Legacy CURRENT_TIMESTAMP defaults carry no offset and were written in UTC.
|
||||
SET LOCAL TIME ZONE 'UTC';
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
-- Separate statements: PL/pgSQL plans each one only when it is reached.
|
||||
IF to_regclass('schema_version') IS NULL THEN
|
||||
RAISE EXCEPTION 'no chess schema in search_path; run chess-server db init first';
|
||||
END IF;
|
||||
IF (SELECT version FROM schema_version) <> 1 THEN
|
||||
RAISE EXCEPTION 'target schema must be at version 1';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM users) OR EXISTS (SELECT 1 FROM games) THEN
|
||||
RAISE EXCEPTION 'target already contains chess data; import only into an empty schema';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TEMP TABLE import_users (
|
||||
user_id text, username text, email text, password_hash text, account_type text,
|
||||
created_at text, expires_at text, last_login_at text
|
||||
) ON COMMIT DROP;
|
||||
CREATE TEMP TABLE import_sessions (
|
||||
session_id text, user_id text, created_at text, expires_at text
|
||||
) ON COMMIT DROP;
|
||||
CREATE TEMP TABLE import_games (
|
||||
game_id text, initial_fen text,
|
||||
white_player_id text, white_type smallint, white_level smallint, white_search_time integer,
|
||||
white_claimed_by text,
|
||||
black_player_id text, black_type smallint, black_level smallint, black_search_time integer,
|
||||
black_claimed_by text,
|
||||
start_time_utc text, result text, end_time_utc text
|
||||
) ON COMMIT DROP;
|
||||
CREATE TEMP TABLE import_moves (
|
||||
game_id text, move_number integer, move_uci text, fen_after_move text,
|
||||
player_color text, move_time_utc text
|
||||
) ON COMMIT DROP;
|
||||
|
||||
\copy import_users FROM 'users.csv' WITH (FORMAT csv, NULL '\N')
|
||||
\copy import_sessions FROM 'sessions.csv' WITH (FORMAT csv, NULL '\N')
|
||||
\copy import_games FROM 'games.csv' WITH (FORMAT csv, NULL '\N')
|
||||
\copy import_moves FROM 'moves.csv' WITH (FORMAT csv, NULL '\N')
|
||||
|
||||
INSERT INTO users (user_id, username, email, password_hash, created_at, last_login_at)
|
||||
SELECT user_id::uuid, lower(username), NULLIF(lower(email), ''), password_hash,
|
||||
created_at::timestamptz, NULLIF(last_login_at, '')::timestamptz
|
||||
FROM import_users;
|
||||
|
||||
INSERT INTO sessions (session_id, user_id, created_at, expires_at)
|
||||
SELECT s.session_id::uuid, s.user_id::uuid, s.created_at::timestamptz, s.expires_at::timestamptz
|
||||
FROM import_sessions s
|
||||
WHERE s.expires_at::timestamptz > now()
|
||||
AND s.user_id IN (SELECT user_id FROM import_users);
|
||||
|
||||
-- A claim may be empty in legacy rows; before claim columns existed, an
|
||||
-- authenticated creator was recorded only as the slot's player ID. Names are
|
||||
-- filled from the imported users below.
|
||||
INSERT INTO games (game_id, initial_fen,
|
||||
white_player_id, white_type, white_level, white_search_time, white_claimed_by,
|
||||
black_player_id, black_type, black_level, black_search_time, black_claimed_by,
|
||||
start_time_utc, result, end_time_utc)
|
||||
SELECT g.game_id::uuid, g.initial_fen,
|
||||
g.white_player_id::uuid, g.white_type, g.white_level, g.white_search_time,
|
||||
COALESCE(NULLIF(g.white_claimed_by, '')::uuid,
|
||||
(SELECT u.user_id::uuid FROM import_users u WHERE u.user_id = g.white_player_id)),
|
||||
g.black_player_id::uuid, g.black_type, g.black_level, g.black_search_time,
|
||||
COALESCE(NULLIF(g.black_claimed_by, '')::uuid,
|
||||
(SELECT u.user_id::uuid FROM import_users u WHERE u.user_id = g.black_player_id)),
|
||||
g.start_time_utc::timestamptz,
|
||||
NULLIF(g.result, ''),
|
||||
CASE WHEN NULLIF(g.result, '') IS NOT NULL
|
||||
THEN COALESCE(NULLIF(g.end_time_utc, '')::timestamptz, g.start_time_utc::timestamptz)
|
||||
END
|
||||
FROM import_games g;
|
||||
|
||||
UPDATE games g SET
|
||||
white_name = (SELECT u.username FROM users u WHERE u.user_id = g.white_claimed_by),
|
||||
black_name = (SELECT u.username FROM users u WHERE u.user_id = g.black_claimed_by);
|
||||
|
||||
INSERT INTO moves (game_id, move_number, move_uci, fen_after_move, player_color, move_time_utc)
|
||||
SELECT m.game_id::uuid, m.move_number, m.move_uci, m.fen_after_move, m.player_color,
|
||||
m.move_time_utc::timestamptz
|
||||
FROM import_moves m
|
||||
WHERE m.game_id IN (SELECT game_id FROM import_games);
|
||||
|
||||
-- Replay listings read the move count from the last move number, so every
|
||||
-- game's line must be numbered 1..n without gaps.
|
||||
DO $$
|
||||
DECLARE broken integer;
|
||||
BEGIN
|
||||
SELECT count(*) INTO broken FROM (
|
||||
SELECT game_id FROM moves GROUP BY game_id HAVING max(move_number) <> count(*)
|
||||
) gaps;
|
||||
IF broken > 0 THEN
|
||||
RAISE EXCEPTION '% game(s) have gaps in move numbering; import aborted', broken;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
SELECT format('imported: %s users (%s dropped), %s sessions (%s expired or orphaned), %s games (%s anonymous), %s moves (%s orphaned)',
|
||||
(SELECT count(*) FROM users), (SELECT count(*) FROM import_users) - (SELECT count(*) FROM users),
|
||||
(SELECT count(*) FROM sessions), (SELECT count(*) FROM import_sessions) - (SELECT count(*) FROM sessions),
|
||||
(SELECT count(*) FROM games),
|
||||
(SELECT count(*) FROM games WHERE white_claimed_by IS NULL AND black_claimed_by IS NULL),
|
||||
(SELECT count(*) FROM moves), (SELECT count(*) FROM import_moves) - (SELECT count(*) FROM moves)) AS summary
|
||||
\gset
|
||||
\echo :summary
|
||||
COMMIT;
|
||||
SQL
|
||||
@@ -0,0 +1,63 @@
|
||||
-- Provision the chess database on PostgreSQL 18. Run once as a superuser:
|
||||
--
|
||||
-- psql -X -U postgres -d postgres -f setup.sql # owner mode
|
||||
-- psql -X -U postgres -d postgres -v split=true -f setup.sql # split mode
|
||||
--
|
||||
-- Owner mode (default): the login role `chess` owns schema `chess`, so the
|
||||
-- server applies migrations itself at startup. It has no other privileges.
|
||||
--
|
||||
-- Split mode: NOLOGIN role `chess_owner` owns the schema and tables; the login
|
||||
-- role `chess` receives only SELECT/INSERT/UPDATE/DELETE. The server starts
|
||||
-- only when the schema is current; an administrator migrates, as the OS user
|
||||
-- postgres, with
|
||||
-- chess-server db init -dsn "dbname=chess user=postgres \
|
||||
-- options='-c role=chess_owner -c search_path=chess'"
|
||||
--
|
||||
-- Neither mode sets a password: the chess OS user authenticates over the Unix
|
||||
-- socket with peer authentication (see doc/deployment.md for pg_hba.conf).
|
||||
\set ON_ERROR_STOP on
|
||||
\if :{?split}
|
||||
\else
|
||||
\set split false
|
||||
\endif
|
||||
|
||||
CREATE ROLE chess LOGIN
|
||||
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS
|
||||
CONNECTION LIMIT 20;
|
||||
|
||||
-- The builtin C.UTF-8 provider gives code-point ordering and Unicode case
|
||||
-- mapping that do not change with operating-system library upgrades.
|
||||
CREATE DATABASE chess
|
||||
OWNER postgres
|
||||
TEMPLATE template0
|
||||
ENCODING 'UTF8'
|
||||
LOCALE_PROVIDER builtin
|
||||
BUILTIN_LOCALE 'C.UTF-8';
|
||||
|
||||
REVOKE ALL ON DATABASE chess FROM PUBLIC;
|
||||
GRANT CONNECT ON DATABASE chess TO chess;
|
||||
|
||||
\connect chess
|
||||
|
||||
-- Nothing may be created in or read from public; chess lives in its own schema.
|
||||
REVOKE ALL ON SCHEMA public FROM PUBLIC;
|
||||
|
||||
\if :split
|
||||
CREATE ROLE chess_owner NOLOGIN
|
||||
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
|
||||
CREATE SCHEMA chess AUTHORIZATION chess_owner;
|
||||
GRANT USAGE ON SCHEMA chess TO chess;
|
||||
-- Tables created by future migrations inherit runtime DML grants.
|
||||
ALTER DEFAULT PRIVILEGES FOR ROLE chess_owner IN SCHEMA chess
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO chess;
|
||||
\else
|
||||
CREATE SCHEMA chess AUTHORIZATION chess;
|
||||
\endif
|
||||
|
||||
-- Resolve unqualified names in the chess schema without a DSN parameter.
|
||||
ALTER ROLE chess IN DATABASE chess SET search_path = chess;
|
||||
-- Bound runaway statements, lock waits, and abandoned transactions. The
|
||||
-- server's own per-operation deadlines are shorter.
|
||||
ALTER ROLE chess IN DATABASE chess SET statement_timeout = '30s';
|
||||
ALTER ROLE chess IN DATABASE chess SET lock_timeout = '10s';
|
||||
ALTER ROLE chess IN DATABASE chess SET idle_in_transaction_session_timeout = '60s';
|
||||
Reference in new issue
Block a user