Files
gitlab-instance-0a899031_sh/d2ypp2/install.sh
eric 78e1308063 ss
2026-05-19 06:47:07 +00:00

427 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# SHD2YPP2 remote bootstrapper.
#
# Supported entrypoints:
# curl -fsSL http://<host>/install.sh | bash
# curl -fsSL http://<host>/install.sh | bash -s -- --profile all
# sudo bash install.sh --profile hub
#
# The heavy OS/service installation stays in scripts/linux/install_stack.sh.
# This file owns the production bootstrap contract: source acquisition, locking,
# logging, privilege handling, profile dispatch, and post-install verification.
set -Eeuo pipefail
PRODUCT_NAME="SHD2YPP2"
DEFAULT_REPO_URL="http://gitlab.nomadro.com/gitlab-instance-0a899031/sh.git"
DEFAULT_REPO_REF="best"
DEFAULT_REPO_SUBDIR="d2ypp2"
DEFAULT_INSTALL_DIR="/opt/live/d2ypp"
DEFAULT_LOG_DIR="/var/log/shd2ypp2"
DEFAULT_LOCK_FILE="/run/lock/shd2ypp2-install.lock"
PROFILE="all"
REPO_URL="${LIVE_REPO_URL:-$DEFAULT_REPO_URL}"
REPO_REF="${LIVE_REPO_REF:-$DEFAULT_REPO_REF}"
REPO_SUBDIR="${LIVE_REPO_SUBDIR:-$DEFAULT_REPO_SUBDIR}"
INSTALL_DIR="${LIVE_INSTALL_DIR:-$DEFAULT_INSTALL_DIR}"
LOG_DIR="${LIVE_INSTALL_LOG_DIR:-$DEFAULT_LOG_DIR}"
LOCK_FILE="${LIVE_INSTALL_LOCK_FILE:-$DEFAULT_LOCK_FILE}"
VERIFY=1
LOCAL_ONLY=0
KEEP_SOURCE=0
ASSUME_YES=1
SELF_PATH="${BASH_SOURCE[0]:-$0}"
START_TS="$(date +%Y%m%d-%H%M%S)"
LOG_FILE="$LOG_DIR/install-$START_TS.log"
log() {
printf '[install] %s\n' "$*" >&2
}
die() {
log "ERROR: $*"
exit 1
}
usage() {
cat <<USAGE
$PRODUCT_NAME one-click installer
Usage:
curl -fsSL <url>/install.sh | bash
bash install.sh [options]
Options:
--profile all|hub|core|business|live Install profile. Default: all
--repo URL Git repository URL. Default: $DEFAULT_REPO_URL
--ref REF Git branch/tag/commit. Default: $DEFAULT_REPO_REF
--subdir PATH Project subdir inside repo. Default: $DEFAULT_REPO_SUBDIR
--dir PATH Runtime source directory. Default: $DEFAULT_INSTALL_DIR
--no-verify Skip post-install health probes
--local-only Require running from an existing project checkout
--keep-source Keep temporary clone directory for debugging
-h, --help Show this help
Environment overrides:
LIVE_REPO_URL, LIVE_REPO_REF, LIVE_REPO_SUBDIR, LIVE_INSTALL_DIR
HOSTNAME_ALIAS, PORT and other config/system-stack.env values
USAGE
}
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--profile)
PROFILE="${2:-}"
shift 2
;;
--profile=*)
PROFILE="${1#*=}"
shift
;;
--repo)
REPO_URL="${2:-}"
shift 2
;;
--repo=*)
REPO_URL="${1#*=}"
shift
;;
--ref)
REPO_REF="${2:-}"
shift 2
;;
--ref=*)
REPO_REF="${1#*=}"
shift
;;
--subdir)
REPO_SUBDIR="${2:-}"
shift 2
;;
--subdir=*)
REPO_SUBDIR="${1#*=}"
shift
;;
--dir)
INSTALL_DIR="${2:-}"
shift 2
;;
--dir=*)
INSTALL_DIR="${1#*=}"
shift
;;
--no-verify)
VERIFY=0
shift
;;
--local-only)
LOCAL_ONLY=1
shift
;;
--keep-source)
KEEP_SOURCE=1
shift
;;
-y|--yes)
ASSUME_YES=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "Unknown argument: $1"
;;
esac
done
case "$PROFILE" in
all|full) PROFILE="all" ;;
hub|core|sh) PROFILE="hub" ;;
business|live|app|sh2) PROFILE="business" ;;
*) die "Unsupported profile: $PROFILE" ;;
esac
[ -n "$REPO_URL" ] || die "--repo cannot be empty"
[ -n "$REPO_REF" ] || die "--ref cannot be empty"
[ -n "$INSTALL_DIR" ] || die "--dir cannot be empty"
}
have() {
command -v "$1" >/dev/null 2>&1
}
root_prefix() {
if [ "$(id -u)" -eq 0 ]; then
return 0
fi
if ! have sudo; then
die "sudo is required when not running as root. Use: curl -fsSL <url>/install.sh | sudo -E bash"
fi
sudo -n true 2>/dev/null || sudo -v
}
run_root() {
if [ "$(id -u)" -eq 0 ]; then
"$@"
else
sudo -E "$@"
fi
}
run_root_shell() {
if [ "$(id -u)" -eq 0 ]; then
bash -lc "$*"
else
sudo -E bash -lc "$*"
fi
}
setup_logging() {
root_prefix
run_root mkdir -p "$LOG_DIR" "$(dirname "$LOCK_FILE")"
run_root chmod 0755 "$LOG_DIR"
if [ "$(id -u)" -eq 0 ]; then
exec > >(tee -a "$LOG_FILE") 2>&1
else
exec > >(sudo -E tee -a "$LOG_FILE") 2>&1
fi
log "$PRODUCT_NAME bootstrap started at $START_TS"
log "Log file: $LOG_FILE"
}
detect_platform() {
[ -r /etc/os-release ] || die "/etc/os-release not found; Debian/Ubuntu/Armbian are supported"
# shellcheck source=/dev/null
. /etc/os-release
local family="${ID:-} ${ID_LIKE:-}"
if ! printf '%s' "$family" | grep -qiE 'debian|ubuntu'; then
die "Unsupported distribution: ${PRETTY_NAME:-unknown}. Use Debian, Ubuntu, Armbian, Raspberry Pi OS, or a compatible derivative."
fi
local arch
arch="$(uname -m 2>/dev/null || true)"
case "$arch" in
x86_64|amd64|aarch64|arm64|armv7l|armv6l) ;;
*) log "WARN: untested architecture: ${arch:-unknown}; continuing with best-effort install" ;;
esac
log "Platform: ${PRETTY_NAME:-$family}; arch=$arch; kernel=$(uname -r)"
}
apt_install_bootstrap_deps() {
local need_apt=0
for cmd in git curl rsync flock; do
have "$cmd" || need_apt=1
done
[ "$need_apt" = 1 ] || return 0
log "Installing bootstrap dependencies"
run_root env DEBIAN_FRONTEND=noninteractive apt-get update
run_root env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl git rsync util-linux sudo
}
acquire_lock() {
run_root touch "$LOCK_FILE"
if [ "$(id -u)" -ne 0 ]; then
run_root chown "$(id -u):$(id -g)" "$LOCK_FILE"
fi
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
die "Another installer is already running. Lock: $LOCK_FILE"
fi
}
is_project_dir() {
local dir="$1"
[ -f "$dir/scripts/linux/install_stack.sh" ] && [ -f "$dir/web.py" ] && [ -d "$dir/web-console" ]
}
local_project_dir() {
local candidate
if [ -n "$SELF_PATH" ] && [ -f "$SELF_PATH" ]; then
candidate="$(cd "$(dirname "$SELF_PATH")" && pwd)"
if is_project_dir "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
fi
candidate="$(pwd)"
if is_project_dir "$candidate"; then
printf '%s\n' "$candidate"
return 0
fi
return 1
}
clone_source() {
local clone_root project_dir
clone_root="/opt/live/src/shd2ypp2-$START_TS"
log "Fetching source: repo=$REPO_URL ref=$REPO_REF subdir=$REPO_SUBDIR"
run_root mkdir -p "$(dirname "$clone_root")"
run_root git clone --depth 1 --branch "$REPO_REF" "$REPO_URL" "$clone_root" || {
log "Shallow clone failed; retrying full fetch"
run_root rm -rf "$clone_root"
run_root git clone "$REPO_URL" "$clone_root"
run_root git -C "$clone_root" checkout "$REPO_REF"
}
if [ -n "$REPO_SUBDIR" ]; then
project_dir="$clone_root/$REPO_SUBDIR"
else
project_dir="$clone_root"
fi
is_project_dir "$project_dir" || die "Fetched source does not contain SHD2YPP2 project at: $project_dir"
printf '%s\n' "$project_dir"
}
sync_to_install_dir() {
local source_dir="$1"
local target="$INSTALL_DIR"
log "Syncing source to $target"
run_root mkdir -p "$target"
run_root rsync -a --delete \
--exclude '.git/' \
--exclude '.venv/' \
--exclude '.next/' \
--exclude 'node_modules/' \
--exclude 'web-console/node_modules/' \
--exclude 'web-console/.next/' \
--exclude 'logs/' \
--exclude 'downloads/' \
--exclude 'backup_config/' \
--exclude '.pm2/' \
--exclude '.process_runner/' \
--exclude 'services/neko/data/' \
--exclude 'config/system-stack.env' \
"$source_dir/" "$target/"
run_root chown -R live:live "$target" 2>/dev/null || true
printf '%s\n' "$target"
}
resolve_project_dir() {
local project=""
if project="$(local_project_dir 2>/dev/null)"; then
log "Using local project checkout: $project"
project="$(sync_to_install_dir "$project")"
printf '%s\n' "$project"
return 0
fi
if [ "$LOCAL_ONLY" = 1 ]; then
die "--local-only was set, but current directory is not a SHD2YPP2 checkout"
fi
apt_install_bootstrap_deps
local fetched
fetched="$(clone_source)"
sync_to_install_dir "$fetched"
if [ "$KEEP_SOURCE" != 1 ]; then
run_root rm -rf "$(dirname "$fetched")"
else
log "Keeping fetched source at $(dirname "$fetched")"
fi
}
install_profile() {
local project_dir="$1"
local stack="$project_dir/scripts/linux/install_stack.sh"
[ -f "$stack" ] || die "Missing installer: $stack"
log "Dispatching install profile: $PROFILE"
run_root chmod +x "$stack" "$project_dir"/install-*.sh "$project_dir"/start.sh 2>/dev/null || true
run_root env \
LIVE_STACK_REPO_ROOT="$INSTALL_DIR" \
LIVE_INSTALL_LOG_FILE="$LOG_FILE" \
ASSUME_YES="$ASSUME_YES" \
bash "$stack" "$PROFILE"
}
post_verify() {
[ "$VERIFY" = 1 ] || {
log "Post-install verification skipped by --no-verify"
return 0
}
local port="${PORT:-8001}"
if [ -f "$INSTALL_DIR/config/system-stack.env" ]; then
# shellcheck source=/dev/null
. "$INSTALL_DIR/config/system-stack.env" || true
port="${PORT:-8001}"
fi
log "Waiting for control plane on 127.0.0.1:$port"
local ok=0 i
for i in $(seq 1 90); do
if curl -fsS -m 3 "http://127.0.0.1:$port/health" >/dev/null 2>&1; then
ok=1
break
fi
sleep 2
done
if [ "$ok" != 1 ]; then
run_root journalctl -u live-console.service -n 120 --no-pager || true
die "Control plane did not become healthy on 127.0.0.1:$port"
fi
log "Control plane health OK"
if curl -fsS -m 8 "http://127.0.0.1:$port/deploy/check" >/tmp/shd2ypp2-deploy-check.json 2>/dev/null; then
python3 - <<'PY'
from __future__ import annotations
import json
from pathlib import Path
data = json.loads(Path("/tmp/shd2ypp2-deploy-check.json").read_text(encoding="utf-8"))
status = "READY" if data.get("ready") else "DEGRADED"
print(f"[install] deploy/check: {status} ({data.get('pass_count', 0)}/{data.get('total_count', 0)})")
for item in data.get("failing_checks") or []:
label = item.get("label") or item.get("id") or "check"
detail = item.get("detail") or item.get("error") or ""
print(f"[install] FAIL {label}: {detail}")
for hint in data.get("hints") or []:
print(f"[install] hint: {hint}")
PY
else
log "WARN: /deploy/check unavailable; /health passed"
fi
local verifier="$INSTALL_DIR/tools/verify_local_install.py"
if [ -f "$verifier" ]; then
log "Running local production verifier"
run_root chmod +x "$verifier" 2>/dev/null || true
run_root python3 "$verifier" --install-dir "$INSTALL_DIR" --timeout 6
else
log "WARN: local verifier not found at $verifier"
fi
}
print_finish() {
local host_alias="${HOSTNAME_ALIAS:-live}"
local port="${PORT:-8001}"
if [ -f "$INSTALL_DIR/config/system-stack.env" ]; then
# shellcheck source=/dev/null
. "$INSTALL_DIR/config/system-stack.env" || true
host_alias="${HOSTNAME_ALIAS:-live}"
port="${PORT:-8001}"
fi
cat <<EOF
[install] $PRODUCT_NAME install finished.
[install] Web console: http://${host_alias}.local
[install] Direct URL: http://${host_alias}.local:${port}
[install] Runtime dir: $INSTALL_DIR
[install] Log file: $LOG_FILE
EOF
}
main() {
parse_args "$@"
setup_logging
acquire_lock
detect_platform
apt_install_bootstrap_deps
local project_dir
project_dir="$(resolve_project_dir)"
install_profile "$project_dir"
post_verify
print_finish
}
main "$@"