#!/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" < "$INCLUDES/etc/network/interfaces.d/smarthome" < "$INCLUDES/etc/hostname" cat > "$INCLUDES/etc/hosts" < "$INCLUDES/etc/default/keyboard" < "$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" </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}"