Add offline Workstation payload foundation
This commit is contained in:
parent
81fa1cdb07
commit
21bca7de3f
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@ coverage/
|
||||
!.env.example
|
||||
test-results/
|
||||
playwright-report/
|
||||
autoinstall/offline-output/
|
||||
|
||||
33
autoinstall/offline/README.md
Normal file
33
autoinstall/offline/README.md
Normal file
@ -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.
|
||||
27
autoinstall/offline/apt-packages.txt
Normal file
27
autoinstall/offline/apt-packages.txt
Normal file
@ -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
|
||||
115
autoinstall/offline/build-bundle.sh
Executable file
115
autoinstall/offline/build-bundle.sh
Executable file
@ -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"
|
||||
7
autoinstall/offline/container-images.txt
Normal file
7
autoinstall/offline/container-images.txt
Normal file
@ -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
|
||||
8
autoinstall/offline/payload.env
Normal file
8
autoinstall/offline/payload.env
Normal file
@ -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"
|
||||
16
autoinstall/offline/python-packages.txt
Normal file
16
autoinstall/offline/python-packages.txt
Normal file
@ -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
|
||||
38
autoinstall/offline/verify-bundle.sh
Executable file
38
autoinstall/offline/verify-bundle.sh
Executable file
@ -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."
|
||||
Loading…
Reference in New Issue
Block a user