#!/usr/bin/env bash set -euo pipefail ROOT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) ANSIBLE_VENV="$ROOT_DIR/.venv-ansible" ANSIBLE_PYTHON="$ANSIBLE_VENV/bin/python" ANSIBLE_PLAYBOOK="$ANSIBLE_VENV/bin/ansible-playbook" export ANSIBLE_CONFIG="$ROOT_DIR/ansible/ansible.cfg" run_project_script() { local script_path=$1 shift || true bash "$script_path" "$@" } usage() { cat <<'EOF' Usage: ./labctl up ./labctl down ./labctl update_wiki ./labctl ollama_models ./labctl preflight ./labctl versions ./labctl assets lab2 [--refresh] ./labctl start [core|all|service...] ./labctl stop [all|service...] ./labctl status [all|service...] ./labctl urls ./labctl open kiln ./labctl logs EOF } netron_version() { local version_file=$ROOT_DIR/ansible/group_vars/all.yml if [ ! -f "$version_file" ]; then printf '%s\n' "unknown" return fi sed -nE 's/^courseware_netron_version:[[:space:]]*"([^"]+)".*/\1/p' "$version_file" | head -n 1 } minimum_ollama_version() { local version_file=$ROOT_DIR/ansible/group_vars/all.yml if [ ! -f "$version_file" ]; then printf '%s\n' "unknown" return fi sed -nE 's/^courseware_ollama_min_version:[[:space:]]*"([^"]+)".*/\1/p' "$version_file" | head -n 1 } print_versions() { cat <&2 WARNING: THIS SCRIPT WILL CONFIGURE YOUR ENVIRONMENT WILL THE FOLLOWING SOFTWARE: - Ollama - llama.cpp - Netron (${pinned_netron}) - Open WebUI - ChunkViz - Embedding Atlas - Promptfoo - Unsloth Studio - Kiln Desktop - Course-specific support assets for lab 1, lab 2, and lab 4 - Pre-pulled Gemma 4 E2B Ollama models for Lab 1 and Lab 2 - Lab 1 confidence support through Gemma 4 E2B Q4 (requires Ollama ${min_ollama}+) IT IS RECOMMENDED TO RUN THIS IN AN ISLOATED ENVIRONMENT (Dedicated WSL, VM, etc.) This process may take a long time. This command requires interactive confirmation. Re-run it from a terminal and answer the prompt. EOF exit 1 fi cat </dev/null 2>&1 && nvidia-smi >/dev/null 2>&1; then return fi cat <<'EOF' >&2 WSL GPU support is not ready yet, so the installer is stopping before Ansible runs. This courseware expects WSL to already see your NVIDIA GPU. Please do this on the Windows side first: 1. Install or update the current NVIDIA Windows driver with WSL/CUDA support. 2. Open Windows PowerShell and run: wsl --update 3. Fully restart WSL: wsl --shutdown 4. Reopen your Linux distro and confirm this works: nvidia-smi If `nvidia-smi` still fails inside WSL: - Reboot Windows - Confirm WSL 2 is in use - Confirm the GPU is NVIDIA and the Windows driver is recent After `nvidia-smi` works inside WSL, rerun: bash deploy-courseware.sh EOF exit 1 } linux_cuda_toolkit_is_ready() { command -v nvcc >/dev/null 2>&1 && return 0 [ -x /usr/local/cuda/bin/nvcc ] && return 0 for candidate in /usr/local/cuda-*/bin/nvcc; do if [ -x "$candidate" ]; then return 0 fi done [ -f /usr/local/cuda/include/cuda_runtime.h ] && return 0 [ -f /usr/include/cuda_runtime.h ] && return 0 return 1 } linux_cuda_toolkit_package_is_available() { command -v apt-cache >/dev/null 2>&1 || return 1 apt-cache show nvidia-cuda-toolkit >/dev/null 2>&1 } print_linux_cuda_toolkit_guidance() { local package_hint package_hint="sudo apt update && sudo apt install -y nvidia-cuda-toolkit" if command -v apt-cache >/dev/null 2>&1; then if ! apt-cache show nvidia-cuda-toolkit >/dev/null 2>&1; then package_hint="Your distro does not expose nvidia-cuda-toolkit in its default apt sources, so add NVIDIA's CUDA repository for your Debian/Ubuntu release and install the toolkit from there." fi fi if host_is_wsl; then cat <&2 CUDA Toolkit is still missing inside this WSL Linux environment, so the installer is stopping before Ansible runs. WSL splits GPU support into two layers: - Windows side: the NVIDIA driver makes the GPU visible to WSL - Linux side: llama.cpp still needs the CUDA toolkit headers/compiler inside the distro What to do: 1. Confirm the driver side works in WSL: nvidia-smi 2. Install the Linux-side CUDA toolkit $package_hint 3. Verify the toolkit: nvcc --version ls /usr/local/cuda/include/cuda_runtime.h After that, rerun: bash deploy-courseware.sh EOF return fi cat <&2 CUDA Toolkit is not installed inside this Linux environment, so the installer is stopping before Ansible runs. This courseware can only build CUDA-enabled llama.cpp when the Linux-side toolkit is present. What to do: 1. Install the CUDA toolkit $package_hint 2. Verify the toolkit: nvcc --version ls /usr/local/cuda/include/cuda_runtime.h After that, rerun: bash deploy-courseware.sh EOF } check_linux_cuda_toolkit() { if [ "$(host_profile)" != "native-debian-ubuntu" ]; then return fi if linux_cuda_toolkit_is_ready; then return fi if linux_cuda_toolkit_package_is_available; then return fi print_linux_cuda_toolkit_guidance exit 1 } resolve_python() { if [ -n "${PYTHON_BIN:-}" ] && command -v "${PYTHON_BIN}" >/dev/null 2>&1; then command -v "${PYTHON_BIN}" return fi if command -v python3 >/dev/null 2>&1; then command -v python3 return fi if command -v python >/dev/null 2>&1; then command -v python return fi cat <<'EOF' >&2 Python 3 was not found. Install it first, then rerun this command: - Debian/Ubuntu/WSL: sudo apt update && sudo apt install -y python3 python3-venv - macOS: brew install python@3.11 EOF exit 1 } host_is_debian_family() { [ -f /etc/os-release ] || return 1 grep -qiE '^(ID|ID_LIKE)=(.*debian|debian.*)$' /etc/os-release } python_has_venv_support() { local python_bin=$1 local probe_parent local probe_venv probe_parent=$(mktemp -d 2>/dev/null || mktemp -d -t labctl-venv-probe) probe_venv="$probe_parent/venv" if "$python_bin" -m venv "$probe_venv" >/dev/null 2>&1; then rm -rf "$probe_parent" return 0 fi rm -rf "$probe_parent" return 1 } install_debian_python_venv_support() { local python_bin=$1 local installer=(apt-get) local versioned_venv_pkg local packages=(python3-venv python3-pip) if ! command -v apt-get >/dev/null 2>&1; then return 1 fi if [ "$(id -u)" -ne 0 ]; then if ! command -v sudo >/dev/null 2>&1; then cat <<'EOF' >&2 Python can be found, but this Debian/Ubuntu system is missing the venv bootstrap packages that Ansible needs. Install them, then rerun this command: apt-get update && apt-get install -y python3-venv python3-pip EOF return 1 fi if [ ! -t 0 ]; then cat <<'EOF' >&2 Python can be found, but this Debian/Ubuntu system still needs sudo access to install python3-venv and python3-pip. Run this command from an interactive terminal so sudo can prompt for your password, or install the packages manually: sudo apt-get update && sudo apt-get install -y python3-venv python3-pip EOF return 1 fi echo "This step needs sudo to install missing Python packages. You may be prompted for your password." installer=(sudo apt-get) fi versioned_venv_pkg=$("$python_bin" -c "import sys; print(f'python{sys.version_info.major}.{sys.version_info.minor}-venv')") if command -v apt-cache >/dev/null 2>&1 && apt-cache show "$versioned_venv_pkg" >/dev/null 2>&1; then packages=("$versioned_venv_pkg" "${packages[@]}") fi echo "Installing missing Python venv support for this Debian/Ubuntu system..." "${installer[@]}" update "${installer[@]}" install -y "${packages[@]}" } ansible_venv_is_usable() { if [ ! -x "$ANSIBLE_PYTHON" ]; then return 1 fi if [ ! -x "$ANSIBLE_PLAYBOOK" ]; then return 1 fi "$ANSIBLE_PYTHON" -c "import ansible" >/dev/null 2>&1 || return 1 "$ANSIBLE_PLAYBOOK" --version >/dev/null 2>&1 } rebuild_ansible() { local python_bin python_bin=$(resolve_python) if ! python_has_venv_support "$python_bin"; then if host_is_debian_family; then install_debian_python_venv_support "$python_bin" fi fi if ! python_has_venv_support "$python_bin"; then cat <<'EOF' >&2 Python 3 is installed, but its virtual environment support is still unavailable. Install the missing venv package for your platform, then rerun this command: - Debian/Ubuntu/WSL: sudo apt update && sudo apt install -y python3-venv python3-pip - macOS: brew reinstall python@3.11 EOF exit 1 fi rm -rf "$ANSIBLE_VENV" echo "Preparing local installer runtime..." "$python_bin" -m venv "$ANSIBLE_VENV" "$ANSIBLE_PYTHON" -m pip install --upgrade pip "$ANSIBLE_PYTHON" -m pip install "ansible-core==2.18.6" } ensure_ansible() { if ansible_venv_is_usable; then return fi if [ -e "$ANSIBLE_VENV" ]; then echo "Refreshing installer runtime for this machine..." fi rebuild_ansible } sudo_keepalive_pid="" ensure_sudo_session() { if [ "$(uname -s)" != "Linux" ]; then return fi if [ "$(id -u)" -eq 0 ]; then return fi if ! command -v sudo >/dev/null 2>&1; then cat <<'EOF' >&2 This installer needs sudo for Linux package setup, but `sudo` is not installed. Install sudo or rerun this command as root, then try again. EOF exit 1 fi if sudo -n true >/dev/null 2>&1; then return fi if [ ! -t 0 ]; then cat <<'EOF' >&2 This installer needs sudo for Linux package setup. Run this command from an interactive terminal so sudo can prompt for your password, then rerun the same `./labctl` command. EOF exit 1 fi echo "This step needs sudo for Linux package setup. You may be prompted for your password." sudo -v } start_sudo_keepalive() { if [ "$(uname -s)" != "Linux" ]; then return fi if [ "$(id -u)" -eq 0 ]; then return fi if ! command -v sudo >/dev/null 2>&1; then return fi ( while true; do sudo -n true >/dev/null 2>&1 || exit 0 sleep 60 done ) & sudo_keepalive_pid=$! } stop_sudo_keepalive() { if [ -n "${sudo_keepalive_pid:-}" ]; then kill "$sudo_keepalive_pid" >/dev/null 2>&1 || true wait "$sudo_keepalive_pid" >/dev/null 2>&1 || true sudo_keepalive_pid="" fi } run_playbook() { local playbook=$1 local escaped_root shift check_wsl_gpu_readiness check_linux_cuda_toolkit ensure_ansible ensure_sudo_session if [ ! -x "$ANSIBLE_PLAYBOOK" ]; then echo "Installer runtime is incomplete. Rebuilding it..." rebuild_ansible fi escaped_root=${ROOT_DIR//\\/\\\\} escaped_root=${escaped_root//\"/\\\"} start_sudo_keepalive trap stop_sudo_keepalive RETURN "$ANSIBLE_PLAYBOOK" \ -e "{\"courseware_root\":\"$escaped_root\"}" \ "$ROOT_DIR/ansible/playbooks/$playbook" \ "$@" } require_arg() { if [ $# -lt 1 ]; then usage exit 1 fi } require_managed_runtime() { if [ ! -f "$ROOT_DIR/state/runtime.env" ]; then cat <<'EOF' >&2 Missing state/runtime.env. Run ./labctl up first so the managed environment exists before using this command. EOF exit 1 fi } handle_assets_command() { local asset_group=${1:-} shift || true case "$asset_group" in lab2) exec bash "$ROOT_DIR/scripts/bootstrap_lab2_assets.sh" "$@" ;; *) usage exit 1 ;; esac } refresh_ollama_models() { require_managed_runtime run_playbook up.yml --tags ollama_models } update_wiki() { require_managed_runtime run_playbook up.yml --tags wiki -e "courseware_wiki_force_update=true" run_project_script "$ROOT_DIR/scripts/service_manager.sh" restart-wiki } main() { local cmd=${1:-} shift || true case "$cmd" in up) confirm_installation run_playbook up.yml run_project_script "$ROOT_DIR/scripts/service_manager.sh" start all ;; ollama_models) refresh_ollama_models ;; update_wiki) update_wiki ;; down) run_project_script "$ROOT_DIR/scripts/service_manager.sh" stop all || true run_playbook down.yml ;; preflight) confirm_installation run_playbook up.yml --tags preflight ;; versions) print_versions ;; assets) require_arg "$@" handle_assets_command "$@" ;; start|stop|status|urls|open|logs) exec bash "$ROOT_DIR/scripts/service_manager.sh" "$cmd" "$@" ;; ""|-h|--help|help) usage ;; *) usage exit 1 ;; esac } main "$@"