feat: stabilize fnos lan delivery flow
This commit is contained in:
@@ -58,6 +58,8 @@ CUTVIDEO_BASE_URL="${CUTVIDEO_BASE_URL:-http://$FNOS_HOST:19186}"
|
||||
LIVE_RECORDER_BASE_URL="${LIVE_RECORDER_BASE_URL:-http://192.168.31.188:19106}"
|
||||
CLOUD_DB_PATH="${CLOUD_DB_PATH:-/home/ubuntu/storyforge/data/collector/storyforge.db}"
|
||||
CLOUD_DB_SNAPSHOT_PATH="${CLOUD_DB_SNAPSHOT_PATH:-/home/ubuntu/storyforge/data/collector/storyforge-fnos-sync.db}"
|
||||
COLLECTOR_HEALTH_RETRY_ATTEMPTS="${COLLECTOR_HEALTH_RETRY_ATTEMPTS:-24}"
|
||||
COLLECTOR_HEALTH_RETRY_SLEEP_SEC="${COLLECTOR_HEALTH_RETRY_SLEEP_SEC:-3}"
|
||||
|
||||
need_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
@@ -256,13 +258,20 @@ else
|
||||
fi
|
||||
|
||||
echo "[7/7] verify lan collector and web binding"
|
||||
for _attempt in 1 2 3 4 5 6 7 8 9 10 11 12; do
|
||||
collector_ready=0
|
||||
for _attempt in $(seq 1 "$COLLECTOR_HEALTH_RETRY_ATTEMPTS"); do
|
||||
if curl -fsS "$BACKEND_URL/healthz" >/dev/null; then
|
||||
collector_ready=1
|
||||
break
|
||||
fi
|
||||
sleep 3
|
||||
echo " -> waiting for collector healthz (${_attempt}/$COLLECTOR_HEALTH_RETRY_ATTEMPTS)"
|
||||
sleep "$COLLECTOR_HEALTH_RETRY_SLEEP_SEC"
|
||||
done
|
||||
curl -fsS "$BACKEND_URL/healthz" >/dev/null
|
||||
if [ "$collector_ready" != "1" ]; then
|
||||
echo "collector did not become healthy in time: $BACKEND_URL" >&2
|
||||
run_fnos_sudo "$FNOS_PASSWORD_VALUE" "cd $(shell_quote "$REMOTE_COMPOSE_DIR") && STORYFORGE_COLLECTOR_DEV_PORT=$(shell_quote "$COLLECTOR_PORT") docker compose -f $(shell_quote "$REMOTE_COMPOSE_FILE") ps && STORYFORGE_COLLECTOR_DEV_PORT=$(shell_quote "$COLLECTOR_PORT") docker compose -f $(shell_quote "$REMOTE_COMPOSE_FILE") logs --tail=80 storyforge-collector-dev" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
curl -fsS -X POST "$BACKEND_URL/v2/auth/auto-session" -H 'content-type: application/json' -d '{}' >/dev/null
|
||||
curl -fsS "http://$FNOS_HOST:$FRONTEND_PORT/assets/storyforge-runtime-config.js" | grep -q "$BACKEND_URL"
|
||||
|
||||
|
||||
34
scripts/deploy_fnos_storyforge_lan_stack.sh
Executable file
34
scripts/deploy_fnos_storyforge_lan_stack.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
FNOS_HOST="${FNOS_HOST:-192.168.31.188}"
|
||||
COLLECTOR_PORT="${STORYFORGE_COLLECTOR_DEV_PORT:-19193}"
|
||||
BACKEND_URL="${STORYFORGE_FNOS_BACKEND_URL:-http://$FNOS_HOST:$COLLECTOR_PORT}"
|
||||
SKIP_TUNNEL="${SKIP_TUNNEL:-0}"
|
||||
SKIP_SMOKE="${SKIP_SMOKE:-0}"
|
||||
|
||||
echo "[1/4] ensure fnOS cutvideo tunnel"
|
||||
if [ "$SKIP_TUNNEL" = "1" ]; then
|
||||
echo "skip tunnel deployment because SKIP_TUNNEL=1"
|
||||
else
|
||||
STORYFORGE_FNOS_BACKEND_URL="$BACKEND_URL" bash "$ROOT/scripts/deploy_fnos_cutvideo_tunnel.sh"
|
||||
fi
|
||||
|
||||
echo "[2/4] deploy fnOS collector"
|
||||
STORYFORGE_FNOS_COLLECTOR_URL="$BACKEND_URL" bash "$ROOT/scripts/deploy_fnos_storyforge_collector.sh"
|
||||
|
||||
echo "[3/4] deploy fnOS web"
|
||||
STORYFORGE_FNOS_BACKEND_URL="$BACKEND_URL" bash "$ROOT/scripts/deploy_fnos_storyforge_web.sh"
|
||||
|
||||
echo "[4/4] smoke fnOS lan stack"
|
||||
if [ "$SKIP_SMOKE" = "1" ]; then
|
||||
echo "skip smoke because SKIP_SMOKE=1"
|
||||
else
|
||||
STORYFORGE_FNOS_COLLECTOR_URL="$BACKEND_URL" STORYFORGE_FNOS_BACKEND_URL="$BACKEND_URL" bash "$ROOT/scripts/smoke_fnos_storyforge_lan.sh"
|
||||
fi
|
||||
|
||||
echo "fnOS StoryForge LAN stack ready:"
|
||||
echo " web: http://$FNOS_HOST:${STORYFORGE_WEB_V4_DEV_PORT:-19192}/"
|
||||
echo " collector: $BACKEND_URL"
|
||||
136
scripts/smoke_fnos_storyforge_lan.sh
Executable file
136
scripts/smoke_fnos_storyforge_lan.sh
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
FNOS_HOST="${FNOS_HOST:-192.168.31.188}"
|
||||
WEB_PORT="${STORYFORGE_WEB_V4_DEV_PORT:-19192}"
|
||||
COLLECTOR_PORT="${STORYFORGE_COLLECTOR_DEV_PORT:-19193}"
|
||||
CUTVIDEO_FORWARD_PORT="${CUTVIDEO_FORWARD_PORT:-19186}"
|
||||
STORYFORGE_COMPAT_FORWARD_PORT="${STORYFORGE_COMPAT_FORWARD_PORT:-19181}"
|
||||
WEB_URL="${STORYFORGE_FNOS_WEB_URL:-http://$FNOS_HOST:$WEB_PORT}"
|
||||
BACKEND_URL="${STORYFORGE_FNOS_COLLECTOR_URL:-http://$FNOS_HOST:$COLLECTOR_PORT}"
|
||||
CUTVIDEO_URL="${CUTVIDEO_BASE_URL:-http://$FNOS_HOST:$CUTVIDEO_FORWARD_PORT}"
|
||||
COMPAT_URL="${STORYFORGE_COMPAT_BASE_URL:-http://$FNOS_HOST:$STORYFORGE_COMPAT_FORWARD_PORT}"
|
||||
CURL_MAX_TIME="${STORYFORGE_FNOS_CURL_MAX_TIME:-60}"
|
||||
|
||||
need_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
echo "missing required command: $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
need_cmd curl
|
||||
need_cmd python3
|
||||
need_cmd rg
|
||||
|
||||
curl_fetch() {
|
||||
curl -fsS --max-time "$CURL_MAX_TIME" "$@"
|
||||
}
|
||||
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
index_file="$tmp_dir/index.html"
|
||||
runtime_file="$tmp_dir/runtime.js"
|
||||
health_file="$tmp_dir/health.json"
|
||||
session_file="$tmp_dir/session.json"
|
||||
integrations_file="$tmp_dir/integrations.json"
|
||||
bootstrap_file="$tmp_dir/bootstrap.json"
|
||||
compat_file="$tmp_dir/compat.html"
|
||||
token_file="$tmp_dir/token.txt"
|
||||
|
||||
echo "[1/6] check fnOS web"
|
||||
curl_fetch "$WEB_URL/" >"$index_file"
|
||||
rg -q "StoryForge" "$index_file"
|
||||
echo "web ok"
|
||||
|
||||
echo "[2/6] check runtime config"
|
||||
curl_fetch "$WEB_URL/assets/storyforge-runtime-config.js" >"$runtime_file"
|
||||
rg -q "$BACKEND_URL" "$runtime_file"
|
||||
echo "runtime config ok"
|
||||
|
||||
echo "[3/6] check collector healthz"
|
||||
curl_fetch "$BACKEND_URL/healthz" >"$health_file"
|
||||
python3 - "$health_file" "$CUTVIDEO_URL" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
expected_cutvideo = sys.argv[2]
|
||||
if str(payload.get("status") or "").lower() != "ok":
|
||||
raise SystemExit(f"unexpected health status: {payload.get('status')!r}")
|
||||
lan_routing = payload.get("lanRouting") or {}
|
||||
if not isinstance(lan_routing, dict):
|
||||
raise SystemExit("lanRouting missing")
|
||||
if lan_routing.get("cutvideoBaseUrl") != expected_cutvideo:
|
||||
raise SystemExit(f"unexpected cutvideoBaseUrl: {lan_routing.get('cutvideoBaseUrl')!r}")
|
||||
if lan_routing.get("cutvideoRouteMode") != "fnos_tunnel":
|
||||
raise SystemExit(f"unexpected cutvideoRouteMode: {lan_routing.get('cutvideoRouteMode')!r}")
|
||||
print("healthz ok")
|
||||
PY
|
||||
|
||||
echo "[4/6] check auto-session"
|
||||
curl_fetch -X POST "$BACKEND_URL/v2/auth/auto-session" \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{}' >"$session_file"
|
||||
python3 - "$session_file" "$token_file" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
token = str(payload.get("token") or "")
|
||||
mode = str(payload.get("mode") or "")
|
||||
username = str((payload.get("account") or {}).get("username") or "")
|
||||
if not token:
|
||||
raise SystemExit("auto-session did not return token")
|
||||
if mode != "auto":
|
||||
raise SystemExit(f"unexpected mode: {mode!r}")
|
||||
if not username:
|
||||
raise SystemExit("auto-session returned empty username")
|
||||
pathlib.Path(sys.argv[2]).write_text(token, encoding="utf-8")
|
||||
print(f"auto-session ok: {username}")
|
||||
PY
|
||||
token="$(cat "$token_file")"
|
||||
|
||||
echo "[5/6] check integrations health"
|
||||
curl_fetch "$BACKEND_URL/v2/integrations/health" \
|
||||
-H "Authorization: Bearer $token" >"$integrations_file"
|
||||
python3 - "$integrations_file" "$CUTVIDEO_URL" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
expected_cutvideo = sys.argv[2]
|
||||
cutvideo = payload.get("cutvideo") or {}
|
||||
if cutvideo.get("base_url") != expected_cutvideo:
|
||||
raise SystemExit(f"unexpected cutvideo base_url: {cutvideo.get('base_url')!r}")
|
||||
if not cutvideo.get("reachable"):
|
||||
raise SystemExit("cutvideo is not reachable")
|
||||
if not cutvideo.get("supports_uploads"):
|
||||
raise SystemExit("cutvideo uploads are not available")
|
||||
print("integrations ok")
|
||||
PY
|
||||
|
||||
echo "[6/6] check fnOS tunnel endpoints"
|
||||
curl_fetch "$CUTVIDEO_URL/api/bootstrap" >"$bootstrap_file"
|
||||
curl_fetch "$COMPAT_URL/" >"$compat_file"
|
||||
python3 - "$bootstrap_file" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
|
||||
if not payload:
|
||||
raise SystemExit("empty cutvideo bootstrap payload")
|
||||
print("cutvideo bootstrap ok")
|
||||
PY
|
||||
echo "compat ok"
|
||||
|
||||
echo "fnOS lan smoke passed:"
|
||||
echo " web: $WEB_URL/"
|
||||
echo " collector: $BACKEND_URL"
|
||||
echo " cutvideo: $CUTVIDEO_URL"
|
||||
echo " compat: $COMPAT_URL"
|
||||
Reference in New Issue
Block a user