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
|
!.env.example
|
||||||
test-results/
|
test-results/
|
||||||
playwright-report/
|
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