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 2026-07-08 15:10:34 +05:30
parent 12d9ea84f6
commit 10b66569b6
4 changed files with 457 additions and 31 deletions

View File

@ -263,11 +263,22 @@ kept outside the Server ladder — see [Product Structure](#product-structure)):
|---|---|---|---| |---|---|---|---|
| Workstation (`workstation`) | NVIDIA DGX Spark, RTX 5090 Workstation, or RTX PRO 6000 Workstation | Local chat, personal RAG, prompt studio, document intelligence | 1-3 active, 5 max | | Workstation (`workstation`) | NVIDIA DGX Spark, RTX 5090 Workstation, or RTX PRO 6000 Workstation | Local chat, personal RAG, prompt studio, document intelligence | 1-3 active, 5 max |
The compact single-GPU ISO build path (`build-iso-starter.sh` / Build a dedicated Workstation ISO with:
`user-data-starter`) is the closest existing hardware profile to Workstation
today. It still writes the `starter` (Server S) tier marker — repointing it to ```bash
build a dedicated Workstation ISO is tracked as follow-up work, not done in bash autoinstall/build-iso-workstation.sh
this pass (see [Migration Notes](../MIGRATION_NOTES.md)). ```
This produces `autoinstall/cezen-ai-workstation-ubuntu2204.iso`, using
`user-data-workstation` — same compact single-GPU hardware profile and
Ansible playbook (`ansible/starter.yml`) as Server S under the hood, but a
distinct `workstation` tier marker (`/opt/cezen/tier`) so the installed
system brands and entitles itself as Nexus One AI Workstation. The first-boot
wizard (`firstboot-setup.sh`) detects `CEZEN_TIER=workstation` and skips the
Server tier-selection step entirely — Workstation is never offered as a
Server S/M/L/Max choice. See [Migration Notes](../MIGRATION_NOTES.md) for
what is and isn't wired up yet (live portal branding still needs a manual
`CEZEN_TIER` override on the deployed service — see below).
Large models can be pulled later. The ISO does not need to contain them. Large models can be pulled later. The ISO does not need to contain them.
@ -449,14 +460,20 @@ codebase does and does not already provide (confirmed by direct inspection of
cgit/ cgit/
├── install.sh # Main installer entry point ├── install.sh # Main installer entry point
├── autoinstall/ # ISO first-boot setup and web setup ├── autoinstall/ # ISO first-boot setup and web setup
│ ├── build-iso.sh # General multi-tier Server ISO builder
│ ├── build-iso-starter.sh # Server S compact single-GPU ISO builder
│ ├── build-iso-workstation.sh # Nexus One AI Workstation ISO builder
│ ├── user-data-starter # Server S autoinstall config
│ ├── user-data-workstation # Workstation autoinstall config
│ └── firstboot-setup.sh # TTY setup wizard (skips tier menu for Workstation)
├── scripts/cezen-feasibility.sh # Existing-server feasibility checker ├── scripts/cezen-feasibility.sh # Existing-server feasibility checker
├── scripts/cezen-backup.sh # Backup/restore helper ├── scripts/cezen-backup.sh # Backup/restore helper
├── ansible/ ├── ansible/
│ ├── phase1_nvidia.yml # NVIDIA/CUDA phase │ ├── phase1_nvidia.yml # NVIDIA/CUDA phase
│ ├── starter.yml # Starter tier │ ├── starter.yml # Server S tier (also used by Workstation)
│ ├── entry.yml # Entry/Basic tier │ ├── entry.yml # Server M tier
│ ├── pro.yml # Pro tier │ ├── pro.yml # Server L tier
│ ├── max.yml # Max tier │ ├── max.yml # Server Max tier
│ └── roles/ │ └── roles/
│ ├── cezen-backend/ # FastAPI backend, cezen-api service │ ├── cezen-backend/ # FastAPI backend, cezen-api service
│ ├── cezen-nginx/ # Portal/nginx deployment │ ├── cezen-nginx/ # Portal/nginx deployment

View File

@ -0,0 +1,208 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────
# Nexus One AI — Workstation ISO Builder
# Hardware target: personal AI appliance (NVIDIA DGX Spark, RTX 5090
# Workstation, or RTX PRO 6000 Workstation; 64 GB RAM, 2 TB NVMe)
#
# Nexus One AI Workstation is a standalone product category (see
# ../README.md → "Product Structure"), not a Server tier. This build
# shares the Server S ("starter") hardware profile and Ansible playbook
# under the hood, but writes a distinct "workstation" tier marker so the
# installed system identifies and brands itself as Nexus One AI
# Workstation rather than a Server S install.
#
# Usage:
# cd ~/aipackage
# bash autoinstall/build-iso-workstation.sh
#
# Output: autoinstall/cezen-ai-workstation-ubuntu2204.iso
# Flash to USB:
# diskutil unmountDisk /dev/diskN
# sudo dd if=cezen-ai-workstation-ubuntu2204.iso of=/dev/diskN bs=4m status=progress
# ─────────────────────────────────────────────────────────────
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
WORK_DIR="/tmp/cezen-iso-workstation-work"
ORIGINAL_ISO="/tmp/ubuntu-22.04.5-live-server-amd64.iso"
OUTPUT_ISO="$SCRIPT_DIR/cezen-ai-workstation-ubuntu2204.iso"
UBUNTU_URL="https://releases.ubuntu.com/22.04.5/ubuntu-22.04.5-live-server-amd64.iso"
TIER="workstation"
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Nexus One AI — ISO Builder [WORKSTATION] ║"
echo "║ DGX Spark / RTX 5090 / RTX PRO 6000 · 1-3 users ║"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
# ── Install build tools ────────────────────────
echo "→ Installing build tools..."
apt-get update -qq
apt-get install -y -qq xorriso wget isolinux rsync
echo "✓ Tools ready"
# ── Download Ubuntu ISO ────────────────────────
if [ -f "$ORIGINAL_ISO" ]; then
echo "✓ Ubuntu ISO already downloaded"
else
echo "→ Downloading Ubuntu 22.04.5 Server ISO (~1.8 GB)..."
wget --show-progress -O "$ORIGINAL_ISO" "$UBUNTU_URL"
echo "✓ Downloaded"
fi
# ── Extract ISO ────────────────────────────────
echo "→ Extracting ISO..."
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
xorriso -osirrox on \
-indev "$ORIGINAL_ISO" \
-extract / "$WORK_DIR" 2>/dev/null
chmod -R u+w "$WORK_DIR"
echo "✓ Extracted"
# ── Inject Workstation autoinstall files ──────
echo "→ Injecting Workstation autoinstall config..."
mkdir -p "$WORK_DIR/nocloud"
cp "$SCRIPT_DIR/user-data-workstation" "$WORK_DIR/nocloud/user-data"
cp "$SCRIPT_DIR/meta-data" "$WORK_DIR/nocloud/meta-data"
echo "✓ user-data-workstation and meta-data injected"
# ── Online installer mode ──────────────────────
# The installed system pulls the current package from cgit on first boot. This
# keeps the ISO small and avoids shipping stale backend/portal code.
echo "✓ Online installer mode: package will be pulled from cgit on first boot"
# ── Patch GRUB ────────────────────────────────
echo "→ Patching GRUB config..."
GRUB_CFG="$WORK_DIR/boot/grub/grub.cfg"
cp "$GRUB_CFG" "$GRUB_CFG.orig"
sed -i "s/set timeout=.*/set timeout=5/" "$GRUB_CFG"
sed -i "s/set timeout_style=.*/set timeout_style=countdown/" "$GRUB_CFG"
sed -i '/^\s*linux.*vmlinuz/s|---|autoinstall ds=nocloud\\;s=/cdrom/nocloud/ ---|' "$GRUB_CFG"
# Update GRUB title to reflect the Workstation category
sed -i 's/Install Ubuntu Server/Install Nexus One AI — Workstation/' "$GRUB_CFG" || true
echo "✓ GRUB patched"
# ── Extract MBR and EFI boot data ─────────────
echo "→ Extracting boot data from original ISO..."
MBR_TEMPLATE=$(mktemp)
EFI_IMG=$(mktemp)
dd if="$ORIGINAL_ISO" bs=1 count=432 of="$MBR_TEMPLATE" 2>/dev/null
EFI_LINE=$(fdisk -l "$ORIGINAL_ISO" 2>/dev/null | grep "EFI")
echo " EFI partition info: $EFI_LINE"
EFI_START=$(echo "$EFI_LINE" | awk '{print $2}')
EFI_SIZE=$(echo "$EFI_LINE" | awk '{print $4}')
if [ -z "$EFI_START" ] || [ -z "$EFI_SIZE" ]; then
echo "ERROR: Could not detect EFI partition in ISO."
echo "Run: fdisk -l $ORIGINAL_ISO"
exit 1
fi
dd if="$ORIGINAL_ISO" bs=512 skip="$EFI_START" count="$EFI_SIZE" \
of="$EFI_IMG" 2>/dev/null
echo "✓ EFI partition extracted (start=$EFI_START, size=$EFI_SIZE)"
# ── Repack ISO (pass 1) ────────────────────────
echo "→ Repacking ISO (pass 1)..."
xorriso -as mkisofs \
-r \
-V "CezenAI_Workstation_2204" \
-o "$OUTPUT_ISO" \
--grub2-mbr "$MBR_TEMPLATE" \
-partition_offset 16 \
--mbr-force-bootable \
-append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b "$EFI_IMG" \
-appended_part_as_gpt \
-iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \
-c "/boot.catalog" \
-b "/boot/grub/i386-pc/eltorito.img" \
-no-emul-boot \
-boot-load-size 4 \
-boot-info-table \
--grub2-boot-info \
-eltorito-alt-boot \
-e "--interval:appended_partition_2:::" \
-no-emul-boot \
"$WORK_DIR"
# ── Refresh md5sum.txt and repack (pass 2) ────
echo "→ Refreshing md5sum.txt..."
FINAL_DIR=$(mktemp -d)
VERIFY_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR" "$MBR_TEMPLATE" "$EFI_IMG" "$FINAL_DIR" "$VERIFY_DIR"' EXIT
xorriso -osirrox on -indev "$OUTPUT_ISO" -extract / "$FINAL_DIR" >/dev/null 2>&1
chmod -R u+w "$FINAL_DIR"
(
cd "$FINAL_DIR"
rm -f md5sum.txt
find . -type f \
! -path './md5sum.txt' \
! -path './boot.catalog' \
-print0 \
| sort -z \
| xargs -0 md5sum > md5sum.txt
)
echo "✓ md5sum.txt refreshed"
echo "→ Repacking ISO (pass 2)..."
xorriso -as mkisofs \
-r \
-V "CezenAI_Workstation_2204" \
-o "$OUTPUT_ISO" \
--grub2-mbr "$MBR_TEMPLATE" \
-partition_offset 16 \
--mbr-force-bootable \
-append_partition 2 28732ac11ff8d211ba4b00a0c93ec93b "$EFI_IMG" \
-appended_part_as_gpt \
-iso_mbr_part_type a2a0d0ebe5b9334487c068b6b72699c7 \
-c "/boot.catalog" \
-b "/boot/grub/i386-pc/eltorito.img" \
-no-emul-boot \
-boot-load-size 4 \
-boot-info-table \
--grub2-boot-info \
-eltorito-alt-boot \
-e "--interval:appended_partition_2:::" \
-no-emul-boot \
"$FINAL_DIR"
# ── Verify output ISO ──────────────────────────
echo "→ Verifying rebuilt ISO manifest..."
xorriso -osirrox on -indev "$OUTPUT_ISO" -extract / "$VERIFY_DIR" >/dev/null 2>&1
chmod -R u+w "$VERIFY_DIR"
(
cd "$VERIFY_DIR"
md5sum -c md5sum.txt >/tmp/cezen-iso-md5check-workstation.log 2>&1 || {
echo "ERROR: Rebuilt ISO failed its own md5sum.txt verification."
sed -n '1,40p' /tmp/cezen-iso-md5check-workstation.log
exit 1
}
)
echo "✓ Output ISO manifest verified"
echo ""
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Done! Workstation ISO ready. ║"
echo "╚══════════════════════════════════════════════════════╝"
echo ""
ls -lh "$OUTPUT_ISO"
echo ""
echo "→ Transfer to MacBook:"
echo " scp user@server:~/aipackage/autoinstall/cezen-ai-workstation-ubuntu2204.iso ."
echo ""
echo "→ Flash to USB (macOS):"
echo " diskutil list # find USB e.g. /dev/disk4"
echo " diskutil unmountDisk /dev/disk4"
echo " sudo dd if=cezen-ai-workstation-ubuntu2204.iso of=/dev/disk4 bs=4m status=progress"
echo ""
echo "→ Post-flash: boot the workstation from USB."
echo " Unattended install completes in ~10 min."
echo " First-boot wizard runs on tty1 — set IP, org name, admin password."
echo " Tier selection (Step 3) is skipped automatically — this image"
echo " installs as Nexus One AI Workstation, not a Server tier."

View File

@ -23,6 +23,24 @@ detect_iface() {
IFACE="$(detect_iface)" IFACE="$(detect_iface)"
IFACE="${IFACE:-$(ip -o link show | awk -F': ' '$2 !~ /lo|docker|br-|veth/ {print $2; exit}')}" 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() { netmask_to_prefix() {
case "$1" in 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" ;; 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 # 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 import json
d=json.load(open("/tmp/cezen-license-check.json")) d=json.load(open("/tmp/cezen-license-check.json"))
labels = { labels = {
"starter": "Starter — 1x RTX 5090 / 32GB VRAM · Small team", "starter": "Server S — 1x RTX 5090 / 32GB VRAM · Small team",
"basic": "Entry — 1x NVIDIA RTX Pro 6000 (96GB) · Up to 20 users", "basic": "Server M — 1x NVIDIA RTX Pro 6000 (96GB) · Up to 20 users",
"pro": "Pro — 2x RTX 5090 / RTX Pro class · Up to 100 users", "pro": "Server L — 2x RTX 5090 / RTX Pro class · Up to 100 users",
"max": "Max — 4-8x H100/H200/A100 class · 100+ users", "max": "Server Max — 4-8x H100/H200/A100 class · 100+ users",
} }
for opt in d.get("tier_options", []): for opt in d.get("tier_options", []):
if opt.get("selectable"): if opt.get("selectable"):
print(opt["tier"]) print(opt["tier"])
print(labels.get(opt["tier"], opt["tier"])) print(labels.get(opt["tier"], opt["tier"]))
PY PY
) )
if [ "${#TIER_MENU[@]}" -eq 0 ]; then if [ "${#TIER_MENU[@]}" -eq 0 ]; then
whiptail --title "$TITLE" \ 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 "\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 $H $W
exit 1 exit 1
fi fi
whiptail --title "$TITLE" \ 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." \ --msgbox "\nLicense status: ${LICENSE_STATUS}\nAllowed tier: ${LICENSE_ALLOWED_TIER}\nHardware recommendation: ${HARDWARE_TIER}\n\nOnly valid tiers will be shown next." \
$H $W $H $W
TIER=$(whiptail --title "$TITLE" \ TIER=$(whiptail --title "$TITLE" \
--menu "\nStep 3 of 4: Select AI Package Tier\n\nChoose the tier allowed by your license and hardware:" \ --menu "\nStep 3 of 4: Select AI Package Tier\n\nChoose the tier allowed by your license and hardware:" \
$H $W 4 \ $H $W 4 \
"${TIER_MENU[@]}" \ "${TIER_MENU[@]}" \
3>&1 1>&2 2>&3) 3>&1 1>&2 2>&3)
fi
# 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 # STEP 4: SELECT AI TOOLS
@ -322,7 +361,7 @@ if [ -n "$LICENSE_PATH" ]; then
fi fi
whiptail --title "$TITLE" \ 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 2040 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 2040 minutes.\nThe server will reboot once during install (NVIDIA drivers).\n\nContinue?" \
$H $W $H $W
# ════════════════════════════════════════════════════════════ # ════════════════════════════════════════════════════════════
@ -355,17 +394,23 @@ TIER=${TIER}
SKIP_ROLES=${SKIP_ROLES} SKIP_ROLES=${SKIP_ROLES}
EOF 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' python3 - <<'PY'
import json, os, time 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 = { payload = {
"schema": "cezen.install_record.v1", "schema": "cezen.install_record.v1",
"customer_name": os.environ.get("CUSTOMER_NAME", "").strip(), "customer_name": os.environ.get("CUSTOMER_NAME", "").strip(),
"customer_id": os.environ.get("CUSTOMER_ID", "").strip(), "customer_id": os.environ.get("CUSTOMER_ID", "").strip(),
"contact_email": os.environ.get("CONTACT_EMAIL", "").strip(), "contact_email": os.environ.get("CONTACT_EMAIL", "").strip(),
"selected_tier": os.environ.get("TIER", "basic").strip(), "selected_tier": os.environ.get("TIER_DISPLAY", "basic").strip(),
"provisioned_tier": os.environ.get("TIER", "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(), "support_until": os.environ.get("SUPPORT_UNTIL", "").strip(),
"install_type": "licensed" if os.path.exists("/opt/cezen/license.json") else "field-staging", "install_type": "licensed" if os.path.exists("/opt/cezen/license.json") else "field-staging",
"license_status": os.environ.get("LICENSE_STATUS", "missing").strip(), "license_status": os.environ.get("LICENSE_STATUS", "missing").strip(),

View File

@ -0,0 +1,156 @@
#cloud-config
# ─────────────────────────────────────────────────────────────
# Nexus One AI — Workstation Autoinstall
# Hardware target: personal AI appliance (Mini-ITX / SFF / DGX Spark class)
# GPU: NVIDIA DGX Spark, RTX 5090 Workstation, or RTX PRO 6000
# Workstation (1 GPU)
# RAM: 64 GB DDR5
# Storage: 1× 2 TB NVMe SSD (single drive — simple LVM)
# Network: 2.5 GbE (single interface)
# Users: 1-3 active, 5 max — not a shared server, no HA
#
# This reuses the same compact single-GPU install profile as the Server S
# ("starter") image below — same hardware class, same Ansible playbook
# (ansible/starter.yml) — but is a distinct, standalone product category
# (Nexus One AI Workstation), not part of the Server S/M/L/Max ladder.
# See cezen_license.WORKSTATION_TIER / TIER_MATRIX["workstation"].
# ─────────────────────────────────────────────────────────────
autoinstall:
version: 1
# ── Locale & keyboard ──────────────────────────
locale: en_IN.UTF-8
keyboard:
layout: us
# ── Network: DHCP during install; static config applied post-install ──
network:
network:
version: 2
ethernets:
any-en:
dhcp4: true
match:
name: "en*"
any-eth:
dhcp4: true
match:
name: "eth*"
# ── Storage: single 2 TB NVMe, simple LVM ─────
# Workstations have one drive — no RAID needed.
storage:
layout:
name: lvm
match:
size: largest
# ── Identity ──────────────────────────────────
identity:
hostname: cezenai-workstation
username: cezen
# Default password: cezen@123 (change via first-boot wizard)
password: "$6$I5VA.42G1xTeVhCv$KCLzqIKg/kbNHZyiTEMAY4FZsJMDDwoS90k6Ffb9VEwmcK.wuzlJNe3ceiEfLrzYzXEvqjYsLc7klAbeGPGab."
# ── SSH ───────────────────────────────────────
ssh:
install-server: true
allow-pw: true
# ── Base packages ─────────────────────────────
packages:
- git
- curl
- wget
- python3
- whiptail
- openssh-server
- nvme-cli # NVMe health / SMART monitoring
# ── Late commands ─────────────────────────────
late-commands:
# Expand LVM to fill the full 2 TB NVMe
- lvextend -l +100%FREE /dev/ubuntu-vg/ubuntu-lv || true
- resize2fs /dev/ubuntu-vg/ubuntu-lv || true
# Passwordless sudo for cezen (needed by install.sh + first-boot wizard)
- echo "cezen ALL=(ALL) NOPASSWD:ALL" > /target/etc/sudoers.d/cezen
- chmod 440 /target/etc/sudoers.d/cezen
# Replace installer netplan with simple DHCP target config.
# The first-boot wizard will switch to static if desired.
- rm -f /target/etc/netplan/50-cloud-init.yaml /target/etc/netplan/00-installer-config.yaml || true
- |
cat > /target/etc/netplan/99-cezen-dhcp.yaml << 'EOF'
network:
version: 2
ethernets:
any-en:
dhcp4: true
match:
name: "en*"
any-eth:
dhcp4: true
match:
name: "eth*"
EOF
# Disable cdrom APT source
- sed -i 's/^deb cdrom:/# deb cdrom:/' /target/etc/apt/sources.list || true
# Pull the Nexus One AI installer from cgit. The ISO intentionally does not
# bundle the full package, keeping the image small and the installed code
# current at deployment time.
- mkdir -p /target/opt/aipackage
- git clone https://cgit.cezentech.com/jinojose/aipackage.git /target/opt/aipackage
# Write tier marker — used by install.sh (as a fallback default only;
# the first-boot wizard always passes --tier=starter explicitly for
# Ansible routing) and by the portal branding/entitlement system, which
# reads "workstation" here to show Nexus One AI Workstation branding and
# the workstation feature set (TIER_MATRIX["workstation"]) instead of a
# Server tier. install.sh never writes to this file, so it persists
# unchanged through Phase 1 and Phase 2.
- mkdir -p /target/opt/cezen
- echo "workstation" > /target/opt/cezen/tier
# Deploy first-boot TUI wizard
- cp /target/opt/aipackage/autoinstall/firstboot-setup.sh /target/opt/cezen/firstboot-setup.sh
- chmod +x /target/opt/cezen/firstboot-setup.sh
# Set hostname
- echo "cezenai-workstation" > /target/etc/hostname
- sed -i 's/aiserver/cezenai-workstation/g' /target/etc/hosts || true
# Systemd service: run first-boot wizard on tty1 once.
# CEZEN_TIER=workstation tells firstboot-setup.sh to skip the Server
# tier-selection menu (Step 3) — Workstation is never offered there.
- |
cat > /target/etc/systemd/system/cezen-setup.service << 'EOF'
[Unit]
Description=Nexus One AI — Console Setup Wizard (Workstation)
After=cloud-final.service cloud-init.target network-online.target
Wants=cloud-init.target network-online.target
ConditionPathExists=!/opt/cezen/.setup-done
OnFailure=getty@tty1.service
[Service]
Type=oneshot
WorkingDirectory=/opt/cezen
ExecStartPre=-/bin/systemctl stop getty@tty1.service
ExecStartPre=-/usr/bin/chvt 1
ExecStart=/bin/bash -lc 'clear >/dev/tty1 2>/dev/null || true; /usr/bin/openvt -c 1 -f -w -- env TERM=linux CEZEN_TIER=workstation /opt/cezen/firstboot-setup.sh'
ExecStartPost=-/bin/systemctl start getty@tty1.service
StandardOutput=journal+console
StandardError=journal+console
Restart=no
[Install]
WantedBy=cloud-init.target
EOF
- curtin in-target -- systemctl enable ssh
- curtin in-target -- systemctl enable cezen-setup.service
user-data:
disable_root: false