240 lines
9.3 KiB
Bash
Executable File
240 lines
9.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# LLM host ISO — the Phase 3 Ollama machine (hosts/llm-host/), as an unattended-install
|
|
# ISO with everything burnt in.
|
|
#
|
|
# The twin of build-container-host-iso.sh. Both read the same CoreSystemConfig.json, so
|
|
# this image's address is the one the container host was built to call, by construction
|
|
# rather than by agreement.
|
|
#
|
|
# This half is deliberately the simpler one, and that asymmetry is the design: the LLM
|
|
# host is a *server*. It doesn't need to know the container host's address, hold any
|
|
# service token, or reach anything at boot beyond a model registry. Nothing here is
|
|
# load-bearing for the house — see hosts/llm-host/README.md's guardrail. Keeping this
|
|
# image dumb is what lets you power it off, reinstall it, or swap the GPU without any
|
|
# of that touching the smart home.
|
|
#
|
|
# Normally invoked via tools/build-core-pair.sh.
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=lib/coreconfig.sh
|
|
source "${SCRIPT_DIR}/lib/coreconfig.sh"
|
|
|
|
core_load
|
|
core_require_root
|
|
|
|
HOST_DIR="${CORE_REPO_ROOT}/hosts/llm-host"
|
|
LIVE_BUILD_DIR="${HOST_DIR}/live-build"
|
|
INCLUDES="${LIVE_BUILD_DIR}/config/includes.chroot"
|
|
PAYLOAD="${INCLUDES}/opt/llm-host"
|
|
OUTPUT_DIR="${CORE_REPO_ROOT}/${CORE_BUILD_OUTPUT_DIR}"
|
|
|
|
command -v lb >/dev/null 2>&1 || core_die "live-build is not installed (apt install live-build)"
|
|
|
|
core_log "Preparing ${LIVE_BUILD_DIR}"
|
|
rm -rf "$INCLUDES"
|
|
mkdir -p \
|
|
"$INCLUDES/etc/systemd/system" \
|
|
"$INCLUDES/etc/default" \
|
|
"$INCLUDES/etc/network/interfaces.d" \
|
|
"$LIVE_BUILD_DIR/config/package-lists" \
|
|
"$LIVE_BUILD_DIR/config/includes.installer" \
|
|
"$PAYLOAD" \
|
|
"$OUTPUT_DIR"
|
|
|
|
mkdir -p "$PAYLOAD/scripts"
|
|
cp "${CORE_TOOLS_DIR}/setup-llm-host.sh" "$PAYLOAD/scripts/"
|
|
chmod +x "$PAYLOAD/scripts/setup-llm-host.sh"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. Bake the config into setup-llm-host.sh's own variables, so the first-boot run
|
|
# needs no arguments and no editing. Values come from CoreSystemConfig.json; the
|
|
# script keeps its defaults for anything not managed centrally.
|
|
# ---------------------------------------------------------------------------
|
|
core_log "Generating the LLM host's setup overrides"
|
|
cat > "$PAYLOAD/llm-host.env" <<EOF
|
|
# GENERATED at image build time by tools/build-llm-host-iso.sh from
|
|
# CoreSystemConfig.json. Sourced by the first-boot unit to override
|
|
# setup-llm-host.sh's defaults.
|
|
# Pair ID: $(core_pair_id)
|
|
TIER=${CORE_LLM_TIER}
|
|
OLLAMA_PORT=${CORE_PORT_OLLAMA}
|
|
GPU_TEXT_MODEL=${CORE_LLM_TEXT_MODEL_GPU}
|
|
CPU_TEXT_MODEL=${CORE_LLM_TEXT_MODEL_CPU}
|
|
VISION_MODEL=${CORE_LLM_VISION_MODEL}
|
|
PULL_VISION_MODEL=${CORE_LLM_PULL_VISION_MODEL}
|
|
OLLAMA_KEEP_ALIVE=${CORE_LLM_KEEP_ALIVE}
|
|
OLLAMA_MAX_LOADED_MODELS=${CORE_LLM_MAX_LOADED_MODELS}
|
|
OLLAMA_NUM_PARALLEL=${CORE_LLM_NUM_PARALLEL}
|
|
EOF
|
|
chmod 600 "$PAYLOAD/llm-host.env"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 2. Static networking — this address is what the container host's OLLAMA_HOST was
|
|
# built to point at, so it cannot be left to DHCP.
|
|
# ---------------------------------------------------------------------------
|
|
cat > "$INCLUDES/etc/network/interfaces.d/smarthome" <<EOF
|
|
# GENERATED from CoreSystemConfig.json. The container host image built alongside this
|
|
# one has ${CORE_OLLAMA_HOST} compiled into its service env files — if this address
|
|
# moves on its own, that host quietly loses inference and (by design) degrades rather
|
|
# than erroring, so the breakage is easy to miss. Change it in CoreSystemConfig.json
|
|
# and rebuild both halves.
|
|
auto lo
|
|
iface lo inet loopback
|
|
|
|
allow-hotplug eth0
|
|
iface eth0 inet static
|
|
address ${CORE_LLM_HOST_IP}
|
|
netmask ${CORE_NETMASK}
|
|
gateway ${CORE_GATEWAY}
|
|
dns-nameservers ${CORE_DNS_SERVERS}
|
|
EOF
|
|
|
|
echo "${CORE_LLM_HOST_NAME}" > "$INCLUDES/etc/hostname"
|
|
cat > "$INCLUDES/etc/hosts" <<EOF
|
|
127.0.0.1 localhost
|
|
127.0.1.1 ${CORE_LLM_HOST_NAME}
|
|
${CORE_LLM_HOST_IP} ${CORE_LLM_HOST_NAME}
|
|
# The twin — recorded for diagnosis, not used. This host never initiates anything
|
|
# toward the container host; it only answers.
|
|
${CORE_CONTAINER_HOST_IP} ${CORE_CONTAINER_HOST_NAME}
|
|
EOF
|
|
|
|
cat > "$INCLUDES/etc/default/keyboard" <<EOF
|
|
XKBMODEL="pc105"
|
|
XKBLAYOUT="${CORE_KEYBOARD_LAYOUT}"
|
|
XKBVARIANT=""
|
|
XKBOPTIONS=""
|
|
BACKSPACE="guess"
|
|
EOF
|
|
|
|
core_write_build_stamp "$INCLUDES/etc/smarthome-build" "llm-host"
|
|
|
|
if [[ -n "$CORE_SSH_AUTHORIZED_KEY" ]]; then
|
|
mkdir -p "$INCLUDES/home/${CORE_LLM_HOST_USER}/.ssh"
|
|
echo "$CORE_SSH_AUTHORIZED_KEY" > "$INCLUDES/home/${CORE_LLM_HOST_USER}/.ssh/authorized_keys"
|
|
chmod 700 "$INCLUDES/home/${CORE_LLM_HOST_USER}/.ssh"
|
|
chmod 600 "$INCLUDES/home/${CORE_LLM_HOST_USER}/.ssh/authorized_keys"
|
|
else
|
|
core_warn "No ssh_authorized_key in the config — this headless host will have no SSH access."
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. First-boot unit. Long timeout on purpose: this pulls several GB of model weights
|
|
# on a first run, and a model download on a slow link genuinely can outlast a
|
|
# conservative systemd timeout.
|
|
# ---------------------------------------------------------------------------
|
|
cat > "$INCLUDES/etc/systemd/system/smarthome-llm-firstboot.service" <<'EOF'
|
|
[Unit]
|
|
Description=SmartestHome first-boot setup (LLM host)
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
ConditionPathExists=!/opt/llm-host/.firstboot-done
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
RemainAfterExit=yes
|
|
EnvironmentFile=/opt/llm-host/llm-host.env
|
|
ExecStart=/opt/llm-host/scripts/setup-llm-host.sh
|
|
ExecStartPost=/usr/bin/touch /opt/llm-host/.firstboot-done
|
|
ExecStartPost=/bin/systemctl disable smarthome-llm-firstboot.service
|
|
StandardOutput=journal+console
|
|
StandardError=journal+console
|
|
# Model pulls are multi-GB; 4h is generous rather than optimistic.
|
|
TimeoutStartSec=14400
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
mkdir -p "$INCLUDES/etc/systemd/system/multi-user.target.wants"
|
|
ln -sf /etc/systemd/system/smarthome-llm-firstboot.service \
|
|
"$INCLUDES/etc/systemd/system/multi-user.target.wants/smarthome-llm-firstboot.service"
|
|
|
|
# firmware-misc-nonfree/nvidia-driver are NOT preinstalled here. Driver choice is the
|
|
# most hardware-specific decision on this machine and picking one blind is how you
|
|
# produce a box that doesn't boot — setup-llm-host.sh checks for a working nvidia-smi
|
|
# and tells you what to install if it's missing. See hosts/llm-host/README.md.
|
|
cat > "$LIVE_BUILD_DIR/config/package-lists/llm-host.list.chroot" <<'EOF'
|
|
ca-certificates
|
|
curl
|
|
gnupg
|
|
openssh-server
|
|
sudo
|
|
python3
|
|
pciutils
|
|
EOF
|
|
|
|
cat > "$LIVE_BUILD_DIR/config/includes.installer/preseed.cfg" <<EOF
|
|
# GENERATED from CoreSystemConfig.json by tools/build-llm-host-iso.sh
|
|
d-i debian-installer/locale string ${CORE_LOCALE}
|
|
d-i keyboard-configuration/xkb-keymap select ${CORE_KEYBOARD_LAYOUT}
|
|
d-i time/zone string ${CORE_TIMEZONE}
|
|
d-i clock-setup/utc boolean true
|
|
|
|
d-i netcfg/choose_interface select auto
|
|
d-i netcfg/disable_autoconfig boolean true
|
|
d-i netcfg/get_ipaddress string ${CORE_LLM_HOST_IP}
|
|
d-i netcfg/get_netmask string ${CORE_NETMASK}
|
|
d-i netcfg/get_gateway string ${CORE_GATEWAY}
|
|
d-i netcfg/get_nameservers string ${CORE_DNS_SERVERS}
|
|
d-i netcfg/confirm_static boolean true
|
|
d-i netcfg/get_hostname string ${CORE_LLM_HOST_NAME}
|
|
d-i netcfg/get_domain string local
|
|
|
|
d-i passwd/root-login boolean false
|
|
d-i passwd/user-fullname string ${CORE_LLM_HOST_USER}
|
|
d-i passwd/username string ${CORE_LLM_HOST_USER}
|
|
$(if [[ -n "$CORE_ADMIN_PASSWORD_HASH" ]]; then
|
|
echo "d-i passwd/user-password-crypted password ${CORE_ADMIN_PASSWORD_HASH}"
|
|
else
|
|
echo "# No admin_password_hash set — the installer will prompt for a password."
|
|
echo "# Generate one with: mkpasswd -m sha-512"
|
|
fi)
|
|
d-i user-setup/allow-password-weak boolean false
|
|
d-i user-setup/encrypt-home boolean false
|
|
|
|
# WHOLE-DISK, AUTOMATIC, NO CONFIRMATION — erases ${CORE_LLM_HOST_DISK} without asking.
|
|
# Model storage wants room: a 14B Q4 model is ~9GB and a vision model another 5-8GB.
|
|
d-i partman-auto/disk string ${CORE_LLM_HOST_DISK}
|
|
d-i partman-auto/method string regular
|
|
d-i partman-auto/choose_recipe select atomic
|
|
d-i partman-partitioning/confirm_write_new_label boolean true
|
|
d-i partman/choose_partition select finish
|
|
d-i partman/confirm boolean true
|
|
d-i partman/confirm_nooverwrite boolean true
|
|
|
|
d-i pkgsel/include string openssh-server sudo curl ca-certificates python3 pciutils
|
|
tasksel tasksel/first multiselect standard, ssh-server
|
|
popularity-contest popularity-contest/participate boolean false
|
|
|
|
d-i grub-installer/only_debian boolean true
|
|
d-i grub-installer/bootdev string ${CORE_LLM_HOST_DISK}
|
|
d-i finish-install/reboot_in_progress note
|
|
EOF
|
|
|
|
cd "$LIVE_BUILD_DIR"
|
|
core_log "Running lb config"
|
|
lb clean --purge >/dev/null 2>&1 || true
|
|
lb config \
|
|
--distribution "$CORE_DEBIAN_RELEASE" \
|
|
--architecture amd64 \
|
|
--binary-images iso-hybrid \
|
|
--debian-installer netinst \
|
|
--debian-installer-gui false \
|
|
--archive-areas "main contrib non-free non-free-firmware" \
|
|
--iso-application "SmartestHome LLM host" \
|
|
--iso-volume "smarthome-llm-$(core_pair_id)"
|
|
|
|
core_log "Running lb build (long, needs network)"
|
|
lb build
|
|
|
|
ISO="$(find "$LIVE_BUILD_DIR" -maxdepth 1 -name 'live-image-amd64.hybrid.iso' -print -quit)"
|
|
[[ -n "$ISO" ]] || core_die "lb build finished but no ISO was produced — check the log above."
|
|
|
|
DEST="${OUTPUT_DIR}/smarthome-llm-host-$(core_pair_id).iso"
|
|
mv "$ISO" "$DEST"
|
|
core_log "LLM host ISO: ${DEST}"
|