Add dedicated Nexus One AI Workstation ISO build path

- New build-iso-workstation.sh / autoinstall/user-data-workstation: writes
  the workstation tier marker (/opt/cezen/tier), reuses the Server S
  (starter) hardware profile and Ansible playbook under the hood
- firstboot-setup.sh: additive WORKSTATION_MODE branch (gated on
  CEZEN_TIER=workstation) skips the Server tier-selection menu entirely —
  Workstation is never offered as a Server S/M/L/Max choice. Non-workstation
  installs are unaffected. Also relabels the Server tier menu to
  Server S/M/L/Max, matching the earlier portal rename
- install.sh and the Ansible playbooks are unchanged; only the pre/post
  marker files differ between a Server S and Workstation install
- README.md + MIGRATION_NOTES.md updated with the new build path and what
  is still manual (CEZEN_TIER isn't yet templated into the deployed
  cezen-api systemd unit — tracked as follow-up)
This commit is contained in:
Jino Jose committed 2026-07-08 15:10:34 +05:30
1 parent 12d9ea84f6
commit 10b66569b6
4 files changed
+457 -31

No files matched your search

+67 -22
View File
@@ -23,6 +23,24 @@ detect_iface() {
IFACE="$(detect_iface)"
IFACE="${IFACE:-$(ip -o link show | awk -F': ' '$2 !~ /lo|docker|br-|veth/ {print $2; exit}')}"
# ── Nexus One AI Workstation detection ─────────────────────
# Workstation is a standalone product category, not a rung on the Server
# S/M/L/Max ladder (see cezen_license.WORKSTATION_TIER). It must never be
# offered as a choice in the Server tier-selection menu below (Step 3).
# When this ISO was built as a Workstation image (CEZEN_TIER=workstation,
# or the /opt/cezen/tier marker says "workstation"), Step 3 is skipped
# entirely and the existing "starter" Ansible profile is reused for
# provisioning — same compact single-GPU stack, different commercial
# category. TIER="starter" here only selects which Ansible playbook runs;
# it does not change the "workstation" marker written to /opt/cezen/tier
# or the install-record tier fields below.
WORKSTATION_MODE=false
if [ "${CEZEN_TIER:-}" = "workstation" ]; then
WORKSTATION_MODE=true
elif [ -f /opt/cezen/tier ] && [ "$(tr -d '[:space:]' < /opt/cezen/tier)" = "workstation" ]; then
WORKSTATION_MODE=true
fi
netmask_to_prefix() {
case "$1" in
32|31|30|29|28|27|26|25|24|23|22|21|20|19|18|17|16|15|14|13|12|11|10|9|8|7|6|5|4|3|2|1) echo "$1" ;;
@@ -257,40 +275,61 @@ PY
# ════════════════════════════════════════════════════════════
# STEP 3: SELECT TIER
# (skipped entirely for Nexus One AI Workstation — it is not a Server
# tier and must never appear in, or be selected from, this menu)
# ════════════════════════════════════════════════════════════
mapfile -t TIER_MENU < <(python3 - <<'PY'
if [ "$WORKSTATION_MODE" = true ]; then
TIER="starter" # Ansible/install.sh routing only — reuses the compact
# single-GPU profile. The "workstation" marker in
# /opt/cezen/tier and the install-record below is what
# actually drives Workstation branding/entitlement.
whiptail --title "$TITLE" \
--msgbox "\nNexus One AI Workstation\n\nLicense status: ${LICENSE_STATUS}\nHardware recommendation: ${HARDWARE_TIER}\n\nThis image installs the personal-appliance stack (local chat, personal RAG, document intelligence, prompt studio) — it is not a Server tier and will not be offered a Server S/M/L/Max upgrade path." \
$H $W
else
mapfile -t TIER_MENU < <(python3 - <<'PY'
import json
d=json.load(open("/tmp/cezen-license-check.json"))
labels = {
"starter": "Starter — 1x RTX 5090 / 32GB VRAM · Small team",
"basic": "Entry — 1x NVIDIA RTX Pro 6000 (96GB) · Up to 20 users",
"pro": "Pro — 2x RTX 5090 / RTX Pro class · Up to 100 users",
"max": "Max — 4-8x H100/H200/A100 class · 100+ users",
"starter": "Server S — 1x RTX 5090 / 32GB VRAM · Small team",
"basic": "Server M — 1x NVIDIA RTX Pro 6000 (96GB) · Up to 20 users",
"pro": "Server L — 2x RTX 5090 / RTX Pro class · Up to 100 users",
"max": "Server Max — 4-8x H100/H200/A100 class · 100+ users",
}
for opt in d.get("tier_options", []):
if opt.get("selectable"):
print(opt["tier"])
print(labels.get(opt["tier"], opt["tier"]))
PY
)
)
if [ "${#TIER_MENU[@]}" -eq 0 ]; then
whiptail --title "$TITLE" \
--msgbox "\nNo installable tiers are available.\n\nLicense status: ${LICENSE_STATUS}\nHardware recommendation: ${HARDWARE_TIER}\n\nCheck the signed license or contact Cezen support." \
$H $W
exit 1
fi
if [ "${#TIER_MENU[@]}" -eq 0 ]; then
whiptail --title "$TITLE" \
--msgbox "\nNo installable tiers are available.\n\nLicense status: ${LICENSE_STATUS}\nHardware recommendation: ${HARDWARE_TIER}\n\nCheck the signed license or contact Cezen support." \
--msgbox "\nLicense status: ${LICENSE_STATUS}\nAllowed tier: ${LICENSE_ALLOWED_TIER}\nHardware recommendation: ${HARDWARE_TIER}\n\nOnly valid tiers will be shown next." \
$H $W
exit 1
TIER=$(whiptail --title "$TITLE" \
--menu "\nStep 3 of 4: Select AI Package Tier\n\nChoose the tier allowed by your license and hardware:" \
$H $W 4 \
"${TIER_MENU[@]}" \
3>&1 1>&2 2>&3)
fi
whiptail --title "$TITLE" \
--msgbox "\nLicense status: ${LICENSE_STATUS}\nAllowed tier: ${LICENSE_ALLOWED_TIER}\nHardware recommendation: ${HARDWARE_TIER}\n\nOnly valid tiers will be shown next." \
$H $W
TIER=$(whiptail --title "$TITLE" \
--menu "\nStep 3 of 4: Select AI Package Tier\n\nChoose the tier allowed by your license and hardware:" \
$H $W 4 \
"${TIER_MENU[@]}" \
3>&1 1>&2 2>&3)
# Display-only label — $TIER itself stays the Ansible/install.sh routing
# value (e.g. "starter"); TIER_DISPLAY is what the operator sees and what
# gets recorded as the product category for Workstation installs.
if [ "$WORKSTATION_MODE" = true ]; then
TIER_DISPLAY="workstation"
else
TIER_DISPLAY="$TIER"
fi
# ════════════════════════════════════════════════════════════
# STEP 4: SELECT AI TOOLS
@@ -322,7 +361,7 @@ if [ -n "$LICENSE_PATH" ]; then
fi
whiptail --title "$TITLE" \
--yesno "\nReady to install. Please confirm:\n\nNetwork: ${NET_MODE} (${MY_IP})\nCustomer: ${CUSTOMER_NAME:-Not entered}\nLicense: ${LICENSE_DISPLAY}\nAllowed: ${LICENSE_ALLOWED_TIER}\nHardware: ${HARDWARE_TIER}\nTier: ${TIER}\n\nTools:\n${TOOLS_DISPLAY}\n\nThis will take 20–40 minutes.\nThe server will reboot once during install (NVIDIA drivers).\n\nContinue?" \
--yesno "\nReady to install. Please confirm:\n\nNetwork: ${NET_MODE} (${MY_IP})\nCustomer: ${CUSTOMER_NAME:-Not entered}\nLicense: ${LICENSE_DISPLAY}\nAllowed: ${LICENSE_ALLOWED_TIER}\nHardware: ${HARDWARE_TIER}\nTier: ${TIER_DISPLAY}\n\nTools:\n${TOOLS_DISPLAY}\n\nThis will take 20–40 minutes.\nThe server will reboot once during install (NVIDIA drivers).\n\nContinue?" \
$H $W
# ════════════════════════════════════════════════════════════
@@ -355,17 +394,23 @@ TIER=${TIER}
SKIP_ROLES=${SKIP_ROLES}
EOF
export CUSTOMER_NAME CUSTOMER_ID CONTACT_EMAIL SUPPORT_UNTIL TIER LICENSE_STATUS LICENSE_ALLOWED_TIER HARDWARE_TIER
export CUSTOMER_NAME CUSTOMER_ID CONTACT_EMAIL SUPPORT_UNTIL TIER TIER_DISPLAY LICENSE_STATUS LICENSE_ALLOWED_TIER HARDWARE_TIER WORKSTATION_MODE
python3 - <<'PY'
import json, os, time
# NOTE: install.sh (Phase 2) rewrites /opt/cezen/install-record.json after
# this, using its own $TIER routing value ("starter" for Workstation) — this
# is expected: provisioned_tier there reflects the Ansible profile actually
# installed. The persistent Workstation marker is /opt/cezen/tier, not this
# file, and install.sh never touches that marker.
payload = {
"schema": "cezen.install_record.v1",
"customer_name": os.environ.get("CUSTOMER_NAME", "").strip(),
"customer_id": os.environ.get("CUSTOMER_ID", "").strip(),
"contact_email": os.environ.get("CONTACT_EMAIL", "").strip(),
"selected_tier": os.environ.get("TIER", "basic").strip(),
"provisioned_tier": os.environ.get("TIER", "basic").strip(),
"selected_tier": os.environ.get("TIER_DISPLAY", "basic").strip(),
"provisioned_tier": os.environ.get("TIER_DISPLAY", "basic").strip(),
"category": "workstation" if os.environ.get("WORKSTATION_MODE") == "true" else "server",
"support_until": os.environ.get("SUPPORT_UNTIL", "").strip(),
"install_type": "licensed" if os.path.exists("/opt/cezen/license.json") else "field-staging",
"license_status": os.environ.get("LICENSE_STATUS", "missing").strip(),