From 10b66569b6aac4f88f93b8434f3206ee51c33778 Mon Sep 17 00:00:00 2001 From: Jino Jose Date: Wed, 8 Jul 2026 15:10:34 +0530 Subject: [PATCH] Add dedicated Nexus One AI Workstation ISO build path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- README.md | 35 +++-- autoinstall/build-iso-workstation.sh | 208 +++++++++++++++++++++++++++ autoinstall/firstboot-setup.sh | 89 +++++++++--- autoinstall/user-data-workstation | 156 ++++++++++++++++++++ 4 files changed, 457 insertions(+), 31 deletions(-) create mode 100755 autoinstall/build-iso-workstation.sh create mode 100644 autoinstall/user-data-workstation diff --git a/README.md b/README.md index ceb3710..0ba274d 100644 --- a/README.md +++ b/README.md @@ -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 | -The compact single-GPU ISO build path (`build-iso-starter.sh` / -`user-data-starter`) is the closest existing hardware profile to Workstation -today. It still writes the `starter` (Server S) tier marker — repointing it to -build a dedicated Workstation ISO is tracked as follow-up work, not done in -this pass (see [Migration Notes](../MIGRATION_NOTES.md)). +Build a dedicated Workstation ISO with: + +```bash +bash autoinstall/build-iso-workstation.sh +``` + +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. @@ -449,14 +460,20 @@ codebase does and does not already provide (confirmed by direct inspection of cgit/ ├── install.sh # Main installer entry point ├── 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-backup.sh # Backup/restore helper ├── ansible/ │ ├── phase1_nvidia.yml # NVIDIA/CUDA phase -│ ├── starter.yml # Starter tier -│ ├── entry.yml # Entry/Basic tier -│ ├── pro.yml # Pro tier -│ ├── max.yml # Max tier +│ ├── starter.yml # Server S tier (also used by Workstation) +│ ├── entry.yml # Server M tier +│ ├── pro.yml # Server L tier +│ ├── max.yml # Server Max tier │ └── roles/ │ ├── cezen-backend/ # FastAPI backend, cezen-api service │ ├── cezen-nginx/ # Portal/nginx deployment diff --git a/autoinstall/build-iso-workstation.sh b/autoinstall/build-iso-workstation.sh new file mode 100755 index 0000000..da3b67f --- /dev/null +++ b/autoinstall/build-iso-workstation.sh @@ -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." diff --git a/autoinstall/firstboot-setup.sh b/autoinstall/firstboot-setup.sh index da389ca..5cb0464 100644 --- a/autoinstall/firstboot-setup.sh +++ b/autoinstall/firstboot-setup.sh @@ -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(), diff --git a/autoinstall/user-data-workstation b/autoinstall/user-data-workstation new file mode 100644 index 0000000..6e4def3 --- /dev/null +++ b/autoinstall/user-data-workstation @@ -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