463 lines
15 KiB
Bash
463 lines
15 KiB
Bash
#!/usr/bin/env bash
|
|
# ─────────────────────────────────────────────
|
|
# Nexus One AI — Installer
|
|
# Usage:
|
|
# sudo bash install.sh → auto-detect tier, Phase 1
|
|
# sudo bash install.sh --tier=starter → Starter tier, Phase 1
|
|
# sudo bash install.sh --tier=basic → Basic tier, Phase 1
|
|
# sudo bash install.sh --tier=pro → Pro tier, Phase 1
|
|
# sudo bash install.sh --tier=max → Max tier, Phase 1
|
|
# sudo bash install.sh --phase=2 --tier=... → Phase 2 only (post-reboot)
|
|
# sudo bash install.sh --software-only → install on customer-owned hardware
|
|
# sudo bash install.sh --feasibility-only → scan hardware and exit
|
|
# sudo bash install.sh --skip-model-pull → install Ollama without preloading models
|
|
# ─────────────────────────────────────────────
|
|
set -e
|
|
|
|
# Auto-detect tier from ISO marker written by autoinstall user-data
|
|
if [ -f /opt/cezen/tier ]; then
|
|
TIER="$(cat /opt/cezen/tier | tr -d '[:space:]')"
|
|
elif [ -f /opt/aipackage/autoinstall/.tier ]; then
|
|
TIER="$(cat /opt/aipackage/autoinstall/.tier | tr -d '[:space:]')"
|
|
else
|
|
TIER="basic" # default if no marker found
|
|
fi
|
|
DISPLAY_TIER="$TIER"
|
|
PRODUCT_CATEGORY="server"
|
|
PHASE="1"
|
|
SKIP_ROLES=""
|
|
SOFTWARE_ONLY=false
|
|
FEASIBILITY_ONLY=false
|
|
SKIP_MODEL_PULL=false
|
|
PROFILE="auto"
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ANSIBLE_DIR="$SCRIPT_DIR/ansible"
|
|
FEASIBILITY_SCRIPT="$SCRIPT_DIR/scripts/cezen-feasibility.sh"
|
|
LICENSE_CHECK_SCRIPT="$SCRIPT_DIR/scripts/cezen-license-check.py"
|
|
FEASIBILITY_JSON="/opt/cezen/feasibility.json"
|
|
LICENSE_JSON="/opt/cezen/license.json"
|
|
OVERRIDE_JSON="/opt/cezen/license.override.json"
|
|
INSTALL_RECORD_JSON="/opt/cezen/install-record.json"
|
|
PUBLIC_KEY_PATH="${CEZEN_LICENSE_PUBLIC_KEY:-$SCRIPT_DIR/autoinstall/keys/cezen-license-public.pem}"
|
|
|
|
# Load saved config (written by web setup UI before phase 1)
|
|
[ -f /opt/cezen/install.conf ] && source /opt/cezen/install.conf
|
|
|
|
for arg in "$@"; do
|
|
case $arg in
|
|
--tier=*) TIER="${arg#*=}" ;;
|
|
--phase=*) PHASE="${arg#*=}" ;;
|
|
--skip=*) SKIP_ROLES="${arg#*=}" ;;
|
|
--profile=*) PROFILE="${arg#*=}" ;;
|
|
--software-only) SOFTWARE_ONLY=true ;;
|
|
--feasibility-only) FEASIBILITY_ONLY=true ;;
|
|
--skip-model-pull) SKIP_MODEL_PULL=true ;;
|
|
esac
|
|
done
|
|
|
|
normalize_tier() {
|
|
case "$TIER" in
|
|
workstation)
|
|
DISPLAY_TIER="workstation"
|
|
PRODUCT_CATEGORY="workstation"
|
|
TIER="starter"
|
|
;;
|
|
entry|basic) TIER="basic" ;;
|
|
mid|pro) TIER="pro" ;;
|
|
advanced|max) TIER="max" ;;
|
|
starter) TIER="starter" ;;
|
|
esac
|
|
if [ "$PRODUCT_CATEGORY" = "server" ]; then
|
|
DISPLAY_TIER="$TIER"
|
|
fi
|
|
}
|
|
|
|
normalize_tier
|
|
|
|
# ── Preflight ──────────────────────────────────
|
|
check_root() {
|
|
if [ "$EUID" -ne 0 ]; then
|
|
echo "ERROR: Run as root: sudo bash install.sh"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
check_os() {
|
|
if [ -f /etc/os-release ]; then
|
|
. /etc/os-release
|
|
if [[ "$ID" != "ubuntu" ]]; then
|
|
echo "ERROR: Ubuntu 22.04 required. Detected: $PRETTY_NAME"
|
|
exit 1
|
|
fi
|
|
echo "✓ OS: $PRETTY_NAME"
|
|
fi
|
|
}
|
|
|
|
install_ansible() {
|
|
if ! command -v ansible-playbook &>/dev/null; then
|
|
echo "→ Installing Ansible..."
|
|
apt-get update -qq
|
|
apt-get install -y -qq ansible python3-pip
|
|
fi
|
|
echo "✓ Ansible ready"
|
|
}
|
|
|
|
append_skip_role() {
|
|
local role="$1"
|
|
if [ -z "$SKIP_ROLES" ]; then
|
|
SKIP_ROLES="$role"
|
|
elif [[ ",$SKIP_ROLES," != *",$role,"* ]]; then
|
|
SKIP_ROLES="$SKIP_ROLES,$role"
|
|
fi
|
|
}
|
|
|
|
run_feasibility() {
|
|
if [ -f "$FEASIBILITY_SCRIPT" ]; then
|
|
bash "$FEASIBILITY_SCRIPT" "$FEASIBILITY_JSON"
|
|
else
|
|
echo "WARNING: Feasibility checker not found: $FEASIBILITY_SCRIPT"
|
|
fi
|
|
}
|
|
|
|
json_field() {
|
|
local expr="$1"
|
|
python3 - "$FEASIBILITY_JSON" "$expr" <<'PY'
|
|
import json, sys
|
|
try:
|
|
d=json.load(open(sys.argv[1]))
|
|
cur=d
|
|
for part in sys.argv[2].split("."):
|
|
cur=cur[part]
|
|
print(cur)
|
|
except Exception:
|
|
print("")
|
|
PY
|
|
}
|
|
|
|
license_eval_field() {
|
|
local expr="$1"
|
|
python3 - "$LICENSE_EVAL_JSON" "$expr" <<'PY'
|
|
import json, sys
|
|
try:
|
|
d=json.load(open(sys.argv[1]))
|
|
cur=d
|
|
for part in sys.argv[2].split("."):
|
|
cur=cur[part]
|
|
print(cur)
|
|
except Exception:
|
|
print("")
|
|
PY
|
|
}
|
|
|
|
run_license_evaluation() {
|
|
LICENSE_EVAL_JSON="/tmp/cezen-license-eval.json"
|
|
if [ -f "$LICENSE_CHECK_SCRIPT" ]; then
|
|
python3 "$LICENSE_CHECK_SCRIPT" \
|
|
--license "$LICENSE_JSON" \
|
|
--override "$OVERRIDE_JSON" \
|
|
--feasibility "$FEASIBILITY_JSON" \
|
|
--public-key "$PUBLIC_KEY_PATH" > "$LICENSE_EVAL_JSON"
|
|
else
|
|
echo "ERROR: License checker not found: $LICENSE_CHECK_SCRIPT"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
enforce_tier_constraints() {
|
|
run_license_evaluation
|
|
local license_status allowed_tier hardware_tier selected_state
|
|
license_status="$(license_eval_field license.status)"
|
|
allowed_tier="$(license_eval_field license.allowed_tier)"
|
|
hardware_tier="$(license_eval_field hardware.recommended_tier)"
|
|
selected_state="$(python3 - "$LICENSE_EVAL_JSON" "$TIER" <<'PY'
|
|
import json, sys
|
|
d=json.load(open(sys.argv[1]))
|
|
tier=sys.argv[2]
|
|
for opt in d.get("tier_options", []):
|
|
if opt.get("tier") == tier:
|
|
print(opt.get("state", "disabled_by_license"))
|
|
break
|
|
else:
|
|
print("disabled_by_license")
|
|
PY
|
|
)"
|
|
echo "→ License status: ${license_status:-missing} | Allowed tier: ${allowed_tier:-basic} | Hardware tier: ${hardware_tier:-starter}"
|
|
case "$selected_state" in
|
|
enabled|override_required)
|
|
;;
|
|
disabled_by_license)
|
|
echo "ERROR: Selected tier '$TIER' exceeds the current license allowance (${allowed_tier:-basic})."
|
|
exit 1
|
|
;;
|
|
disabled_by_hardware)
|
|
echo "ERROR: Selected tier '$TIER' exceeds hardware feasibility (${hardware_tier:-starter})."
|
|
exit 1
|
|
;;
|
|
*)
|
|
echo "ERROR: Selected tier '$TIER' is not allowed (state: $selected_state)."
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
write_install_record() {
|
|
python3 - "$INSTALL_RECORD_JSON" "$TIER" "$PROFILE" "$SKIP_ROLES" "$GPU_AVAILABLE" "$FEASIBILITY_JSON" "$LICENSE_EVAL_JSON" "$DISPLAY_TIER" "$PRODUCT_CATEGORY" <<'PY'
|
|
import json, sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
out = Path(sys.argv[1])
|
|
tier = sys.argv[2]
|
|
profile = sys.argv[3]
|
|
skip_roles = [r for r in sys.argv[4].split(",") if r]
|
|
gpu_available = sys.argv[5].lower() == "true"
|
|
feasibility = {}
|
|
license_eval = {}
|
|
for src, dest in ((sys.argv[6], "feasibility"), (sys.argv[7], "license_eval")):
|
|
try:
|
|
with open(src) as fh:
|
|
data = json.load(fh)
|
|
if dest == "feasibility":
|
|
feasibility = data
|
|
else:
|
|
license_eval = data
|
|
except Exception:
|
|
pass
|
|
components = {
|
|
"ollama": "ollama" not in skip_roles,
|
|
"jupyterlab": "jupyterlab" not in skip_roles,
|
|
"chromadb": "chromadb" not in skip_roles,
|
|
"vllm": "vllm" not in skip_roles,
|
|
"mlflow": "mlflow" not in skip_roles,
|
|
"minio": "minio" not in skip_roles,
|
|
"monitoring": "monitoring" not in skip_roles,
|
|
"k3s": "k3s" not in skip_roles,
|
|
}
|
|
payload = {
|
|
"schema": "cezen.install_record.v1",
|
|
"generated_at": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"),
|
|
"selected_tier": sys.argv[8],
|
|
"provisioned_tier": tier,
|
|
"category": sys.argv[9],
|
|
"provisioned_profile": profile,
|
|
"skip_roles": skip_roles,
|
|
"components": components,
|
|
"gpu_available": gpu_available,
|
|
"hardware_recommended_tier": ((feasibility.get("recommendation") or {}).get("recommended_tier") or "starter"),
|
|
"hardware_recommended_profile": ((feasibility.get("recommendation") or {}).get("recommended_profile") or "core"),
|
|
"licensed_tier": (((license_eval.get("license") or {}).get("allowed_tier")) or "basic"),
|
|
"license_status": ((license_eval.get("license") or {}).get("status") or "missing"),
|
|
"install_type": ((license_eval.get("license") or {}).get("install_type") or "field-staging"),
|
|
"override_active": bool(((license_eval.get("override") or {}).get("allow_hardware_mismatch"))),
|
|
}
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
out.write_text(json.dumps(payload, indent=2))
|
|
PY
|
|
}
|
|
|
|
apply_profile_from_feasibility() {
|
|
[ -f "$FEASIBILITY_JSON" ] || return 0
|
|
local detected_profile
|
|
detected_profile="$(json_field recommendation.recommended_profile)"
|
|
if [ "$PROFILE" = "auto" ] && [ -n "$detected_profile" ]; then
|
|
PROFILE="$detected_profile"
|
|
fi
|
|
|
|
case "$PROFILE" in
|
|
core)
|
|
append_skip_role docker
|
|
append_skip_role k3s
|
|
append_skip_role ollama
|
|
append_skip_role vllm
|
|
append_skip_role jupyterlab
|
|
append_skip_role chromadb
|
|
append_skip_role mlflow
|
|
append_skip_role minio
|
|
append_skip_role monitoring
|
|
SKIP_MODEL_PULL=true
|
|
;;
|
|
cpu-ai)
|
|
append_skip_role k3s
|
|
append_skip_role vllm
|
|
append_skip_role mlflow
|
|
append_skip_role minio
|
|
SKIP_MODEL_PULL=true
|
|
;;
|
|
gpu-lite|gpu-starter)
|
|
append_skip_role k3s
|
|
append_skip_role mlflow
|
|
append_skip_role minio
|
|
SKIP_MODEL_PULL=true
|
|
;;
|
|
gpu-standard)
|
|
append_skip_role mlflow
|
|
append_skip_role minio
|
|
;;
|
|
gpu-pro|gpu-max)
|
|
;;
|
|
*)
|
|
echo "WARNING: Unknown profile '$PROFILE'; using explicit skip list only."
|
|
;;
|
|
esac
|
|
}
|
|
|
|
warn_tier_vs_feasibility() {
|
|
[ -f "$FEASIBILITY_JSON" ] || return 0
|
|
local recommended_tier recommended_profile
|
|
recommended_tier="$(json_field recommendation.recommended_tier)"
|
|
recommended_profile="$(json_field recommendation.recommended_profile)"
|
|
[ -n "$recommended_tier" ] || return 0
|
|
if [ "$recommended_tier" != "$TIER" ]; then
|
|
echo "⚠ Feasibility recommends tier '$recommended_tier' / profile '${recommended_profile:-unknown}' for this hardware."
|
|
echo " Selected tier remains '$TIER'; unsupported services may be skipped by the feasibility profile."
|
|
else
|
|
echo "✓ Feasibility matches selected tier '$TIER' / profile '${recommended_profile:-unknown}'."
|
|
fi
|
|
}
|
|
|
|
has_nvidia_pci_gpu() {
|
|
for vendor_file in /sys/bus/pci/devices/*/vendor; do
|
|
[ -f "$vendor_file" ] || continue
|
|
if [ "$(tr '[:upper:]' '[:lower:]' < "$vendor_file")" = "0x10de" ]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
has_working_nvidia_driver() {
|
|
command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null
|
|
}
|
|
|
|
# ── Phase 1: NVIDIA drivers only ──────────────
|
|
run_phase1() {
|
|
echo ""
|
|
echo "╔══════════════════════════════════════════╗"
|
|
echo "║ Nexus One AI — Phase 1: NVIDIA ║"
|
|
echo "╚══════════════════════════════════════════╝"
|
|
|
|
if ! has_nvidia_pci_gpu; then
|
|
echo "No NVIDIA GPU found. Continuing with CPU/non-GPU installation path."
|
|
PHASE="2"
|
|
run_phase2
|
|
return
|
|
fi
|
|
|
|
ANSIBLE_STDOUT_CALLBACK=yaml \
|
|
ansible-playbook -i localhost, -c local "$ANSIBLE_DIR/phase1_nvidia.yml" \
|
|
-e "tier=$TIER"
|
|
|
|
# Register phase 2 as a one-shot systemd service so it runs after reboot
|
|
cat > /etc/systemd/system/cezen-phase2.service << EOF
|
|
[Unit]
|
|
Description=Nexus One AI Phase 2 Installer
|
|
After=network-online.target nvidia-persistenced.service
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
ExecStart=/bin/bash -lc 'set -o pipefail; /bin/bash ${SCRIPT_DIR}/install.sh --phase=2 --tier=${TIER} 2>&1 | tee -a /var/log/cezen-install.log'
|
|
RemainAfterExit=yes
|
|
StandardOutput=journal
|
|
StandardError=journal
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable cezen-phase2.service
|
|
|
|
echo ""
|
|
echo "✓ Phase 2 registered — will run automatically after reboot"
|
|
echo "→ Rebooting in 10 seconds..."
|
|
sleep 10
|
|
reboot
|
|
}
|
|
|
|
# ── Phase 2: Full stack ────────────────────────
|
|
run_phase2() {
|
|
echo ""
|
|
echo "╔══════════════════════════════════════════╗"
|
|
echo "║ Nexus One AI — Phase 2: Stack ║"
|
|
echo "╚══════════════════════════════════════════╝"
|
|
|
|
apply_profile_from_feasibility
|
|
warn_tier_vs_feasibility
|
|
|
|
GPU_AVAILABLE=false
|
|
if ! has_working_nvidia_driver; then
|
|
echo "No working NVIDIA GPU/driver found. Continuing with CPU/non-GPU installation path."
|
|
echo "GPU-only features such as NVIDIA Docker runtime, DCGM metrics, and vLLM serving will be skipped or left inactive."
|
|
else
|
|
GPU_AVAILABLE=true
|
|
echo "✓ NVIDIA driver: $(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -1)"
|
|
fi
|
|
|
|
# Build skip_roles extra var (comma-separated list, empty string = skip nothing)
|
|
EXTRA_VARS="tier=$TIER skip_roles=\"$SKIP_ROLES\" gpu_available=$GPU_AVAILABLE skip_model_pull=$SKIP_MODEL_PULL"
|
|
echo "→ Tier: $TIER | Skip: ${SKIP_ROLES:-none}"
|
|
echo "→ GPU available: $GPU_AVAILABLE"
|
|
echo "→ Skip model pull: $SKIP_MODEL_PULL"
|
|
write_install_record
|
|
|
|
# Select Ansible playbook by tier
|
|
case "$TIER" in
|
|
starter) PLAYBOOK="$ANSIBLE_DIR/starter.yml" ;;
|
|
basic|entry) PLAYBOOK="$ANSIBLE_DIR/entry.yml" ;;
|
|
pro) PLAYBOOK="$ANSIBLE_DIR/pro.yml" ;;
|
|
max) PLAYBOOK="$ANSIBLE_DIR/max.yml" ;;
|
|
*)
|
|
echo "ERROR: Unknown tier '$TIER'. Valid: starter | basic | pro | max"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
echo "→ Playbook: $PLAYBOOK"
|
|
ANSIBLE_STDOUT_CALLBACK=yaml \
|
|
ansible-playbook -i localhost, -c local "$PLAYBOOK" \
|
|
-e "$EXTRA_VARS"
|
|
|
|
# Disable one-shot service so it doesn't run again on next reboot
|
|
systemctl disable cezen-phase2.service 2>/dev/null || true
|
|
|
|
echo ""
|
|
echo "╔══════════════════════════════════════════╗"
|
|
echo "║ Nexus One AI installation complete! ║"
|
|
echo "║ Tier: $(printf '%-33s' "$DISPLAY_TIER")║"
|
|
echo "║ ║"
|
|
echo "║ Portal → http://localhost ║"
|
|
echo "║ Ollama API → http://localhost:11434 ║"
|
|
echo "║ vLLM API → http://localhost:8000 ║"
|
|
echo "║ Grafana → http://localhost:3000 ║"
|
|
echo "╚══════════════════════════════════════════╝"
|
|
}
|
|
|
|
# ── Main ───────────────────────────────────────
|
|
check_os
|
|
|
|
if [ "$FEASIBILITY_ONLY" = true ]; then
|
|
run_feasibility
|
|
exit 0
|
|
fi
|
|
|
|
check_root
|
|
run_feasibility
|
|
|
|
if [ "$SOFTWARE_ONLY" = true ]; then
|
|
PHASE="2"
|
|
fi
|
|
|
|
enforce_tier_constraints
|
|
|
|
install_ansible
|
|
|
|
if [ "$PHASE" = "1" ]; then
|
|
run_phase1
|
|
elif [ "$PHASE" = "2" ]; then
|
|
run_phase2
|
|
else
|
|
echo "ERROR: Unknown phase '$PHASE'. Use --phase=1 or --phase=2"
|
|
exit 1
|
|
fi
|