Phase 1: first-run setup wizard, bash installer, Docker profiles
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+331
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Personal Internet Cell (PIC) — Bash Installer
|
||||
# =============================================================================
|
||||
#
|
||||
# SECURITY NOTICE
|
||||
# ---------------
|
||||
# You are about to execute this script with elevated privileges.
|
||||
# ALWAYS review a script before running it:
|
||||
#
|
||||
# curl -fsSL https://git.pic.ngo/roof/pic/raw/branch/main/install.sh | less
|
||||
#
|
||||
# SHA256 checksum (verify before running):
|
||||
# PLACEHOLDER — updated when script is published at git.pic.ngo
|
||||
#
|
||||
# Verify with:
|
||||
# sha256sum install.sh
|
||||
# # or, via curl before piping:
|
||||
# curl -fsSL https://git.pic.ngo/roof/pic/raw/branch/main/install.sh \
|
||||
# | sha256sum
|
||||
#
|
||||
# =============================================================================
|
||||
#
|
||||
# Usage:
|
||||
# sudo bash install.sh # Standard install
|
||||
# sudo bash install.sh --force # Bypass idempotency check
|
||||
# sudo PIC_DIR=/srv/pic bash install.sh # Custom install directory
|
||||
#
|
||||
# Supported OS: Debian/Ubuntu (apt), Fedora/RHEL (dnf), Alpine Linux (apk)
|
||||
#
|
||||
# =============================================================================
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
PIC_DIR="${PIC_DIR:-/opt/pic}"
|
||||
PIC_REPO="${PIC_REPO:-https://git.pic.ngo/roof/pic.git}"
|
||||
PIC_USER="${PIC_USER:-pic}"
|
||||
API_HEALTH_URL="http://127.0.0.1:3000/health"
|
||||
API_HEALTH_TIMEOUT=60
|
||||
WEBUI_PORT=8081
|
||||
FORCE=0
|
||||
|
||||
# Parse flags
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--force) FORCE=1 ;;
|
||||
*)
|
||||
echo "Unknown argument: $arg" >&2
|
||||
echo "Usage: $0 [--force]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Color output
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ -t 1 ] && command -v tput >/dev/null 2>&1 && tput setaf 1 >/dev/null 2>&1; then
|
||||
RED="$(tput setaf 1)"
|
||||
GREEN="$(tput setaf 2)"
|
||||
YELLOW="$(tput setaf 3)"
|
||||
BOLD="$(tput bold)"
|
||||
RESET="$(tput sgr0)"
|
||||
else
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
fi
|
||||
|
||||
log_step() { printf "\n${BOLD}[%s/%s] %s${RESET}\n" "$1" "$TOTAL_STEPS" "$2"; }
|
||||
log_ok() { printf " ${GREEN}✓${RESET} %s\n" "$1"; }
|
||||
log_warn() { printf " ${YELLOW}⚠${RESET} %s\n" "$1"; }
|
||||
log_error() { printf "\n${RED}${BOLD}ERROR:${RESET}${RED} %s${RESET}\n" "$1" >&2; }
|
||||
|
||||
die() { log_error "$1"; exit 1; }
|
||||
|
||||
TOTAL_STEPS=7
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Must run as root
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
die "This installer must be run as root (use sudo)."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Idempotency guard
|
||||
# ---------------------------------------------------------------------------
|
||||
if [ -f "${PIC_DIR}/.installed" ] && [ "$FORCE" -eq 0 ]; then
|
||||
printf "${YELLOW}Already installed.${RESET} Run ${BOLD}'make update'${RESET} to update.\n"
|
||||
printf "To force a full reinstall, run: ${BOLD}$0 --force${RESET}\n"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 1 — Detect OS / package manager
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 1 "Detecting operating system..."
|
||||
|
||||
PKG_MANAGER=""
|
||||
OS_ID=""
|
||||
|
||||
if [ -f /etc/os-release ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. /etc/os-release
|
||||
OS_ID="${ID:-unknown}"
|
||||
fi
|
||||
|
||||
case "$OS_ID" in
|
||||
ubuntu|debian|raspbian)
|
||||
PKG_MANAGER="apt"
|
||||
;;
|
||||
fedora)
|
||||
PKG_MANAGER="dnf"
|
||||
;;
|
||||
rhel|centos|almalinux|rocky)
|
||||
PKG_MANAGER="dnf"
|
||||
;;
|
||||
alpine)
|
||||
PKG_MANAGER="apk"
|
||||
;;
|
||||
*)
|
||||
# Last-resort detection
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
PKG_MANAGER="apt"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
PKG_MANAGER="dnf"
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
PKG_MANAGER="apk"
|
||||
else
|
||||
die "Unsupported OS '${OS_ID}'. PIC requires Debian/Ubuntu, Fedora/RHEL, or Alpine Linux."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
log_ok "Detected OS: ${OS_ID} (package manager: ${PKG_MANAGER})"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 2 — Install required packages
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 2 "Installing dependencies..."
|
||||
|
||||
case "$PKG_MANAGER" in
|
||||
|
||||
apt)
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq git curl make docker.io docker-compose-plugin 2>&1 \
|
||||
| grep -v "^$" | sed 's/^/ /' || true
|
||||
|
||||
# Verify docker compose plugin installed
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
log_warn "docker-compose-plugin not available; falling back to standalone docker-compose"
|
||||
apt-get install -y -qq docker-compose 2>&1 | grep -v "^$" | sed 's/^/ /' || true
|
||||
fi
|
||||
;;
|
||||
|
||||
dnf)
|
||||
dnf install -y -q git curl make docker 2>&1 | sed 's/^/ /' || true
|
||||
|
||||
# Enable and start Docker (dnf installs but doesn't enable it)
|
||||
systemctl enable --now docker >/dev/null 2>&1 || true
|
||||
|
||||
# Docker Compose plugin comes bundled with the Docker CE package on Fedora/RHEL.
|
||||
# If not present, install via the docker-compose-plugin package (Docker CE repo).
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
log_warn "docker compose plugin not found; installing docker-compose-plugin..."
|
||||
dnf install -y -q docker-compose-plugin 2>&1 | sed 's/^/ /' || true
|
||||
fi
|
||||
;;
|
||||
|
||||
apk)
|
||||
apk add --quiet git curl make docker docker-cli-compose 2>&1 | sed 's/^/ /' || true
|
||||
|
||||
# Enable Docker on Alpine (OpenRC)
|
||||
rc-update add docker default >/dev/null 2>&1 || true
|
||||
service docker start >/dev/null 2>&1 || true
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
# Final sanity checks
|
||||
command -v git >/dev/null 2>&1 || die "git could not be installed. Aborting."
|
||||
command -v curl >/dev/null 2>&1 || die "curl could not be installed. Aborting."
|
||||
command -v make >/dev/null 2>&1 || die "make could not be installed. Aborting."
|
||||
command -v docker >/dev/null 2>&1 || die "docker could not be installed. Aborting."
|
||||
|
||||
docker compose version >/dev/null 2>&1 || \
|
||||
docker-compose version >/dev/null 2>&1 || \
|
||||
die "Neither 'docker compose' (plugin) nor 'docker-compose' is available. Aborting."
|
||||
|
||||
log_ok "All dependencies satisfied"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 3 — Create system user
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 3 "Configuring system user..."
|
||||
|
||||
if ! id "$PIC_USER" >/dev/null 2>&1; then
|
||||
case "$PKG_MANAGER" in
|
||||
apk)
|
||||
adduser -S -D -H -s /sbin/nologin "$PIC_USER"
|
||||
;;
|
||||
*)
|
||||
useradd --system --no-create-home --shell /usr/sbin/nologin "$PIC_USER"
|
||||
;;
|
||||
esac
|
||||
log_ok "Created system user: ${PIC_USER}"
|
||||
else
|
||||
log_ok "System user already exists: ${PIC_USER}"
|
||||
fi
|
||||
|
||||
# Ensure docker group exists and user is in it
|
||||
if ! getent group docker >/dev/null 2>&1; then
|
||||
groupadd docker
|
||||
log_ok "Created docker group"
|
||||
fi
|
||||
|
||||
if ! id -nG "$PIC_USER" | grep -qw docker; then
|
||||
usermod -aG docker "$PIC_USER"
|
||||
log_ok "Added ${PIC_USER} to docker group"
|
||||
else
|
||||
log_ok "${PIC_USER} is already in docker group"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 4 — Clone or update repository
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 4 "Setting up repository at ${PIC_DIR}..."
|
||||
|
||||
if [ -d "${PIC_DIR}/.git" ]; then
|
||||
log_warn "Repository already cloned — running git pull"
|
||||
git -C "$PIC_DIR" pull --ff-only 2>&1 | sed 's/^/ /'
|
||||
log_ok "Repository updated"
|
||||
elif [ -d "$PIC_DIR" ] && [ "$(ls -A "$PIC_DIR" 2>/dev/null)" ]; then
|
||||
die "${PIC_DIR} exists and is not empty and is not a git repo. Aborting to avoid data loss."
|
||||
else
|
||||
mkdir -p "$(dirname "$PIC_DIR")"
|
||||
git clone "$PIC_REPO" "$PIC_DIR" 2>&1 | sed 's/^/ /'
|
||||
log_ok "Repository cloned to ${PIC_DIR}"
|
||||
fi
|
||||
|
||||
# Ensure the pic user owns the directory
|
||||
chown -R "${PIC_USER}:${PIC_USER}" "$PIC_DIR"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 5 — Run make install
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 5 "Running 'make install'..."
|
||||
|
||||
# make install generates config, writes the systemd unit, and touches .installed.
|
||||
# We run it as the pic user (via sudo -u) so files get correct ownership, but
|
||||
# make install itself calls sudo internally where root is needed.
|
||||
cd "$PIC_DIR"
|
||||
|
||||
if ! make install 2>&1 | sed 's/^/ /'; then
|
||||
die "'make install' failed. Check the output above."
|
||||
fi
|
||||
|
||||
log_ok "'make install' complete"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 6 — Start core services
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 6 "Starting core services..."
|
||||
|
||||
cd "$PIC_DIR"
|
||||
|
||||
if ! make start-core 2>&1 | sed 's/^/ /'; then
|
||||
die "'make start-core' failed. Check the output above."
|
||||
fi
|
||||
|
||||
log_ok "Core services started"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Step 7 — Health check + print wizard URL
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 7 "Waiting for API health check (up to ${API_HEALTH_TIMEOUT}s)..."
|
||||
|
||||
ELAPSED=0
|
||||
HEALTHY=0
|
||||
while [ "$ELAPSED" -lt "$API_HEALTH_TIMEOUT" ]; do
|
||||
if curl -fsS "$API_HEALTH_URL" >/dev/null 2>&1; then
|
||||
HEALTHY=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
ELAPSED=$((ELAPSED + 2))
|
||||
printf " Waiting... (%ds)\r" "$ELAPSED"
|
||||
done
|
||||
printf "\n"
|
||||
|
||||
if [ "$HEALTHY" -ne 1 ]; then
|
||||
log_warn "API did not respond within ${API_HEALTH_TIMEOUT}s at ${API_HEALTH_URL}"
|
||||
log_warn "The stack may still be starting up. Check with: make -C ${PIC_DIR} status"
|
||||
log_warn "Or follow logs with: make -C ${PIC_DIR} logs"
|
||||
else
|
||||
log_ok "API is healthy"
|
||||
fi
|
||||
|
||||
# Detect the host's primary outbound IP address
|
||||
HOST_IP="$(ip route get 1.1.1.1 2>/dev/null | awk '/src/{print $7; exit}' || true)"
|
||||
if [ -z "$HOST_IP" ]; then
|
||||
# Fallback: first non-loopback IPv4
|
||||
HOST_IP="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
|
||||
fi
|
||||
HOST_IP="${HOST_IP:-<host-ip>}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Done
|
||||
# ---------------------------------------------------------------------------
|
||||
printf "\n${GREEN}${BOLD}============================================================${RESET}\n"
|
||||
printf "${GREEN}${BOLD} PIC installed successfully!${RESET}\n"
|
||||
printf "${GREEN}${BOLD}============================================================${RESET}\n"
|
||||
printf "\n"
|
||||
printf " Open the setup wizard at:\n"
|
||||
printf "\n"
|
||||
printf " ${BOLD}http://${HOST_IP}:${WEBUI_PORT}/setup${RESET}\n"
|
||||
printf "\n"
|
||||
printf " Useful commands:\n"
|
||||
printf " make -C ${PIC_DIR} status — check service status\n"
|
||||
printf " make -C ${PIC_DIR} logs — follow all service logs\n"
|
||||
printf " make -C ${PIC_DIR} start — start all services\n"
|
||||
printf " make -C ${PIC_DIR} stop — stop all services\n"
|
||||
printf " make -C ${PIC_DIR} update — pull latest code and restart\n"
|
||||
printf "\n"
|
||||
Reference in New Issue
Block a user