From 21bca7de3f905dce31b169e1358b774987395b32 Mon Sep 17 00:00:00 2001 From: Jino Jose Date: Tue, 14 Jul 2026 11:16:06 +0530 Subject: [PATCH] Add offline Workstation payload foundation --- .gitignore | 1 + autoinstall/offline/README.md | 33 +++++++ autoinstall/offline/apt-packages.txt | 27 ++++++ autoinstall/offline/build-bundle.sh | 115 +++++++++++++++++++++++ autoinstall/offline/container-images.txt | 7 ++ autoinstall/offline/payload.env | 8 ++ autoinstall/offline/python-packages.txt | 16 ++++ autoinstall/offline/verify-bundle.sh | 38 ++++++++ 8 files changed, 245 insertions(+) create mode 100644 autoinstall/offline/README.md create mode 100644 autoinstall/offline/apt-packages.txt create mode 100755 autoinstall/offline/build-bundle.sh create mode 100644 autoinstall/offline/container-images.txt create mode 100644 autoinstall/offline/payload.env create mode 100644 autoinstall/offline/python-packages.txt create mode 100755 autoinstall/offline/verify-bundle.sh diff --git a/.gitignore b/.gitignore index 4d58cfe..9b45dec 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ coverage/ !.env.example test-results/ playwright-report/ +autoinstall/offline-output/ diff --git a/autoinstall/offline/README.md b/autoinstall/offline/README.md new file mode 100644 index 0000000..98a88a9 --- /dev/null +++ b/autoinstall/offline/README.md @@ -0,0 +1,33 @@ +# Nexus One AI Offline Server Payload + +This directory defines the payload embedded by `build-iso-offline.sh`. The +offline installer is deliberately separate from the existing online bootstrap +ISO until disconnected clean-install validation is complete. + +## Offline Workstation contents + +- the exact `aipackage` source revision used for the build +- Ubuntu, Docker, NVIDIA and appliance `.deb` packages +- Python wheels for the backend, RAG, ChromaDB and optional JupyterLab +- Miniconda installer and an offline Python 3.11 environment package +- Ollama runtime with `phi3:mini` and `nomic-embed-text` +- vLLM and web/monitoring container images +- the pinned Phi-3 Hugging Face model snapshot used by vLLM +- K3s, MinIO and `mc` binaries for update/model packs (not enabled by Starter) +- SHA-256 manifests and build provenance + +Server L/Max models are intentionally separate signed model packs. Optional external +connectors cannot operate without a customer-approved route to their target. + +## Build stages + +1. On a connected Ubuntu 22.04 staging host with at least 80 GiB free, run + `sudo bash autoinstall/offline/build-bundle.sh`. +2. Review `autoinstall/offline-output/manifest/` and sign + `SHA256SUMS` with the release key. +3. Run `sudo bash autoinstall/build-iso-offline.sh` to inject the verified + payload into the Ubuntu Server ISO. +4. Validate with all network interfaces disconnected. + +The bundle builder fails closed when an expected payload is absent. An ISO must +not be labelled offline merely because the source tree was embedded. diff --git a/autoinstall/offline/apt-packages.txt b/autoinstall/offline/apt-packages.txt new file mode 100644 index 0000000..e252971 --- /dev/null +++ b/autoinstall/offline/apt-packages.txt @@ -0,0 +1,27 @@ +# Packages required in addition to the Ubuntu 22.04.5 Server media. +ansible +avahi-daemon +build-essential +ca-certificates +curl +ffmpeg +git +gnupg +htop +jq +libmupdf-dev +lsb-release +mupdf-tools +net-tools +nginx +openssh-server +python3-pip +python3-venv +python3.11 +python3.11-venv +rsync +software-properties-common +ttyd +unzip +wget +whiptail diff --git a/autoinstall/offline/build-bundle.sh b/autoinstall/offline/build-bundle.sh new file mode 100755 index 0000000..26633bc --- /dev/null +++ b/autoinstall/offline/build-bundle.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Stage all network-fetched Server Starter inputs for a disconnected install. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +OUTPUT_DIR="${OFFLINE_OUTPUT_DIR:-$PACKAGE_DIR/autoinstall/offline-output}" +PAYLOAD_DIR="$OUTPUT_DIR/payload" +MANIFEST_DIR="$OUTPUT_DIR/manifest" + +# shellcheck source=payload.env +source "$SCRIPT_DIR/payload.env" + +if [ "${EUID:-$(id -u)}" -ne 0 ]; then + echo "ERROR: run as root on a connected Ubuntu 22.04 staging host." >&2 + exit 1 +fi +. /etc/os-release +if [ "${ID:-}" != ubuntu ] || [ "${VERSION_ID:-}" != 22.04 ]; then + echo "ERROR: Ubuntu 22.04 staging host required; found ${PRETTY_NAME:-unknown}." >&2 + exit 1 +fi + +for command in apt-get curl docker git python3 rsync sha256sum tar; do + command -v "$command" >/dev/null || { + echo "ERROR: missing staging command: $command" >&2 + exit 1 + } +done + +available_kb="$(df -Pk "$(dirname "$OUTPUT_DIR")" | awk 'NR==2 {print $4}')" +if [ "$available_kb" -lt $((80 * 1024 * 1024)) ]; then + echo "ERROR: at least 80 GiB free is required for the Workstation offline payload." >&2 + exit 1 +fi + +rm -rf "$OUTPUT_DIR" +mkdir -p "$PAYLOAD_DIR"/{apt,containers,models,python,runtime,source} "$MANIFEST_DIR" + +echo "-> Capturing pinned appliance source" +git -C "$PACKAGE_DIR" rev-parse HEAD > "$MANIFEST_DIR/source-commit.txt" +rsync -a --delete \ + --exclude '.git/' --exclude '*.iso' --exclude 'offline-output/' \ + --exclude 'how-it-flows.gif' \ + "$PACKAGE_DIR/" "$PAYLOAD_DIR/source/aipackage/" + +echo "-> Downloading Ubuntu package payload" +export DEBIAN_FRONTEND=noninteractive +mapfile -t apt_packages < <(sed -E '/^[[:space:]]*(#|$)/d' "$SCRIPT_DIR/apt-packages.txt") +apt-get update +apt-get install -y --download-only --reinstall \ + -o "Dir::Cache::archives=$PAYLOAD_DIR/apt" "${apt_packages[@]}" + +echo "-> Downloading Python wheelhouse" +python3 -m pip download --dest "$PAYLOAD_DIR/python" \ + -r "$PACKAGE_DIR/ansible/roles/cezen-backend/files/requirements.txt" \ + -r "$SCRIPT_DIR/python-packages.txt" + +echo "-> Capturing Miniconda installer" +curl --fail --location --retry 3 "$MINICONDA_URL" \ + --output "$PAYLOAD_DIR/runtime/miniconda.sh" +chmod 0755 "$PAYLOAD_DIR/runtime/miniconda.sh" + +echo "-> Pulling and exporting container images" +: > "$MANIFEST_DIR/container-images.txt" +while IFS= read -r image; do + case "$image" in ''|'#'*) continue ;; esac + docker pull "$image" + safe_name="$(printf '%s' "$image" | tr '/:@' '___')" + docker save "$image" | gzip -1 > "$PAYLOAD_DIR/containers/$safe_name.tar.gz" + digest="$(docker image inspect "$image" --format '{{join .RepoDigests ","}}')" + printf '%s\t%s\n' "$image" "$digest" >> "$MANIFEST_DIR/container-images.txt" +done < "$SCRIPT_DIR/container-images.txt" + +echo "-> Capturing Ollama runtime and Starter models" +if ! command -v ollama >/dev/null; then + curl --fail --location --retry 3 https://ollama.com/install.sh | sh +fi +systemctl start ollama 2>/dev/null || true +for model in $OLLAMA_MODELS; do ollama pull "$model"; done +tar -C / -czf "$PAYLOAD_DIR/runtime/ollama-runtime.tar.gz" \ + usr/local/bin/ollama usr/local/lib/ollama +ollama_root="${OLLAMA_MODELS_DIR:-/usr/share/ollama/.ollama/models}" +[ -d "$ollama_root" ] || ollama_root="/root/.ollama/models" +[ -d "$ollama_root" ] || { + echo "ERROR: Ollama model store not found after pull." >&2 + exit 1 +} +tar -C "$(dirname "$ollama_root")" -czf "$PAYLOAD_DIR/models/ollama-models.tar.gz" models + +echo "-> Capturing vLLM Starter model snapshot" +python3 -m pip install --quiet huggingface-hub +python3 - "$VLLM_MODEL" "$PAYLOAD_DIR/models/huggingface" <<'PY' +import sys +from huggingface_hub import snapshot_download +snapshot_download(repo_id=sys.argv[1], local_dir=sys.argv[2]) +PY + +echo "-> Writing provenance and integrity manifest" +{ + printf 'schema=%s\n' "$OFFLINE_SCHEMA" + printf 'tier=%s\n' "$OFFLINE_TIER" + printf 'ubuntu_release=%s\n' "$UBUNTU_RELEASE" + printf 'built_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + printf 'builder=%s\n' "$(hostname -f 2>/dev/null || hostname)" +} > "$MANIFEST_DIR/provenance.env" +( + cd "$OUTPUT_DIR" + find payload manifest -type f ! -name SHA256SUMS -print0 \ + | sort -z | xargs -0 sha256sum > manifest/SHA256SUMS + sha256sum -c manifest/SHA256SUMS >/dev/null +) + +echo "Offline payload staged at $OUTPUT_DIR" +du -sh "$OUTPUT_DIR" diff --git a/autoinstall/offline/container-images.txt b/autoinstall/offline/container-images.txt new file mode 100644 index 0000000..96351f3 --- /dev/null +++ b/autoinstall/offline/container-images.txt @@ -0,0 +1,7 @@ +# Bundle builder records the resolved digest for each image. +ghcr.io/open-webui/open-webui:main +vllm/vllm-openai:v0.10.2 +nvcr.io/nvidia/k8s/dcgm-exporter:3.3.0-3.2.0-ubuntu22.04 +prom/prometheus:latest +prom/node-exporter:latest +grafana/grafana:latest diff --git a/autoinstall/offline/payload.env b/autoinstall/offline/payload.env new file mode 100644 index 0000000..a93dc44 --- /dev/null +++ b/autoinstall/offline/payload.env @@ -0,0 +1,8 @@ +# Offline Server Starter release inputs. Mutable tags are resolved to immutable +# image digests in the generated provenance file at bundle-build time. +OFFLINE_SCHEMA="cezen.offline_payload.v1" +OFFLINE_TIER="workstation" +UBUNTU_RELEASE="22.04.5" +MINICONDA_URL="https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh" +OLLAMA_MODELS="phi3:mini nomic-embed-text" +VLLM_MODEL="microsoft/Phi-3-mini-4k-instruct" diff --git a/autoinstall/offline/python-packages.txt b/autoinstall/offline/python-packages.txt new file mode 100644 index 0000000..0a0e15e --- /dev/null +++ b/autoinstall/offline/python-packages.txt @@ -0,0 +1,16 @@ +# Additional wheelhouse groups beyond backend requirements.txt. +chromadb==0.5.23 +jupyterlab +ipywidgets +ipykernel +notebook +nbconvert +langchain +langchain-community +llama-index +transformers +huggingface-hub +peft +bitsandbytes +accelerate +sentence-transformers diff --git a/autoinstall/offline/verify-bundle.sh b/autoinstall/offline/verify-bundle.sh new file mode 100755 index 0000000..7aa84e7 --- /dev/null +++ b/autoinstall/offline/verify-bundle.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +OUTPUT_DIR="${OFFLINE_OUTPUT_DIR:-$PACKAGE_DIR/autoinstall/offline-output}" + +required=( + manifest/provenance.env + manifest/source-commit.txt + manifest/container-images.txt + manifest/SHA256SUMS + payload/source/aipackage/install.sh + payload/runtime/miniconda.sh + payload/runtime/ollama-runtime.tar.gz + payload/models/ollama-models.tar.gz +) + +for path in "${required[@]}"; do + [ -s "$OUTPUT_DIR/$path" ] || { + echo "ERROR: required offline payload is missing or empty: $path" >&2 + exit 1 + } +done + +for directory in payload/apt payload/containers payload/python payload/models/huggingface; do + find "$OUTPUT_DIR/$directory" -type f -print -quit | grep -q . || { + echo "ERROR: required offline payload directory is empty: $directory" >&2 + exit 1 + } +done + +( + cd "$OUTPUT_DIR" + sha256sum -c manifest/SHA256SUMS +) + +echo "Offline payload integrity and completeness checks passed."