#!/bin/bash set -euo pipefail LIVE_PACKAGE=${1:?usage: smoke-fnos-migration.sh liverecorder.fpk postgresql-service.fpk [temporary-directory]} POSTGRES_PACKAGE=${2:?usage: smoke-fnos-migration.sh liverecorder.fpk postgresql-service.fpk [temporary-directory]} SMOKE_TMP_ROOT="${3:-${LIVERECORDER_MIGRATION_SMOKE_TMPDIR:-${TMPDIR:-/tmp}}}" mkdir -p "$SMOKE_TMP_ROOT" SMOKE_TMP_ROOT=$(CDPATH= cd -- "$SMOKE_TMP_ROOT" && pwd) WORK_DIR=$(mktemp -d "${SMOKE_TMP_ROOT%/}/liverecorder-migration-smoke.XXXXXX") STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/liverecorder-migration-state.XXXXXX") LIVE_PACKAGE_ROOT="$WORK_DIR/live-package" LIVE_APP_ROOT="$WORK_DIR/live-app" LIVE_DATA_ROOT="$STATE_DIR/live-var" LIVE_VOLUME_ROOT="$WORK_DIR/live-volume" PG_PACKAGE_ROOT="$WORK_DIR/postgres-package" PG_APP_ROOT="$WORK_DIR/postgres-app" PG_DATA_ROOT="$STATE_DIR/postgres-var" PG_VOLUME_ROOT="$WORK_DIR/postgres-volume" LIVE_PORT=${LIVERECORDER_MIGRATION_SMOKE_PORT:-19680} PRIVATE_PG_PORT=${LIVERECORDER_MIGRATION_PRIVATE_PG_PORT:-19629} PG_API_PORT=${POSTGRES_SERVICE_MIGRATION_API_PORT:-19633} PG_PORT=${POSTGRES_SERVICE_MIGRATION_PG_PORT:-19632} LIVE_CONTROL="$LIVE_PACKAGE_ROOT/cmd/main" PG_CONTROL="$PG_PACKAGE_ROOT/cmd/main" ADMIN_PASSWORD='LiveRecorder-Migration-2026!' PG_ADMIN_PASSWORD='Postgres-Migration-Admin-2026!' ENROLLMENT_TOKEN='Postgres-Migration-Enrollment-2026!' MEDIA_PATH="${PATH:-/usr/local/bin:/usr/bin:/bin}" if ! PATH="$MEDIA_PATH" command -v ffmpeg >/dev/null 2>&1 || \ ! PATH="$MEDIA_PATH" command -v ffprobe >/dev/null 2>&1; then mkdir -p "$WORK_DIR/system-media-stubs" ln -s "$(type -P true)" "$WORK_DIR/system-media-stubs/ffmpeg" ln -s "$(type -P true)" "$WORK_DIR/system-media-stubs/ffprobe" MEDIA_PATH="$WORK_DIR/system-media-stubs:$MEDIA_PATH" fi run_live_control() { TRIM_APPDEST="$LIVE_APP_ROOT" TRIM_PKGVAR="$LIVE_DATA_ROOT" \ TRIM_APPDEST_VOL="$LIVE_VOLUME_ROOT" TRIM_SERVICE_PORT="$LIVE_PORT" \ LIVE_RECORDER_POSTGRES_PORT="$PRIVATE_PG_PORT" \ POSTGRES_SERVICE_API="http://127.0.0.1:$PG_API_PORT" PATH="$MEDIA_PATH" "$LIVE_CONTROL" "$@" } run_postgres_control() { TRIM_APPDEST="$PG_APP_ROOT" TRIM_PKGVAR="$PG_DATA_ROOT" \ TRIM_APPDEST_VOL="$PG_VOLUME_ROOT" TRIM_SERVICE_PORT="$PG_API_PORT" \ POSTGRES_SERVICE_PORT="$PG_PORT" "$PG_CONTROL" "$@" } cleanup() { status=$? if [ -x "$LIVE_CONTROL" ]; then run_live_control stop >/dev/null 2>&1 || true; fi if [ -x "$PG_CONTROL" ]; then run_postgres_control stop >/dev/null 2>&1 || true; fi if [ "$status" -ne 0 ]; then printf '%s\n' 'fnOS PostgreSQL migration smoke test failed; service logs follow:' >&2 for log_file in \ "$PG_DATA_ROOT/log/postgresql.log" "$PG_DATA_ROOT/log/postgres-service.log" \ "$LIVE_DATA_ROOT/log/postgresql.log" "$LIVE_DATA_ROOT/log/liverecorder.log"; do if [ -f "$log_file" ]; then printf '%s\n' "--- $log_file ---" >&2 tail -n 200 "$log_file" >&2 || true fi done fi rm -rf -- "$WORK_DIR" rm -rf -- "$STATE_DIR" return "$status" } trap cleanup EXIT HUP INT TERM mkdir -p "$LIVE_PACKAGE_ROOT" "$LIVE_APP_ROOT" "$LIVE_VOLUME_ROOT" \ "$PG_PACKAGE_ROOT" "$PG_APP_ROOT" "$PG_VOLUME_ROOT" tar -xzf "$LIVE_PACKAGE" -C "$LIVE_PACKAGE_ROOT" tar -xzf "$LIVE_PACKAGE_ROOT/app.tgz" -C "$LIVE_APP_ROOT" rm -f "$LIVE_PACKAGE_ROOT/app.tgz" tar -xzf "$POSTGRES_PACKAGE" -C "$PG_PACKAGE_ROOT" tar -xzf "$PG_PACKAGE_ROOT/app.tgz" -C "$PG_APP_ROOT" rm -f "$PG_PACKAGE_ROOT/app.tgz" TRIM_PKGVAR="$LIVE_DATA_ROOT" \ wizard_admin_password="$ADMIN_PASSWORD" wizard_admin_password_confirm="$ADMIN_PASSWORD" \ wizard_postgres_enrollment_token="$ENROLLMENT_TOKEN" \ "$LIVE_PACKAGE_ROOT/cmd/install_callback" mv "$LIVE_DATA_ROOT/postgres-enrollment-token.seed" "$WORK_DIR/enrollment-token.seed" PRIVATE_PG_BIN="$LIVE_APP_ROOT/runtime/usr/lib/postgresql/15/bin" PRIVATE_PG_SHARE="$LIVE_APP_ROOT/runtime/usr/share/postgresql/15" PRIVATE_PG_LIB="$LIVE_APP_ROOT/runtime/usr/lib/postgresql/15/lib" PRIVATE_RUNTIME_LIBS="$LIVE_APP_ROOT/runtime/lib:$PRIVATE_PG_LIB" PRIVATE_PG_DATA="$LIVE_DATA_ROOT/postgres" PRIVATE_RUN_ROOT="$LIVE_DATA_ROOT/run" mkdir -p "$PRIVATE_PG_DATA" "$PRIVATE_RUN_ROOT" "$LIVE_DATA_ROOT/log" chmod 0700 "$PRIVATE_PG_DATA" "$PRIVATE_RUN_ROOT" env LD_LIBRARY_PATH="$PRIVATE_RUNTIME_LIBS" "$PRIVATE_PG_BIN/initdb" \ -D "$PRIVATE_PG_DATA" -L "$PRIVATE_PG_SHARE" --username=liverecorder \ --auth-local=trust --auth-host=reject --encoding=UTF8 --no-locale \ >"$LIVE_DATA_ROOT/log/postgresql.log" 2>&1 { printf "listen_addresses = ''\n" printf "port = %s\n" "$PRIVATE_PG_PORT" printf "unix_socket_directories = '%s'\n" "$PRIVATE_RUN_ROOT" printf "max_connections = 40\nshared_buffers = '64MB'\ntimezone = 'UTC'\nlog_timezone = 'UTC'\n" } >>"$PRIVATE_PG_DATA/postgresql.conf" # With a legacy PG15 cluster and no enrollment seed, the package must boot the # old database. The Web API then creates the exact EF schema and initial admin. run_live_control start curl -fsS "http://127.0.0.1:$LIVE_PORT/health/ready" | grep -q '"status":"ready"' source_user_count=$(env LD_LIBRARY_PATH="$PRIVATE_RUNTIME_LIBS" \ "$PRIVATE_PG_BIN/psql" -h "$PRIVATE_RUN_ROOT" -p "$PRIVATE_PG_PORT" \ -U liverecorder -d live_recorder -Atqc 'SELECT count(*) FROM "UserAccounts"') test "$source_user_count" -gt 0 run_live_control stop TRIM_PKGVAR="$PG_DATA_ROOT" \ wizard_postgres_admin_password="$PG_ADMIN_PASSWORD" \ wizard_postgres_admin_password_confirm="$PG_ADMIN_PASSWORD" \ wizard_postgres_enrollment_token="$ENROLLMENT_TOKEN" \ wizard_postgres_enrollment_token_confirm="$ENROLLMENT_TOKEN" \ "$PG_PACKAGE_ROOT/cmd/install_callback" mv "$WORK_DIR/enrollment-token.seed" "$LIVE_DATA_ROOT/postgres-enrollment-token.seed" chmod 0600 "$LIVE_DATA_ROOT/postgres-enrollment-token.seed" run_postgres_control start run_live_control start curl -fsS "http://127.0.0.1:$LIVE_PORT/health/ready" | grep -q '"status":"ready"' MARKER="$LIVE_DATA_ROOT/postgres-migration/shared-database.active" DUMP="$LIVE_DATA_ROOT/postgres-migration/private-postgres-15.dump" test -s "$MARKER" grep -q '^migrated_at=' "$MARKER" test -s "$DUMP" sha256sum -c "$DUMP.sha256" >/dev/null test -f "$LIVE_DATA_ROOT/postgres/PG_VERSION" if env LD_LIBRARY_PATH="$PRIVATE_RUNTIME_LIBS" \ "$PRIVATE_PG_BIN/pg_ctl" -D "$PRIVATE_PG_DATA" status >/dev/null 2>&1; then printf '%s\n' 'legacy private PostgreSQL was still running after migration' >&2 exit 1 fi CREDENTIALS="$LIVE_DATA_ROOT/postgres-client.conf" shared_database=$(sed -n 's/^database=//p' "$CREDENTIALS") shared_user=$(sed -n 's/^username=//p' "$CREDENTIALS") shared_password=$(sed -n 's/^password=//p' "$CREDENTIALS") SHARED_PG_BIN="$PG_APP_ROOT/runtime/usr/lib/postgresql/15/bin" SHARED_PG_LIBS="$PG_APP_ROOT/runtime/usr/lib/x86_64-linux-gnu:$PG_APP_ROOT/runtime/lib/x86_64-linux-gnu:$PG_APP_ROOT/runtime/usr/lib/postgresql/15/lib" target_user_count=$(env LD_LIBRARY_PATH="$SHARED_PG_LIBS" PGPASSWORD="$shared_password" \ "$SHARED_PG_BIN/psql" -h 127.0.0.1 -p "$PG_PORT" -U "$shared_user" \ -d "$shared_database" -Atqc 'SELECT count(*) FROM "UserAccounts"') test "$source_user_count" = "$target_user_count" run_live_control stop run_postgres_control status curl -fsS "http://127.0.0.1:$PG_API_PORT/health/ready" | grep -q '"status":"ready"' printf '%s\n' 'fnOS migration smoke test passed: populated legacy PG15 migrated with row-count parity, checksum dump and rollback data preserved'