#!/usr/bin/env bash # SHD2YPP2 remote bootstrapper. # # Supported entrypoints: # curl -fsSL http:///install.sh | bash # curl -fsSL http:///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 </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 /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 <