#!/usr/bin/env bash # ============================================================================== # Netwing 1.0 Enterprise Commercial Turnkey Installer (Image-Only Appliance) # Principle: Zero source code on customer host; version-pinned image deployment. # ============================================================================== set -euo pipefail OFFLINE_BUNDLE="" TARGET_VERSION="${NETWING_VERSION:-2.0.3}" DEMO_FLAG="false" while [[ $# -gt 0 ]]; do case "$1" in --offline) if [[ -n "${2:-}" && ! "${2:-}" =~ ^-- ]]; then OFFLINE_BUNDLE="$2" shift 2 else shift 1 fi ;; --version) TARGET_VERSION="${2:-2.0.3}" shift 2 ;; --demo) DEMO_FLAG="true" shift 1 ;; *) shift ;; esac done get_env_var() { local key="$1" local default_val="${2:-}" if [ -f .env ] && grep -q "^${key}=" .env; then grep "^${key}=" .env | tail -n 1 | cut -d'=' -f2- | tr -d '\r' else echo "${default_val}" fi } echo "==================================================================" echo " Starting Netwing 1.0 Enterprise Turnkey Production Installer" echo " Mode: Image-Only Appliance | Target Version: v${TARGET_VERSION}" echo "==================================================================" # 1. Supported Platform & Architecture Matrix Verification HOST_ARCH="$(uname -m)" if [ "${HOST_ARCH}" != "x86_64" ] && [ "${HOST_ARCH}" != "aarch64" ]; then echo "[-] Error: Unsupported CPU architecture '${HOST_ARCH}'. Netwing requires x86_64 or aarch64." >&2 exit 1 fi if [ -f /etc/os-release ]; then . /etc/os-release case "${ID:-}" in ubuntu|debian|rhel|rocky|almalinux|centos) echo "[+] Certified platform detected: ${PRETTY_NAME:-$ID} (${HOST_ARCH})" ;; *) echo "[!] Notice: Host platform '${PRETTY_NAME:-$ID}' is not an officially certified Netwing target." echo "[!] Certified platforms: Ubuntu 22.04/24.04 LTS, Debian 12, RHEL / Rocky Linux 9." ;; esac fi # 2. Check Docker & Docker Compose Prerequisites install_docker_engine() { echo "[+] Installing Docker Engine via official repository..." local success=0 if curl -fsSL https://get.docker.com | sh; then success=1 fi # Fallback if get.docker.com failed (e.g. Ubuntu 20.04 missing docker-model-plugin) if ! command -v docker &>/dev/null; then echo "[+] Falling back to direct distribution package repository..." if [ -f /etc/os-release ]; then . /etc/os-release fi case "${ID:-}" in ubuntu|debian) export DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null 2>&1 || true apt-get install -y -qq ca-certificates curl gnupg lsb-release >/dev/null 2>&1 || true mkdir -p /etc/apt/keyrings curl -fsSL "https://download.docker.com/linux/${ID}/gpg" 2>/dev/null | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg 2>/dev/null || \ curl -fsSL "https://download.docker.com/linux/ubuntu/gpg" 2>/dev/null | gpg --dearmor --yes -o /etc/apt/keyrings/docker.gpg 2>/dev/null || true local codename="${VERSION_CODENAME:-}" if [ -z "${codename}" ]; then codename="$(lsb_release -cs 2>/dev/null || echo "focal")" fi echo "deb [arch=$(dpkg --print-architecture 2>/dev/null || echo "amd64") signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${ID:-ubuntu} ${codename} stable" > /etc/apt/sources.list.d/docker.list apt-get update -qq >/dev/null 2>&1 || true apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin docker-buildx-plugin || \ apt-get install -y docker.io docker-compose || true ;; rhel|rocky|almalinux|centos|fedora) yum install -y -q yum-utils >/dev/null 2>&1 || dnf install -y -q yum-utils >/dev/null 2>&1 || true yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo >/dev/null 2>&1 || true yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin || \ dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin || true ;; esac fi systemctl enable --now docker 2>/dev/null || service docker start 2>/dev/null || true } if ! command -v docker &>/dev/null; then echo "[-] Docker is not installed on this host." if [ -z "${OFFLINE_BUNDLE}" ] && [ -t 0 ]; then read -rp "Would you like to automatically install Docker Engine via official packages? [y/N]: " install_docker_choice if [[ "${install_docker_choice:-}" =~ ^[Yy]$ ]]; then install_docker_engine else echo "[-] Error: Please install Docker Engine 24+ and Docker Compose v2, then re-run installer." >&2 exit 1 fi else echo "[-] Error: Docker Engine is required. For offline hosts, Docker must be pre-provisioned." >&2 exit 1 fi fi if docker compose version &>/dev/null; then COMPOSE="docker compose" elif command -v docker-compose &>/dev/null; then COMPOSE="docker-compose" else echo "[+] Installing Docker Compose plugin..." if command -v apt-get &>/dev/null; then apt-get update -qq >/dev/null 2>&1 || true apt-get install -y docker-compose-plugin 2>/dev/null || apt-get install -y docker-compose 2>/dev/null || true elif command -v yum &>/dev/null; then yum install -y docker-compose-plugin 2>/dev/null || true fi if docker compose version &>/dev/null; then COMPOSE="docker compose" elif command -v docker-compose &>/dev/null; then COMPOSE="docker-compose" else echo "[-] Error: Docker Compose is required. Please install Docker Engine 24+ and Docker Compose." >&2 exit 1 fi fi if ! docker info &>/dev/null; then echo "[-] Error: Docker daemon is not running or accessible by current user." >&2 exit 1 fi BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ ! -f "${BASE_DIR}/docker-compose.prod.yml" ] && [ -f "${BASE_DIR}/../../codebase/docker-compose.prod.yml" ]; then BASE_DIR="$(cd "${BASE_DIR}/../../codebase" && pwd)" fi cd "$BASE_DIR" RELEASE_SERVER="${NETWING_RELEASE_SERVER:-https://get.netwing.dev}" # Auto-download companion files if running standalone from curl if [ ! -f "${BASE_DIR}/docker-compose.prod.yml" ]; then echo "[+] Fetching docker-compose.prod.yml from ${RELEASE_SERVER}..." curl -fsSL "${RELEASE_SERVER}/releases/${TARGET_VERSION}/docker-compose.prod.yml" -o "${BASE_DIR}/docker-compose.prod.yml" || \ curl -fsSL "${RELEASE_SERVER}/releases/docker-compose.prod.yml" -o "${BASE_DIR}/docker-compose.prod.yml" || true fi if [ ! -f "${BASE_DIR}/netwingctl.sh" ]; then echo "[+] Fetching netwingctl.sh from ${RELEASE_SERVER}..." curl -fsSL "${RELEASE_SERVER}/releases/${TARGET_VERSION}/netwingctl.sh" -o "${BASE_DIR}/netwingctl.sh" || \ curl -fsSL "${RELEASE_SERVER}/releases/netwingctl.sh" -o "${BASE_DIR}/netwingctl.sh" || true chmod +x "${BASE_DIR}/netwingctl.sh" 2>/dev/null || true fi if [ ! -f "${BASE_DIR}/upgrade.sh" ]; then echo "[+] Fetching upgrade.sh from ${RELEASE_SERVER}..." curl -fsSL "${RELEASE_SERVER}/releases/${TARGET_VERSION}/upgrade.sh" -o "${BASE_DIR}/upgrade.sh" || \ curl -fsSL "${RELEASE_SERVER}/releases/upgrade.sh" -o "${BASE_DIR}/upgrade.sh" || true chmod +x "${BASE_DIR}/upgrade.sh" 2>/dev/null || true fi # Record canonical install directory and install 'netwing' CLI helper mkdir -p /etc/netwing >/dev/null 2>&1 || sudo mkdir -p /etc/netwing >/dev/null 2>&1 || true if [ -d /etc/netwing ]; then echo "INSTALL_DIR=${BASE_DIR}" | tee /etc/netwing/install.conf >/dev/null 2>&1 || \ echo "INSTALL_DIR=${BASE_DIR}" | sudo tee /etc/netwing/install.conf >/dev/null 2>&1 || true fi if [ -f "${BASE_DIR}/netwingctl.sh" ]; then chmod +x "${BASE_DIR}/netwingctl.sh" cp -f "${BASE_DIR}/netwingctl.sh" "${BASE_DIR}/netwing" 2>/dev/null || true cp -f "${BASE_DIR}/netwingctl.sh" /usr/local/bin/netwing >/dev/null 2>&1 || \ sudo cp -f "${BASE_DIR}/netwingctl.sh" /usr/local/bin/netwing >/dev/null 2>&1 || true chmod +x /usr/local/bin/netwing >/dev/null 2>&1 || sudo chmod +x /usr/local/bin/netwing >/dev/null 2>&1 || true fi # Detect Host LAN IPs and Hostname PRIMARY_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1") ALL_HOST_IPS=$(hostname -I 2>/dev/null | tr ' ' ',' | sed 's/,$//' || echo "127.0.0.1") HOST_FQDN=$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "netwing.local") # 2. Provision Directory Hierarchy echo "[+] Step 1/5: Provisioning secure configuration directories..." mkdir -p data backups chmod 700 data backups 2>/dev/null || true # 3. Environment & Cryptographic Secrets Generation if [ ! -f .env ]; then echo "[+] Step 2/5: Generating cryptographic production secrets (.env)..." if command -v python3 &>/dev/null; then MASTER_KEY=$(python3 -c "import base64, os; print(base64.urlsafe_b64encode(os.urandom(32)).decode())") DJANGO_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))") DB_PASS=$(python3 -c "import secrets; print(secrets.token_urlsafe(24))") else MASTER_KEY=$(openssl rand -base64 32) DJANGO_KEY=$(openssl rand -base64 48) DB_PASS=$(openssl rand -base64 24) fi cat < .env DEBUG=False DEMO_MODE=${DEMO_FLAG} DJANGO_SECRET_KEY=${DJANGO_KEY} NETWING_MASTER_KEY=${MASTER_KEY} CREDENTIAL_ENCRYPTION_KEY=${MASTER_KEY} NETWING_IMAGE=ghcr.io/elhachemi-ha/netwing NETWING_VERSION=${TARGET_VERSION} ALLOWED_HOSTS=localhost,127.0.0.1,backend,netwing_backend,${HOST_FQDN},${ALL_HOST_IPS} CSRF_TRUSTED_ORIGINS=http://${PRIMARY_IP}:8000,http://localhost:8000,http://127.0.0.1:8000,http://${HOST_FQDN}:8000 POSTGRES_DB=netwing POSTGRES_USER=postgres POSTGRES_PASSWORD=${DB_PASS} DATABASE_NAME=netwing DATABASE_USER=postgres DATABASE_PASSWORD=${DB_PASS} REDIS_URL=redis://redis:6379/0 EOF chmod 600 .env echo "[+] Generated secure .env file with AES-256 Fernet master encryption key." else echo "[+] Step 2/5: Existing .env configuration detected; preserving customer secrets." if [ "${DEMO_FLAG}" = "true" ] && ! grep -q "^DEMO_MODE=" .env; then echo "DEMO_MODE=true" >> .env fi chmod 600 .env 2>/dev/null || true fi IMAGE_REPO="$(get_env_var "NETWING_IMAGE" "netwing/appliance")" VERSION="$(get_env_var "NETWING_VERSION" "${TARGET_VERSION}")" FULL_IMAGE="${IMAGE_REPO}:${VERSION}" # Auto-reconcile existing or legacy image tags if docker image inspect "ghcr.io/elhachemi-ha/netwing:${VERSION}" &>/dev/null && ! docker image inspect "${FULL_IMAGE}" &>/dev/null; then docker tag "ghcr.io/elhachemi-ha/netwing:${VERSION}" "${FULL_IMAGE}" 2>/dev/null || true fi # 4. Acquire & Verify Production Appliance Image (Offline Bundle or Direct Release Download) echo "[+] Step 3/5: Preparing production appliance image (v${VERSION})..." if [ -n "${OFFLINE_BUNDLE}" ]; then if [ ! -f "${OFFLINE_BUNDLE}" ]; then echo "[-] Error: Offline bundle not found at ${OFFLINE_BUNDLE}" >&2 exit 1 fi if [ -f "${OFFLINE_BUNDLE}.sha256" ]; then echo "[+] Verifying SHA-256 checksum for ${OFFLINE_BUNDLE}..." (cd "$(dirname "${OFFLINE_BUNDLE}")" && sha256sum -c "$(basename "${OFFLINE_BUNDLE}.sha256")") fi gzip -t "${OFFLINE_BUNDLE}" docker load -i "${OFFLINE_BUNDLE}" if docker image inspect "ghcr.io/elhachemi-ha/netwing:${VERSION}" &>/dev/null && ! docker image inspect "${FULL_IMAGE}" &>/dev/null; then docker tag "ghcr.io/elhachemi-ha/netwing:${VERSION}" "${FULL_IMAGE}" 2>/dev/null || true fi elif ! docker image inspect "${FULL_IMAGE}" &>/dev/null; then echo "[+] Downloading Netwing release image bundle from ${RELEASE_SERVER} (resumable)..." TMP_BUNDLE="${BASE_DIR}/netwing-${TARGET_VERSION}.tar.gz" CURL_OPTS="-C - --retry 10 --retry-delay 3 --retry-connrefused --connect-timeout 15 -fL --progress-bar" if curl ${CURL_OPTS} "${RELEASE_SERVER}/releases/${TARGET_VERSION}/netwing-${TARGET_VERSION}.tar.gz" -o "${TMP_BUNDLE}" || \ curl ${CURL_OPTS} "${RELEASE_SERVER}/releases/netwing-${TARGET_VERSION}.tar.gz" -o "${TMP_BUNDLE}"; then echo "[+] Successfully downloaded release bundle. Loading container images into Docker..." docker load -i "${TMP_BUNDLE}" if docker image inspect "ghcr.io/elhachemi-ha/netwing:${VERSION}" &>/dev/null && ! docker image inspect "${FULL_IMAGE}" &>/dev/null; then docker tag "ghcr.io/elhachemi-ha/netwing:${VERSION}" "${FULL_IMAGE}" 2>/dev/null || true fi rm -f "${TMP_BUNDLE}" else echo "[-] Error: Could not download release image bundle from ${RELEASE_SERVER}." >&2 echo "[-] For air-gapped installation, download netwing-${TARGET_VERSION}.tar.gz and run:" >&2 echo " sudo ./install.sh --offline netwing-${TARGET_VERSION}.tar.gz" >&2 exit 1 fi fi docker image inspect "${FULL_IMAGE}" >/dev/null # 5. Start PostgreSQL & Redis and Initialize Database Schema Quietly echo "[+] Step 4/5: Initializing database schema and core services..." $COMPOSE -f docker-compose.prod.yml up -d db redis >/dev/null 2>&1 sleep 4 # Ensure persistent volumes (/app/data and /app/backups) are writable by non-root UID 10001:10001 $COMPOSE -f docker-compose.prod.yml run --rm --user "0:0" --entrypoint sh backend -c \ "mkdir -p /app/data/snapshots /app/data/upgrades /app/backups && chown -R 10001:10001 /app/data /app/backups" >/dev/null 2>&1 MIGRATE_LOG="${BASE_DIR}/install_migrate.log" if ! $COMPOSE -f docker-compose.prod.yml run --rm backend python manage.py migrate --no-input > "${MIGRATE_LOG}" 2>&1; then echo "[-] Database schema migration encountered an error:" >&2 cat "${MIGRATE_LOG}" >&2 rm -f "${MIGRATE_LOG}" exit 1 fi rm -f "${MIGRATE_LOG}" # Idempotently bootstrap initial Super Administrator & seed RBAC permissions BOOTSTRAP_OUT=$($COMPOSE -f docker-compose.prod.yml run --rm backend python -c " import django, os, secrets os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'netwing.settings') django.setup() from accounts.models import User from netwing.upgrade.engine import UpgradeSafetyEngine from netwing.licensing import HardwareIdentityCollector engine = UpgradeSafetyEngine() engine.reconcile_state() code, _ = HardwareIdentityCollector.compute_canonical_fingerprint() if not User.objects.filter(is_superuser=True).exists(): init_pass = secrets.token_urlsafe(12) User.objects.create_superuser(username='admin', email='admin@netwing.local', password=init_pass) print(f'CREATED_ADMIN:{init_pass}') else: print('EXISTING_ADMIN') print(f'APPLIANCE_ID:{code}') " 2>/dev/null || true) CREATED_PASS=$(echo "${BOOTSTRAP_OUT}" | grep "^CREATED_ADMIN:" | cut -d':' -f2- | tr -d '\r' || true) APPLIANCE_ID=$(echo "${BOOTSTRAP_OUT}" | grep "^APPLIANCE_ID:" | cut -d':' -f2- | tr -d '\r' || echo "N/A") if [ -n "${CREATED_PASS}" ]; then echo "ADMIN_USERNAME=admin" > .initial_admin_credentials echo "ADMIN_PASSWORD=${CREATED_PASS}" >> .initial_admin_credentials chmod 600 .initial_admin_credentials fi # 6. Launch Complete Container Fleet echo "[+] Step 5/5: Launching complete production container fleet..." $COMPOSE -f docker-compose.prod.yml up -d --remove-orphans if [ "${DEMO_FLAG}" = "true" ] || [ "$(get_env_var "DEMO_MODE" "false")" = "true" ]; then echo "[+] Seeding Netwing 1.0 Interactive Public Demo Environment..." sleep 3 $COMPOSE -f docker-compose.prod.yml exec -T backend python scripts/seed_demo_environment.py fi echo "==================================================================" echo " Netwing 1.0 Enterprise Installation Complete (v${VERSION})!" echo "------------------------------------------------------------------" echo " Web UI URL : http://${PRIMARY_IP}:8000/" echo " Appliance ID : ${APPLIANCE_ID}" if [ -n "${CREATED_PASS}" ]; then echo " Initial Login : admin / ${CREATED_PASS}" echo " (Saved in ${BASE_DIR}/.initial_admin_credentials)" fi echo " CLI Helper : netwing status | netwing rollback | netwing logs" echo "=================================================================="