1293 lines
55 KiB
Bash
1293 lines
55 KiB
Bash
#!/bin/bash
|
|
|
|
# Project NOMAD Installation Script
|
|
|
|
###################################################################################################################################################################################################
|
|
|
|
# Script | Project NOMAD Installation Script
|
|
# Version | 1.0.0
|
|
# Author | Crosstalk Solutions, LLC
|
|
# Website | https://crosstalksolutions.com
|
|
|
|
###################################################################################################################################################################################################
|
|
# #
|
|
# Color Codes #
|
|
# #
|
|
###################################################################################################################################################################################################
|
|
|
|
RESET='\033[0m'
|
|
YELLOW='\033[1;33m'
|
|
WHITE_R='\033[39m' # Same as GRAY_R for terminals with white background.
|
|
GRAY_R='\033[39m'
|
|
RED='\033[1;31m' # Light Red.
|
|
GREEN='\033[1;32m' # Light Green.
|
|
|
|
###################################################################################################################################################################################################
|
|
# #
|
|
# Constants & Variables #
|
|
# #
|
|
###################################################################################################################################################################################################
|
|
|
|
WHIPTAIL_TITLE="Project NOMAD Installation"
|
|
NOMAD_DIR="/opt/project-nomad"
|
|
MANAGEMENT_COMPOSE_FILE_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/management_compose.yaml"
|
|
START_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/start_nomad.sh"
|
|
STOP_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/stop_nomad.sh"
|
|
UPDATE_SCRIPT_URL="https://raw.githubusercontent.com/Crosstalk-Solutions/project-nomad/refs/heads/main/install/update_nomad.sh"
|
|
script_option_debug='true'
|
|
accepted_terms='false'
|
|
local_ip_address=''
|
|
has_lan_address='false'
|
|
|
|
# Offline artifact mode. When an artifact bundle is selected (--artifacts or
|
|
# NOMAD_ARTIFACT_PATH) every dependency — host packages, Docker images, helper
|
|
# scripts and the management compose file — is taken from that bundle, and the
|
|
# installer never falls back to the network. See admin/docs/offline-install.md.
|
|
SUPPORTED_BUNDLE_FORMAT_VERSION='1'
|
|
NOMAD_ARTIFACT_PATH="${NOMAD_ARTIFACT_PATH:-}"
|
|
artifact_mode='false'
|
|
existing_install='false'
|
|
existing_app_key=''
|
|
existing_db_password=''
|
|
existing_db_root_password=''
|
|
existing_url=''
|
|
artifact_manifest_file=''
|
|
artifact_apt_repo_dir=''
|
|
artifact_image_dir=''
|
|
artifact_payload_dir=''
|
|
|
|
###################################################################################################################################################################################################
|
|
# #
|
|
# Functions #
|
|
# #
|
|
###################################################################################################################################################################################################
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Project NOMAD Installation Script
|
|
|
|
Usage:
|
|
sudo bash $(basename "$0") [options]
|
|
|
|
Options:
|
|
--artifacts PATH Install from a local offline artifact bundle instead of
|
|
downloading dependencies from the internet. Bundles are
|
|
produced on a connected machine by
|
|
install/build_offline_bundle.sh and are specific to one
|
|
OS / version / architecture.
|
|
-h, --help Show this help text and exit.
|
|
|
|
Environment:
|
|
NOMAD_ARTIFACT_PATH Equivalent to --artifacts. The command line option wins
|
|
when both are supplied.
|
|
|
|
With no artifact path the installer behaves exactly as before and downloads its
|
|
dependencies from the internet.
|
|
EOF
|
|
}
|
|
|
|
parse_installer_args() {
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--artifacts)
|
|
if [[ $# -lt 2 || -z "$2" ]]; then
|
|
echo -e "${RED}#${RESET} --artifacts requires the path to an offline artifact bundle."
|
|
exit 1
|
|
fi
|
|
NOMAD_ARTIFACT_PATH="$2"
|
|
shift
|
|
;;
|
|
--artifacts=*)
|
|
NOMAD_ARTIFACT_PATH="${1#*=}"
|
|
if [[ -z "${NOMAD_ARTIFACT_PATH}" ]]; then
|
|
echo -e "${RED}#${RESET} --artifacts requires the path to an offline artifact bundle."
|
|
exit 1
|
|
fi
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo -e "${RED}#${RESET} Unknown option: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
if [[ -n "${NOMAD_ARTIFACT_PATH}" ]]; then
|
|
local resolved_path
|
|
if ! resolved_path="$(cd -- "${NOMAD_ARTIFACT_PATH}" 2>/dev/null && pwd)"; then
|
|
echo -e "${RED}#${RESET} Offline artifact bundle directory not found: ${NOMAD_ARTIFACT_PATH}"
|
|
exit 1
|
|
fi
|
|
NOMAD_ARTIFACT_PATH="${resolved_path}"
|
|
artifact_mode='true'
|
|
artifact_manifest_file="${NOMAD_ARTIFACT_PATH}/manifest"
|
|
artifact_apt_repo_dir="${NOMAD_ARTIFACT_PATH}/packages/apt"
|
|
artifact_image_dir="${NOMAD_ARTIFACT_PATH}/images"
|
|
artifact_payload_dir="${NOMAD_ARTIFACT_PATH}/payload/nomad"
|
|
fi
|
|
}
|
|
|
|
artifact_mode_enabled() {
|
|
[[ "${artifact_mode}" == 'true' ]]
|
|
}
|
|
|
|
artifact_manifest_get() {
|
|
# The manifest is data, not shell. Read individual keys instead of sourcing it
|
|
# so a malformed or hostile bundle cannot execute code on the target.
|
|
local key="$1"
|
|
awk -F= -v wanted="${key}" '$1 == wanted { sub(/^[^=]*=/, "", $0); print; exit }' "${artifact_manifest_file}"
|
|
}
|
|
|
|
normalize_arch() {
|
|
case "$1" in
|
|
x86_64|amd64) echo 'amd64' ;;
|
|
aarch64|arm64) echo 'arm64' ;;
|
|
*) echo "$1" ;;
|
|
esac
|
|
}
|
|
|
|
os_release_get() {
|
|
local key="$1"
|
|
[[ -r /etc/os-release ]] || return 0
|
|
awk -F= -v wanted="${key}" '$1 == wanted { gsub(/^"|"$/, "", $2); print $2; exit }' /etc/os-release
|
|
}
|
|
|
|
validate_artifact_bundle() {
|
|
echo -e "${YELLOW}#${RESET} Offline artifact mode selected: ${NOMAD_ARTIFACT_PATH}\\n"
|
|
|
|
if ! command -v sha256sum &> /dev/null; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} sha256sum is required to verify an offline artifact bundle but was not found."
|
|
exit 1
|
|
fi
|
|
|
|
# Distinguish "not a bundle at all" from "a damaged bundle". Pointing
|
|
# --artifacts at a source checkout instead of a built bundle is an easy
|
|
# mistake to make, and deserves a better answer than a missing-file list.
|
|
if [[ ! -f "${artifact_manifest_file}" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} ${NOMAD_ARTIFACT_PATH} is not an offline artifact bundle (it has no 'manifest' file).\\n"
|
|
echo -e "${RED}#${RESET} --artifacts must point at a bundle directory, not at a Project NOMAD source"
|
|
echo -e "${RED}#${RESET} checkout. Bundles are named project-nomad-offline-<os>-<version>-<arch>-<commit>"
|
|
echo -e "${RED}#${RESET} and contain a manifest, SHA256SUMS, packages/, images/ and payload/.\\n"
|
|
echo -e "${RED}#${RESET} Build one on an internet-connected machine:"
|
|
echo -e "${RED}#${RESET} ./install/build_offline_bundle.sh --target ubuntu:26.04 --output <output-dir>\\n"
|
|
echo -e "${RED}#${RESET} Then copy the resulting directory to this machine, cd into it, and run:"
|
|
echo -e "${RED}#${RESET} sudo bash ./install_nomad.sh --artifacts ."
|
|
exit 1
|
|
fi
|
|
|
|
# Every one of these is required for a complete offline install. A bundle that
|
|
# is missing any of them must fail here rather than part-way through, because
|
|
# artifact mode has no network fallback to fill the gap.
|
|
local required_files=(
|
|
"${NOMAD_ARTIFACT_PATH}/SHA256SUMS"
|
|
"${artifact_apt_repo_dir}/Packages"
|
|
"${artifact_apt_repo_dir}/Packages.gz"
|
|
"${artifact_image_dir}/core-images.txt"
|
|
"${artifact_image_dir}/core-images.tar"
|
|
"${artifact_payload_dir}/management_compose.yaml"
|
|
"${artifact_payload_dir}/compose.artifact.yml"
|
|
"${artifact_payload_dir}/start_nomad.sh"
|
|
"${artifact_payload_dir}/stop_nomad.sh"
|
|
"${artifact_payload_dir}/update_nomad.sh"
|
|
)
|
|
local required_file
|
|
for required_file in "${required_files[@]}"; do
|
|
if [[ ! -f "${required_file}" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} The offline artifact bundle is incomplete. Missing: ${required_file}"
|
|
echo -e "${RED}#${RESET} Rebuild the bundle with install/build_offline_bundle.sh on a connected machine and try again."
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Transfer integrity only. These checksums prove the bundle arrived intact;
|
|
# they are not a publisher signature and do not prove who produced it.
|
|
echo -e "${YELLOW}#${RESET} Verifying artifact bundle checksums...\\n"
|
|
if ! (cd "${NOMAD_ARTIFACT_PATH}" && sha256sum -c SHA256SUMS > /dev/null); then
|
|
header_red
|
|
echo -e "${RED}#${RESET} The offline artifact bundle failed checksum verification."
|
|
echo -e "${RED}#${RESET} It is corrupt or incomplete — copy it again from the build machine and retry."
|
|
exit 1
|
|
fi
|
|
|
|
local bundle_format target_os target_version target_arch
|
|
bundle_format="$(artifact_manifest_get BUNDLE_FORMAT_VERSION)"
|
|
target_os="$(artifact_manifest_get TARGET_OS)"
|
|
target_version="$(artifact_manifest_get TARGET_VERSION)"
|
|
target_arch="$(artifact_manifest_get TARGET_ARCH)"
|
|
|
|
if [[ "${bundle_format}" != "${SUPPORTED_BUNDLE_FORMAT_VERSION}" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Unsupported artifact bundle format '${bundle_format:-unknown}'."
|
|
echo -e "${RED}#${RESET} This installer supports bundle format ${SUPPORTED_BUNDLE_FORMAT_VERSION}."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${target_os}" || -z "${target_version}" || -z "${target_arch}" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} The artifact manifest is incomplete — TARGET_OS, TARGET_VERSION and TARGET_ARCH are all required."
|
|
exit 1
|
|
fi
|
|
|
|
# Bundles carry a dependency closure resolved for one exact distribution and
|
|
# architecture, so a mismatch is an error rather than a warning.
|
|
local host_os host_version host_arch
|
|
host_os="$(os_release_get ID)"
|
|
host_version="$(os_release_get VERSION_ID)"
|
|
host_arch="$(normalize_arch "$(dpkg --print-architecture 2>/dev/null || uname -m)")"
|
|
|
|
if [[ "${host_os}" != "${target_os}" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} This bundle targets ${target_os}, but this host is ${host_os:-unknown}."
|
|
echo -e "${RED}#${RESET} Build a bundle for ${host_os:-this host} and try again."
|
|
exit 1
|
|
fi
|
|
if [[ "${host_version}" != "${target_version}" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} This bundle targets ${target_os} ${target_version}, but this host runs ${host_version:-unknown}."
|
|
echo -e "${RED}#${RESET} Bundles are specific to one OS version because they carry a resolved package set."
|
|
exit 1
|
|
fi
|
|
if [[ "${host_arch}" != "${target_arch}" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} This bundle targets ${target_arch}, but this host is ${host_arch:-unknown}."
|
|
exit 1
|
|
fi
|
|
|
|
echo -e "${GREEN}#${RESET} Artifact bundle validated (NOMAD commit $(artifact_manifest_get NOMAD_COMMIT), built $(artifact_manifest_get CREATED_AT_UTC)).\\n"
|
|
}
|
|
|
|
header() {
|
|
if [[ "${script_option_debug}" != 'true' ]]; then clear; clear; fi
|
|
echo -e "${GREEN}#########################################################################${RESET}\\n"
|
|
}
|
|
|
|
header_red() {
|
|
if [[ "${script_option_debug}" != 'true' ]]; then clear; clear; fi
|
|
echo -e "${RED}#########################################################################${RESET}\\n"
|
|
}
|
|
|
|
check_has_sudo() {
|
|
if sudo -n true 2>/dev/null; then
|
|
echo -e "${GREEN}#${RESET} User has sudo permissions.\\n"
|
|
else
|
|
echo "User does not have sudo permissions"
|
|
header_red
|
|
echo -e "${RED}#${RESET} This script requires sudo permissions to run. Please run the script with sudo.\\n"
|
|
echo -e "${RED}#${RESET} For example: sudo bash $(basename "$0")"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
check_is_bash() {
|
|
if [[ -z "$BASH_VERSION" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} This script requires bash to run. Please run the script using bash.\\n"
|
|
echo -e "${RED}#${RESET} For example: bash $(basename "$0")"
|
|
exit 1
|
|
fi
|
|
echo -e "${GREEN}#${RESET} This script is running in bash.\\n"
|
|
}
|
|
|
|
check_is_debian_based() {
|
|
if [[ ! -f /etc/debian_version ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} This script is designed to run on Debian-based systems only.\\n"
|
|
echo -e "${RED}#${RESET} Please run this script on a Debian-based system and try again."
|
|
exit 1
|
|
fi
|
|
echo -e "${GREEN}#${RESET} This script is running on a Debian-based system.\\n"
|
|
}
|
|
|
|
check_is_x86_64() {
|
|
local arch
|
|
arch="$(uname -m)"
|
|
if [[ "${arch}" != "x86_64" && "${arch}" != "amd64" ]]; then
|
|
echo -e "${YELLOW}#${RESET} WARNING: Detected architecture '${arch}'. NOMAD officially supports x86_64 only.\\n"
|
|
echo -e "${YELLOW}#${RESET} ARM64/aarch64 support is tracked in PR #419 and is not yet ready.\\n"
|
|
echo -e "${YELLOW}#${RESET} Continuing on an unsupported architecture will likely fail and may leave\\n"
|
|
echo -e "${YELLOW}#${RESET} partial Docker images and files behind that you'll need to clean up manually.\\n"
|
|
echo -e "${YELLOW}#${RESET} Continuing in 10 seconds... press Ctrl+C now to abort.\\n"
|
|
sleep 10
|
|
return
|
|
fi
|
|
echo -e "${GREEN}#${RESET} Architecture check passed (${arch}).\\n"
|
|
}
|
|
|
|
ensure_dependencies_installed() {
|
|
local missing_deps=()
|
|
|
|
# Check for curl
|
|
if ! command -v curl &> /dev/null; then
|
|
missing_deps+=("curl")
|
|
fi
|
|
|
|
# Check for gpg (required for NVIDIA container toolkit keyring)
|
|
if ! command -v gpg &> /dev/null; then
|
|
missing_deps+=("gpg")
|
|
fi
|
|
|
|
# Check for whiptail (used for dialogs, though not currently active)
|
|
# if ! command -v whiptail &> /dev/null; then
|
|
# missing_deps+=("whiptail")
|
|
# fi
|
|
|
|
if [[ ${#missing_deps[@]} -gt 0 ]]; then
|
|
echo -e "${YELLOW}#${RESET} Installing required dependencies: ${missing_deps[*]}...\\n"
|
|
sudo apt-get update
|
|
sudo apt-get install -y "${missing_deps[@]}"
|
|
|
|
# Verify installation
|
|
for dep in "${missing_deps[@]}"; do
|
|
if ! command -v "$dep" &> /dev/null; then
|
|
echo -e "${RED}#${RESET} Failed to install $dep. Please install it manually and try again."
|
|
exit 1
|
|
fi
|
|
done
|
|
echo -e "${GREEN}#${RESET} Dependencies installed successfully.\\n"
|
|
else
|
|
echo -e "${GREEN}#${RESET} All required dependencies are already installed.\\n"
|
|
fi
|
|
}
|
|
|
|
check_is_debug_mode(){
|
|
# Check if the script is being run in debug mode
|
|
if [[ "${script_option_debug}" == 'true' ]]; then
|
|
echo -e "${YELLOW}#${RESET} Debug mode is enabled, the script will not clear the screen...\\n"
|
|
else
|
|
clear; clear
|
|
fi
|
|
}
|
|
|
|
generateRandomPass() {
|
|
local length="${1:-32}" # Default to 32
|
|
local password
|
|
|
|
# Generate random password using /dev/urandom
|
|
password=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length")
|
|
|
|
echo "$password"
|
|
}
|
|
|
|
ensure_docker_installed() {
|
|
if ! command -v docker &> /dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Docker not found. Installing Docker...\\n"
|
|
|
|
# Update package database
|
|
sudo apt-get update
|
|
|
|
# Install prerequisites
|
|
sudo apt-get install -y ca-certificates curl
|
|
|
|
# Create directory for keyrings
|
|
# sudo install -m 0755 -d /etc/apt/keyrings
|
|
|
|
# # Download Docker's official GPG key
|
|
# sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
|
# sudo chmod a+r /etc/apt/keyrings/docker.asc
|
|
|
|
# # Add the repository to Apt sources
|
|
# echo \
|
|
# "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
|
|
# $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
|
|
# sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
|
|
|
# # Update the package database with the Docker packages from the newly added repo
|
|
# sudo apt-get update
|
|
|
|
# # Install Docker packages
|
|
# sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
|
|
|
# Download the Docker convenience script
|
|
curl -fsSL https://get.docker.com -o get-docker.sh
|
|
|
|
# Run the Docker installation script
|
|
sudo sh get-docker.sh
|
|
|
|
# Check if Docker was installed successfully
|
|
if ! command -v docker &> /dev/null; then
|
|
echo -e "${RED}#${RESET} Docker installation failed. Please check the logs and try again."
|
|
exit 1
|
|
fi
|
|
|
|
echo -e "${GREEN}#${RESET} Docker installation completed.\\n"
|
|
else
|
|
echo -e "${GREEN}#${RESET} Docker is already installed.\\n"
|
|
|
|
# Check if Docker service is running
|
|
if ! systemctl is-active --quiet docker; then
|
|
echo -e "${YELLOW}#${RESET} Docker is installed but not running. Attempting to start Docker...\\n"
|
|
sudo systemctl start docker
|
|
if ! systemctl is-active --quiet docker; then
|
|
echo -e "${RED}#${RESET} Failed to start Docker. Please check the Docker service status and try again."
|
|
exit 1
|
|
else
|
|
echo -e "${GREEN}#${RESET} Docker service started successfully.\\n"
|
|
fi
|
|
else
|
|
echo -e "${GREEN}#${RESET} Docker service is already running.\\n"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
install_packages_from_artifacts() {
|
|
# Artifact-mode replacement for ensure_dependencies_installed + ensure_docker_installed.
|
|
# Everything comes from the bundle's flat APT repository; the host's configured
|
|
# remote repositories take no part in dependency resolution.
|
|
echo -e "${YELLOW}#${RESET} Installing host dependencies from the offline artifact bundle...\\n"
|
|
|
|
local apt_root
|
|
if ! apt_root="$(mktemp -d /tmp/nomad-artifact-apt.XXXXXX)"; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Failed to create a temporary directory for the local APT repository."
|
|
exit 1
|
|
fi
|
|
|
|
# Removable media is routinely mounted under a path containing spaces, which
|
|
# the APT "file:" source syntax cannot express. Reach the bundle repository
|
|
# through a whitespace-free symlink instead.
|
|
if ! ln -s "${artifact_apt_repo_dir}" "${apt_root}/repo"; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Failed to link the local APT repository at ${artifact_apt_repo_dir}."
|
|
exit 1
|
|
fi
|
|
mkdir -p "${apt_root}/lists/partial"
|
|
echo "deb [trusted=yes] file:${apt_root}/repo ./" > "${apt_root}/sources.list"
|
|
|
|
# Isolation: only the bundle repository is visible (sourcelist), /etc/apt/sources.list.d
|
|
# is excluded (sourceparts=-), list state is kept out of the host's (Dir::State::Lists),
|
|
# and APT is told not to retry fetches. A dependency that is missing from the
|
|
# bundle therefore fails the install instead of being pulled from the internet.
|
|
local apt_opts=(
|
|
-o "Dir::Etc::sourcelist=${apt_root}/sources.list"
|
|
-o "Dir::Etc::sourceparts=-"
|
|
-o "Dir::State::Lists=${apt_root}/lists"
|
|
-o "APT::Get::List-Cleanup=0"
|
|
-o "Acquire::Languages=none"
|
|
-o "Acquire::Retries=0"
|
|
)
|
|
|
|
# Mirrors what the online path installs: the dependencies checked by
|
|
# ensure_dependencies_installed, the Docker packages the convenience script
|
|
# would install, and the host utilities used later by verify_gpu_setup.
|
|
local packages=(
|
|
ca-certificates
|
|
curl
|
|
gnupg
|
|
jq
|
|
pciutils
|
|
docker-ce
|
|
docker-ce-cli
|
|
containerd.io
|
|
docker-buildx-plugin
|
|
docker-compose-plugin
|
|
)
|
|
if [[ "$(artifact_manifest_get WITH_NVIDIA_TOOLKIT)" == '1' ]]; then
|
|
packages+=(nvidia-container-toolkit)
|
|
fi
|
|
|
|
if ! sudo apt-get "${apt_opts[@]}" update; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Failed to read the offline APT repository at ${artifact_apt_repo_dir}."
|
|
rm -rf "${apt_root}"
|
|
exit 1
|
|
fi
|
|
|
|
if ! sudo DEBIAN_FRONTEND=noninteractive apt-get "${apt_opts[@]}" install -y --no-install-recommends "${packages[@]}"; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Failed to install host dependencies from the offline artifact bundle."
|
|
echo -e "${RED}#${RESET} The bundle is missing one or more packages required by this system. Rebuild it"
|
|
echo -e "${RED}#${RESET} on a connected machine for $(artifact_manifest_get TARGET_OS) $(artifact_manifest_get TARGET_VERSION) and try again."
|
|
rm -rf "${apt_root}"
|
|
exit 1
|
|
fi
|
|
|
|
rm -rf "${apt_root}"
|
|
|
|
if ! command -v docker &> /dev/null; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Docker is still not available after installing packages from the bundle."
|
|
exit 1
|
|
fi
|
|
|
|
if ! systemctl is-active --quiet docker; then
|
|
echo -e "${YELLOW}#${RESET} Docker is installed but not running. Attempting to start Docker...\\n"
|
|
sudo systemctl enable --now docker
|
|
if ! systemctl is-active --quiet docker; then
|
|
echo -e "${RED}#${RESET} Failed to start Docker. Please check the Docker service status and try again."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
echo -e "${GREEN}#${RESET} Host dependencies installed from the offline artifact bundle.\\n"
|
|
}
|
|
|
|
check_docker_compose() {
|
|
# Check if 'docker compose' (v2 plugin) is available
|
|
if ! docker compose version &>/dev/null; then
|
|
echo -e "${RED}#${RESET} Docker Compose v2 is not installed or not available as a Docker plugin."
|
|
echo -e "${YELLOW}#${RESET} This script requires 'docker compose' (v2), not 'docker-compose' (v1)."
|
|
echo -e "${YELLOW}#${RESET} Please read the Docker documentation at https://docs.docker.com/compose/install/ for instructions on how to install Docker Compose v2."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
detect_nvidia_gpu() {
|
|
# Shared hardware detection for the online and artifact toolkit paths.
|
|
# Returns 0 when an NVIDIA GPU is present.
|
|
|
|
# Safely detect NVIDIA GPU
|
|
local has_nvidia_gpu=false
|
|
if command -v lspci &> /dev/null; then
|
|
if lspci 2>/dev/null | grep -i nvidia &> /dev/null; then
|
|
has_nvidia_gpu=true
|
|
echo -e "${GREEN}#${RESET} NVIDIA GPU detected.\\n"
|
|
fi
|
|
fi
|
|
|
|
# Also check for nvidia-smi
|
|
if ! $has_nvidia_gpu && command -v nvidia-smi &> /dev/null; then
|
|
if nvidia-smi &> /dev/null; then
|
|
has_nvidia_gpu=true
|
|
echo -e "${GREEN}#${RESET} NVIDIA GPU detected via nvidia-smi.\\n"
|
|
fi
|
|
fi
|
|
|
|
$has_nvidia_gpu
|
|
}
|
|
|
|
setup_nvidia_container_toolkit_from_artifacts() {
|
|
# Artifact-mode counterpart of setup_nvidia_container_toolkit. Same non-blocking
|
|
# philosophy: warn and continue rather than fail the install. The toolkit, when
|
|
# present, was installed from the bundle by install_packages_from_artifacts —
|
|
# the NVIDIA package repository is never contacted.
|
|
|
|
echo -e "${YELLOW}#${RESET} Checking for NVIDIA GPU...\\n"
|
|
|
|
if ! detect_nvidia_gpu; then
|
|
echo -e "${YELLOW}#${RESET} No NVIDIA GPU detected. Skipping NVIDIA container toolkit configuration.\\n"
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v nvidia-ctk &> /dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Warning: an NVIDIA GPU is present but this bundle does not include the NVIDIA"
|
|
echo -e "${YELLOW}#${RESET} Container Toolkit. Rebuild the bundle without --without-nvidia-toolkit to add it.\\n"
|
|
echo -e "${YELLOW}#${RESET} Continuing without NVIDIA container acceleration.\\n"
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v nvidia-smi &> /dev/null || ! nvidia-smi &> /dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Warning: the NVIDIA Container Toolkit is installed but no working host NVIDIA"
|
|
echo -e "${YELLOW}#${RESET} driver was detected. Host GPU drivers are outside the scope of offline bundles"
|
|
echo -e "${YELLOW}#${RESET} and must be installed separately. Continuing without GPU acceleration.\\n"
|
|
return 0
|
|
fi
|
|
|
|
echo -e "${YELLOW}#${RESET} Configuring Docker to use NVIDIA runtime...\\n"
|
|
if ! sudo nvidia-ctk runtime configure --runtime=docker 2>/dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Warning: nvidia-ctk runtime configuration failed. GPU support may require manual setup.\\n"
|
|
return 0
|
|
fi
|
|
|
|
echo -e "${YELLOW}#${RESET} Restarting Docker service...\\n"
|
|
if ! sudo systemctl restart docker 2>/dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Warning: Failed to restart Docker service. You may need to restart it manually.\\n"
|
|
return 0
|
|
fi
|
|
|
|
echo -e "${GREEN}#${RESET} NVIDIA container toolkit configuration completed.\\n"
|
|
}
|
|
|
|
setup_nvidia_container_toolkit() {
|
|
# This function attempts to set up NVIDIA GPU support but is non-blocking
|
|
# Any failures will result in warnings but will NOT stop the installation process
|
|
|
|
echo -e "${YELLOW}#${RESET} Checking for NVIDIA GPU...\\n"
|
|
|
|
if ! detect_nvidia_gpu; then
|
|
echo -e "${YELLOW}#${RESET} No NVIDIA GPU detected. Skipping NVIDIA container toolkit installation.\\n"
|
|
return 0
|
|
fi
|
|
|
|
# Check if nvidia-container-toolkit is already installed
|
|
if command -v nvidia-ctk &> /dev/null; then
|
|
echo -e "${GREEN}#${RESET} NVIDIA container toolkit is already installed.\\n"
|
|
return 0
|
|
fi
|
|
|
|
echo -e "${YELLOW}#${RESET} Installing NVIDIA container toolkit...\\n"
|
|
|
|
# Install dependencies per https://docs.ollama.com/docker - wrapped in error handling
|
|
if ! curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey 2>/dev/null | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg 2>/dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Warning: Failed to add NVIDIA container toolkit GPG key. Continuing anyway...\\n"
|
|
return 0
|
|
fi
|
|
|
|
if ! curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list 2>/dev/null \
|
|
| sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
|
|
| sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list > /dev/null 2>&1; then
|
|
echo -e "${YELLOW}#${RESET} Warning: Failed to add NVIDIA container toolkit repository. Continuing anyway...\\n"
|
|
return 0
|
|
fi
|
|
|
|
if ! sudo apt-get update 2>/dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Warning: Failed to update package list. Continuing anyway...\\n"
|
|
return 0
|
|
fi
|
|
|
|
if ! sudo apt-get install -y nvidia-container-toolkit 2>/dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Warning: Failed to install NVIDIA container toolkit. Continuing anyway...\\n"
|
|
return 0
|
|
fi
|
|
|
|
echo -e "${GREEN}#${RESET} NVIDIA container toolkit installed successfully.\\n"
|
|
|
|
# Configure Docker to use NVIDIA runtime
|
|
echo -e "${YELLOW}#${RESET} Configuring Docker to use NVIDIA runtime...\\n"
|
|
|
|
if ! sudo nvidia-ctk runtime configure --runtime=docker 2>/dev/null; then
|
|
echo -e "${YELLOW}#${RESET} nvidia-ctk configure failed, attempting manual configuration...\\n"
|
|
|
|
# Fallback: Manually configure daemon.json
|
|
local daemon_json="/etc/docker/daemon.json"
|
|
local config_success=false
|
|
|
|
if [[ -f "$daemon_json" ]]; then
|
|
# Backup existing config (best effort)
|
|
sudo cp "$daemon_json" "${daemon_json}.backup" 2>/dev/null || true
|
|
|
|
# Check if nvidia runtime already exists
|
|
if ! grep -q '"nvidia"' "$daemon_json" 2>/dev/null; then
|
|
# Add nvidia runtime to existing config using jq if available
|
|
if command -v jq &> /dev/null; then
|
|
if sudo jq '. + {"runtimes": {"nvidia": {"path": "nvidia-container-runtime", "runtimeArgs": []}}}' "$daemon_json" > /tmp/daemon.json.tmp 2>/dev/null; then
|
|
if sudo mv /tmp/daemon.json.tmp "$daemon_json" 2>/dev/null; then
|
|
config_success=true
|
|
fi
|
|
fi
|
|
# Clean up temp file if move failed
|
|
sudo rm -f /tmp/daemon.json.tmp 2>/dev/null || true
|
|
else
|
|
echo -e "${YELLOW}#${RESET} jq not available, skipping manual daemon.json configuration...\\n"
|
|
fi
|
|
else
|
|
config_success=true # Already configured
|
|
fi
|
|
else
|
|
# Create new daemon.json with nvidia runtime (best effort)
|
|
if echo '{"runtimes":{"nvidia":{"path":"nvidia-container-runtime","runtimeArgs":[]}}}' | sudo tee "$daemon_json" > /dev/null 2>&1; then
|
|
config_success=true
|
|
fi
|
|
fi
|
|
|
|
if ! $config_success; then
|
|
echo -e "${YELLOW}#${RESET} Manual daemon.json configuration unsuccessful. GPU support may require manual setup.\\n"
|
|
fi
|
|
fi
|
|
|
|
# Restart Docker service
|
|
echo -e "${YELLOW}#${RESET} Restarting Docker service...\\n"
|
|
if ! sudo systemctl restart docker 2>/dev/null; then
|
|
echo -e "${YELLOW}#${RESET} Warning: Failed to restart Docker service. You may need to restart it manually.\\n"
|
|
return 0
|
|
fi
|
|
|
|
# Verify NVIDIA runtime is available
|
|
echo -e "${YELLOW}#${RESET} Verifying NVIDIA runtime configuration...\\n"
|
|
sleep 2 # Give Docker a moment to fully restart
|
|
|
|
if docker info 2>/dev/null | grep -q "nvidia"; then
|
|
echo -e "${GREEN}#${RESET} NVIDIA runtime successfully configured and verified.\\n"
|
|
else
|
|
echo -e "${YELLOW}#${RESET} Warning: NVIDIA runtime not detected in Docker info. GPU acceleration may not work.\\n"
|
|
echo -e "${YELLOW}#${RESET} You may need to manually configure /etc/docker/daemon.json and restart Docker.\\n"
|
|
fi
|
|
|
|
echo -e "${GREEN}#${RESET} NVIDIA container toolkit configuration completed.\\n"
|
|
}
|
|
|
|
get_install_confirmation(){
|
|
if artifact_mode_enabled && [[ "${existing_install}" == 'true' ]]; then
|
|
# Re-running artifact mode over an existing install is the supported offline
|
|
# update path: new images and helper scripts are applied, data is kept.
|
|
echo -e "${YELLOW}#${RESET} An existing Project NOMAD installation was found at ${NOMAD_DIR}."
|
|
echo -e "${YELLOW}#${RESET} This will update it from the artifact bundle, keeping your database, installed apps and content."
|
|
echo -e "${YELLOW}#${RESET} Backing up ${NOMAD_DIR} first is still recommended."
|
|
read -p "Are you sure you want to continue? (y/N): " choice
|
|
case "$choice" in
|
|
y|Y )
|
|
echo -e "${GREEN}#${RESET} User chose to continue with the update."
|
|
return 0
|
|
;;
|
|
* )
|
|
echo "User chose not to continue with the update."
|
|
exit 0
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
echo -e "${YELLOW}#${RESET} This script will install Project NOMAD and its dependencies on your machine."
|
|
echo -e "${YELLOW}#${RESET} If you already have Project NOMAD installed with customized config or data, please be aware that running this installation script may overwrite existing files and configurations. It is highly recommended to back up any important data/configs before proceeding."
|
|
read -p "Are you sure you want to continue? (y/N): " choice
|
|
case "$choice" in
|
|
y|Y )
|
|
echo -e "${GREEN}#${RESET} User chose to continue with the installation."
|
|
;;
|
|
* )
|
|
echo "User chose not to continue with the installation."
|
|
exit 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
accept_terms() {
|
|
printf "\n\n"
|
|
echo "License Agreement & Terms of Use"
|
|
echo "__________________________"
|
|
printf "\n\n"
|
|
echo "Project NOMAD is licensed under the Apache License 2.0. The full license can be found at https://www.apache.org/licenses/LICENSE-2.0 or in the LICENSE file of this repository."
|
|
printf "\n"
|
|
echo "By accepting this agreement, you acknowledge that you have read and understood the terms and conditions of the Apache License 2.0 and agree to be bound by them while using Project NOMAD"
|
|
echo -e "\n\n"
|
|
read -p "I have read and accept License Agreement & Terms of Use (y/N)? " choice
|
|
case "$choice" in
|
|
y|Y )
|
|
accepted_terms='true'
|
|
;;
|
|
* )
|
|
echo "License Agreement & Terms of Use not accepted. Installation cannot continue."
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
create_nomad_directory(){
|
|
# Ensure the main installation directory exists
|
|
if [[ ! -d "$NOMAD_DIR" ]]; then
|
|
echo -e "${YELLOW}#${RESET} Creating directory for Project NOMAD at $NOMAD_DIR...\\n"
|
|
sudo mkdir -p "$NOMAD_DIR"
|
|
sudo chown "$(whoami):$(whoami)" "$NOMAD_DIR"
|
|
|
|
echo -e "${GREEN}#${RESET} Directory created successfully.\\n"
|
|
else
|
|
echo -e "${GREEN}#${RESET} Directory $NOMAD_DIR already exists.\\n"
|
|
fi
|
|
|
|
# Also ensure the directory has a /storage/logs/ subdirectory
|
|
sudo mkdir -p "${NOMAD_DIR}/storage/logs"
|
|
|
|
# Create a admin.log file in the logs directory
|
|
sudo touch "${NOMAD_DIR}/storage/logs/admin.log"
|
|
}
|
|
|
|
copy_artifact_payload_file() {
|
|
local source_name="$1"
|
|
local destination="$2"
|
|
|
|
if [[ ! -f "${artifact_payload_dir}/${source_name}" ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Required file missing from the artifact bundle payload: ${source_name}"
|
|
exit 1
|
|
fi
|
|
|
|
if ! cp "${artifact_payload_dir}/${source_name}" "${destination}"; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Failed to copy ${source_name} from the artifact bundle to ${destination}."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
download_management_compose_file() {
|
|
local compose_file_path="${NOMAD_DIR}/compose.yml"
|
|
|
|
echo -e "${YELLOW}#${RESET} Downloading docker-compose file for management...\\n"
|
|
if ! curl -fsSL "$MANAGEMENT_COMPOSE_FILE_URL" -o "$compose_file_path"; then
|
|
echo -e "${RED}#${RESET} Failed to download the docker compose file. Please check the URL and try again."
|
|
exit 1
|
|
fi
|
|
echo -e "${GREEN}#${RESET} Docker compose file downloaded successfully to $compose_file_path.\\n"
|
|
}
|
|
|
|
copy_management_compose_file_from_artifacts() {
|
|
local compose_file_path="${NOMAD_DIR}/compose.yml"
|
|
|
|
echo -e "${YELLOW}#${RESET} Installing docker-compose file for management from the artifact bundle...\\n"
|
|
copy_artifact_payload_file management_compose.yaml "$compose_file_path"
|
|
# Layered at startup to force pull_policy: never for every service, so the
|
|
# canonical compose file keeps its normal pull_policy: always.
|
|
copy_artifact_payload_file compose.artifact.yml "${NOMAD_DIR}/compose.artifact.yml"
|
|
echo -e "${GREEN}#${RESET} Docker compose file installed successfully to $compose_file_path.\\n"
|
|
}
|
|
|
|
compose_env_value() {
|
|
# Reads a "- KEY=value" entry from a compose file. Prints nothing when the key
|
|
# is absent or still holds the placeholder.
|
|
local key="$1" file="$2" value
|
|
[[ -f "$file" ]] || return 0
|
|
value="$(awk -v key="${key}" '
|
|
$0 ~ "^[[:space:]]*-[[:space:]]*" key "=" {
|
|
sub("^[[:space:]]*-[[:space:]]*" key "=", "", $0)
|
|
print
|
|
exit
|
|
}
|
|
' "$file")"
|
|
[[ "$value" != 'replaceme' ]] || return 0
|
|
echo "$value"
|
|
}
|
|
|
|
detect_existing_installation() {
|
|
# Must run before the compose file is replaced, so an update can carry the
|
|
# generated credentials forward instead of orphaning the existing database.
|
|
local compose_file_path="${NOMAD_DIR}/compose.yml"
|
|
[[ -f "$compose_file_path" ]] || return 0
|
|
|
|
existing_app_key="$(compose_env_value APP_KEY "$compose_file_path")"
|
|
existing_db_password="$(compose_env_value MYSQL_PASSWORD "$compose_file_path")"
|
|
existing_db_root_password="$(compose_env_value MYSQL_ROOT_PASSWORD "$compose_file_path")"
|
|
existing_url="$(compose_env_value URL "$compose_file_path")"
|
|
|
|
if [[ -n "$existing_app_key" && -n "$existing_db_password" && -n "$existing_db_root_password" ]]; then
|
|
existing_install='true'
|
|
fi
|
|
}
|
|
|
|
configure_management_compose_file() {
|
|
local compose_file_path="${NOMAD_DIR}/compose.yml"
|
|
local app_key db_root_password db_user_password
|
|
|
|
# Re-running artifact mode over an existing install is an update, not a fresh
|
|
# install: keep the credentials the database was initialised with so its data,
|
|
# installed apps and settings survive. MySQL only applies these passwords on
|
|
# first startup, so regenerating them here is what would force wiping the data
|
|
# directory.
|
|
if artifact_mode_enabled && [[ "${existing_install}" == 'true' ]]; then
|
|
echo -e "${YELLOW}#${RESET} Existing Project NOMAD installation detected — updating in place and keeping your data.\\n"
|
|
app_key="${existing_app_key}"
|
|
db_root_password="${existing_db_root_password}"
|
|
db_user_password="${existing_db_password}"
|
|
else
|
|
app_key=$(generateRandomPass)
|
|
db_root_password=$(generateRandomPass)
|
|
db_user_password=$(generateRandomPass)
|
|
|
|
# If MySQL data directory exists from a previous install attempt, remove it.
|
|
# MySQL only initializes credentials on first startup when the data dir is empty.
|
|
# If stale data exists, MySQL ignores the new passwords above and uses the old ones,
|
|
# causing "Access denied" errors when the admin container tries to connect.
|
|
if [[ -d "${NOMAD_DIR}/mysql" ]]; then
|
|
echo -e "${YELLOW}#${RESET} Removing existing MySQL data directory to ensure credentials match...\\n"
|
|
sudo rm -rf "${NOMAD_DIR}/mysql"
|
|
fi
|
|
fi
|
|
|
|
# Inject dynamic env values into the compose file
|
|
echo -e "${YELLOW}#${RESET} Configuring docker-compose file env variables...\\n"
|
|
if artifact_mode_enabled && [[ -n "${existing_url}" ]]; then
|
|
# Keep the address the instance is already reachable at rather than silently
|
|
# repointing it during an update.
|
|
sed -i "s|URL=replaceme|URL=${existing_url}|g" "$compose_file_path"
|
|
else
|
|
sed -i "s|URL=replaceme|URL=http://${local_ip_address}:8080|g" "$compose_file_path"
|
|
fi
|
|
sed -i "s|APP_KEY=replaceme|APP_KEY=${app_key}|g" "$compose_file_path"
|
|
|
|
sed -i "s|DB_PASSWORD=replaceme|DB_PASSWORD=${db_user_password}|g" "$compose_file_path"
|
|
sed -i "s|MYSQL_ROOT_PASSWORD=replaceme|MYSQL_ROOT_PASSWORD=${db_root_password}|g" "$compose_file_path"
|
|
sed -i "s|MYSQL_PASSWORD=replaceme|MYSQL_PASSWORD=${db_user_password}|g" "$compose_file_path"
|
|
|
|
echo -e "${GREEN}#${RESET} Docker compose file configured successfully.\\n"
|
|
}
|
|
|
|
setup_management_compose_file() {
|
|
# Read the outgoing compose file before it is overwritten.
|
|
detect_existing_installation
|
|
|
|
if artifact_mode_enabled; then
|
|
copy_management_compose_file_from_artifacts
|
|
else
|
|
download_management_compose_file
|
|
fi
|
|
configure_management_compose_file
|
|
}
|
|
|
|
download_helper_scripts() {
|
|
local start_script_path="${NOMAD_DIR}/start_nomad.sh"
|
|
local stop_script_path="${NOMAD_DIR}/stop_nomad.sh"
|
|
local update_script_path="${NOMAD_DIR}/update_nomad.sh"
|
|
|
|
echo -e "${YELLOW}#${RESET} Downloading helper scripts...\\n"
|
|
if ! curl -fsSL --retry 5 --retry-delay 3 "$START_SCRIPT_URL" -o "$start_script_path"; then
|
|
echo -e "${RED}#${RESET} Failed to download the start script. Please check the URL and try again."
|
|
exit 1
|
|
fi
|
|
chmod +x "$start_script_path"
|
|
|
|
if ! curl -fsSL --retry 5 --retry-delay 3 "$STOP_SCRIPT_URL" -o "$stop_script_path"; then
|
|
echo -e "${RED}#${RESET} Failed to download the stop script. Please check the URL and try again."
|
|
exit 1
|
|
fi
|
|
chmod +x "$stop_script_path"
|
|
|
|
if ! curl -fsSL --retry 5 --retry-delay 3 "$UPDATE_SCRIPT_URL" -o "$update_script_path"; then
|
|
echo -e "${RED}#${RESET} Failed to download the update script. Please check the URL and try again."
|
|
exit 1
|
|
fi
|
|
chmod +x "$update_script_path"
|
|
|
|
echo -e "${GREEN}#${RESET} Helper scripts downloaded successfully to $start_script_path, $stop_script_path, and $update_script_path.\\n"
|
|
}
|
|
|
|
copy_helper_scripts_from_artifacts() {
|
|
local start_script_path="${NOMAD_DIR}/start_nomad.sh"
|
|
local stop_script_path="${NOMAD_DIR}/stop_nomad.sh"
|
|
local update_script_path="${NOMAD_DIR}/update_nomad.sh"
|
|
|
|
echo -e "${YELLOW}#${RESET} Installing helper scripts from the artifact bundle...\\n"
|
|
copy_artifact_payload_file start_nomad.sh "$start_script_path"
|
|
chmod +x "$start_script_path"
|
|
|
|
copy_artifact_payload_file stop_nomad.sh "$stop_script_path"
|
|
chmod +x "$stop_script_path"
|
|
|
|
copy_artifact_payload_file update_nomad.sh "$update_script_path"
|
|
chmod +x "$update_script_path"
|
|
|
|
# The uninstall script is normally fetched from GitHub on demand, which a
|
|
# disconnected host cannot do. Ship it alongside the others when the bundle
|
|
# carries it.
|
|
if [[ -f "${artifact_payload_dir}/uninstall_nomad.sh" ]]; then
|
|
copy_artifact_payload_file uninstall_nomad.sh "${NOMAD_DIR}/uninstall_nomad.sh"
|
|
chmod +x "${NOMAD_DIR}/uninstall_nomad.sh"
|
|
fi
|
|
|
|
echo -e "${GREEN}#${RESET} Helper scripts installed successfully to $start_script_path, $stop_script_path, and $update_script_path.\\n"
|
|
}
|
|
|
|
setup_helper_scripts() {
|
|
if artifact_mode_enabled; then
|
|
copy_helper_scripts_from_artifacts
|
|
else
|
|
download_helper_scripts
|
|
fi
|
|
}
|
|
|
|
load_artifact_images() {
|
|
echo -e "${YELLOW}#${RESET} Loading Docker images from the artifact bundle...\\n"
|
|
|
|
local archive
|
|
local archives_loaded=0
|
|
for archive in "${artifact_image_dir}"/*.tar; do
|
|
[[ -f "$archive" ]] || continue
|
|
# Skip AppleDouble sidecars ("._core-images.tar"), which macOS leaves on
|
|
# FAT/exFAT media next to real files. They match *.tar but are metadata.
|
|
[[ "$(basename "$archive")" != ._* ]] || continue
|
|
echo -e "${YELLOW}#${RESET} Loading $(basename "$archive")...\\n"
|
|
if ! sudo docker load -i "$archive"; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Failed to load Docker images from $(basename "$archive")."
|
|
exit 1
|
|
fi
|
|
archives_loaded=$((archives_loaded + 1))
|
|
done
|
|
|
|
if [[ "$archives_loaded" -eq 0 ]]; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} No Docker image archives were found in ${artifact_image_dir}."
|
|
exit 1
|
|
fi
|
|
|
|
# Fail closed: the stack starts with --pull never, so a management image that
|
|
# did not make it into the archive must be caught here with a clear message
|
|
# rather than as an opaque Compose failure.
|
|
local image
|
|
while IFS= read -r image; do
|
|
[[ -n "$image" ]] || continue
|
|
if ! sudo docker image inspect "$image" &> /dev/null; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} The bundle lists image ${image} but it is not present after loading."
|
|
echo -e "${RED}#${RESET} Rebuild the bundle on a connected machine and try again."
|
|
exit 1
|
|
fi
|
|
done < "${artifact_image_dir}/core-images.txt"
|
|
|
|
echo -e "${GREEN}#${RESET} Docker images loaded successfully from the artifact bundle.\\n"
|
|
}
|
|
|
|
seed_artifact_content() {
|
|
# Optional extension point: a bundle may carry pre-staged NOMAD storage
|
|
# content. Transporting files here does not make every optional app or content
|
|
# type installable offline — see admin/docs/offline-install.md.
|
|
[[ -d "${NOMAD_ARTIFACT_PATH}/content" ]] || return 0
|
|
|
|
echo -e "${YELLOW}#${RESET} Seeding pre-staged content into ${NOMAD_DIR}/storage...\\n"
|
|
sudo mkdir -p "${NOMAD_DIR}/storage"
|
|
# -n so re-running never overwrites content the user has changed or replaced;
|
|
# new files in a later bundle are still added.
|
|
if ! sudo cp -a -n "${NOMAD_ARTIFACT_PATH}/content/." "${NOMAD_DIR}/storage/"; then
|
|
header_red
|
|
echo -e "${RED}#${RESET} Failed to seed pre-staged content from the artifact bundle."
|
|
exit 1
|
|
fi
|
|
echo -e "${GREEN}#${RESET} Pre-staged content seeded successfully.\\n"
|
|
}
|
|
|
|
start_management_containers() {
|
|
echo -e "${YELLOW}#${RESET} Starting management containers using docker compose...\\n"
|
|
|
|
local compose_args=(-p project-nomad -f "${NOMAD_DIR}/compose.yml")
|
|
if artifact_mode_enabled; then
|
|
# Layer the bundle's override (pull_policy: never for every service) and
|
|
# forbid pulls outright, so startup uses only the images loaded above.
|
|
compose_args+=(-f "${NOMAD_DIR}/compose.artifact.yml" up -d --pull never)
|
|
else
|
|
compose_args+=(up -d)
|
|
fi
|
|
|
|
if ! sudo docker compose "${compose_args[@]}"; then
|
|
echo -e "${RED}#${RESET} Failed to start management containers. Please check the logs and try again."
|
|
exit 1
|
|
fi
|
|
echo -e "${GREEN}#${RESET} Management containers started successfully.\\n"
|
|
}
|
|
|
|
get_local_ip() {
|
|
local_ip_address=$(hostname -I 2>/dev/null | awk '{print $1}')
|
|
if [[ -n "$local_ip_address" ]]; then
|
|
has_lan_address='true'
|
|
return 0
|
|
fi
|
|
|
|
if artifact_mode_enabled; then
|
|
# A genuinely disconnected target may have no LAN address at all. That must
|
|
# not block an offline install, so fall back to localhost and simply don't
|
|
# advertise a LAN URL.
|
|
echo -e "${YELLOW}#${RESET} No LAN address detected. Project NOMAD will be reachable at http://localhost:8080.\\n"
|
|
local_ip_address='localhost'
|
|
return 0
|
|
fi
|
|
|
|
echo -e "${RED}#${RESET} Unable to determine local IP address. Please check your network configuration."
|
|
exit 1
|
|
}
|
|
verify_gpu_setup() {
|
|
# This function only displays GPU setup status and is completely non-blocking
|
|
# It never exits or returns error codes - purely informational
|
|
|
|
echo -e "\\n${YELLOW}#${RESET} GPU Setup Verification\\n"
|
|
echo -e "${YELLOW}===========================================${RESET}\\n"
|
|
|
|
# Check if NVIDIA GPU is present
|
|
if command -v nvidia-smi &> /dev/null; then
|
|
echo -e "${GREEN}✓${RESET} NVIDIA GPU detected:"
|
|
nvidia-smi --query-gpu=name,memory.total --format=csv,noheader 2>/dev/null | while read -r line; do
|
|
echo -e " ${WHITE_R}$line${RESET}"
|
|
done
|
|
echo ""
|
|
else
|
|
echo -e "${YELLOW}○${RESET} No NVIDIA GPU detected (nvidia-smi not available)\\n"
|
|
fi
|
|
|
|
# Check if NVIDIA Container Toolkit is installed
|
|
if command -v nvidia-ctk &> /dev/null; then
|
|
echo -e "${GREEN}✓${RESET} NVIDIA Container Toolkit installed: $(nvidia-ctk --version 2>/dev/null | head -n1)\\n"
|
|
else
|
|
echo -e "${YELLOW}○${RESET} NVIDIA Container Toolkit not installed\\n"
|
|
fi
|
|
|
|
# Check if Docker has NVIDIA runtime
|
|
if docker info 2>/dev/null | grep -q "nvidia"; then
|
|
echo -e "${GREEN}✓${RESET} Docker NVIDIA runtime configured\\n"
|
|
else
|
|
echo -e "${YELLOW}○${RESET} Docker NVIDIA runtime not detected\\n"
|
|
fi
|
|
|
|
# Check for AMD GPU — restrict to display controller classes to avoid false positives
|
|
# from AMD CPU host bridges, PCI bridges, and chipset devices.
|
|
local has_amd_gpu='false'
|
|
local amd_gfx_version=''
|
|
if command -v lspci &> /dev/null; then
|
|
if lspci 2>/dev/null | grep -iE "VGA|3D controller|Display" | grep -iE "amd|radeon" &> /dev/null; then
|
|
has_amd_gpu='true'
|
|
echo -e "${GREEN}✓${RESET} AMD GPU detected — ROCm acceleration will be configured automatically when AI Assistant is installed.\\n"
|
|
|
|
# Map AMD codename → gfx version so the admin can pick the right HSA_OVERRIDE_GFX_VERSION.
|
|
# gfx1030/1100/1101/1102 are on AMD's official ROCm allowlist and need NO override —
|
|
# forcing one (e.g. 11.0.0) breaks GPU discovery on these. Other variants do need it.
|
|
local amd_devices
|
|
amd_devices=$(lspci -vmm 2>/dev/null | awk -F'\t' '/^Class:.*(VGA|3D|Display)/{c=1} c && /^Device:/{print $2; c=0}')
|
|
if echo "${amd_devices}" | grep -iq 'Navi 21'; then
|
|
amd_gfx_version='gfx1030'
|
|
elif echo "${amd_devices}" | grep -iq 'Navi 22'; then
|
|
amd_gfx_version='gfx1031'
|
|
elif echo "${amd_devices}" | grep -iq 'Navi 23'; then
|
|
amd_gfx_version='gfx1032'
|
|
elif echo "${amd_devices}" | grep -iq 'Navi 24'; then
|
|
amd_gfx_version='gfx1034'
|
|
elif echo "${amd_devices}" | grep -iq 'Rembrandt'; then
|
|
amd_gfx_version='gfx1035'
|
|
elif echo "${amd_devices}" | grep -iEq 'Phoenix[0-9]?|Hawk Point|Radeon (780M|760M)'; then
|
|
# Phoenix (Ryzen 7040) / Hawk Point (Ryzen 8040) — 780M & 760M are both gfx1103.
|
|
# lspci device strings vary (Phoenix1/Phoenix2/Phoenix3, "Hawk Point", or the bare
|
|
# "Radeon 780M Graphics" marketing name), so match all of them or the marker goes
|
|
# missing and the 780M silently drops to CPU. Kept before the Strix branches so a
|
|
# "Radeon 780M" string can't be miscaught. See gfx1103 regression.
|
|
amd_gfx_version='gfx1103'
|
|
elif echo "${amd_devices}" | grep -iEq 'Strix Halo'; then
|
|
amd_gfx_version='gfx1151'
|
|
elif echo "${amd_devices}" | grep -iEq 'Strix( Point)?'; then
|
|
amd_gfx_version='gfx1150'
|
|
elif echo "${amd_devices}" | grep -iq 'Navi 31'; then
|
|
amd_gfx_version='gfx1100'
|
|
elif echo "${amd_devices}" | grep -iq 'Navi 32'; then
|
|
amd_gfx_version='gfx1101'
|
|
elif echo "${amd_devices}" | grep -iq 'Navi 33'; then
|
|
amd_gfx_version='gfx1102'
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Write detected GPU type to a marker file the admin container can read. The admin
|
|
# container lacks lspci and AMD GPUs don't register a Docker runtime, so this is the
|
|
# only reliable way for the admin to know an AMD GPU is present at install time.
|
|
local gpu_marker_path="${NOMAD_DIR}/storage/.nomad-gpu-type"
|
|
if command -v nvidia-smi &> /dev/null; then
|
|
echo 'nvidia' | sudo tee "${gpu_marker_path}" > /dev/null 2>&1 || true
|
|
elif [[ "${has_amd_gpu}" == 'true' ]]; then
|
|
echo 'amd' | sudo tee "${gpu_marker_path}" > /dev/null 2>&1 || true
|
|
else
|
|
sudo rm -f "${gpu_marker_path}" 2>/dev/null || true
|
|
fi
|
|
|
|
# Companion marker used by the admin to pick the right HSA_OVERRIDE_GFX_VERSION for
|
|
# the detected card. Absence of this file means "unknown gfx" — the admin falls back
|
|
# to its built-in default. Always rewrite (or remove) on install to keep state fresh.
|
|
local amd_gfx_marker_path="${NOMAD_DIR}/storage/.nomad-amd-gfx"
|
|
if [[ -n "${amd_gfx_version}" ]]; then
|
|
echo "${amd_gfx_version}" | sudo tee "${amd_gfx_marker_path}" > /dev/null 2>&1 || true
|
|
else
|
|
sudo rm -f "${amd_gfx_marker_path}" 2>/dev/null || true
|
|
fi
|
|
|
|
echo -e "${YELLOW}===========================================${RESET}\\n"
|
|
|
|
# Summary
|
|
if command -v nvidia-smi &> /dev/null && docker info 2>/dev/null | grep -q "nvidia"; then
|
|
echo -e "${GREEN}#${RESET} GPU acceleration is properly configured! The AI Assistant will use your GPU.\\n"
|
|
elif [[ "${has_amd_gpu}" == 'true' ]]; then
|
|
echo -e "${GREEN}#${RESET} GPU acceleration will be enabled (AMD/ROCm) when AI Assistant is installed from the dashboard.\\n"
|
|
else
|
|
echo -e "${YELLOW}#${RESET} GPU acceleration not detected. The AI Assistant will run in CPU-only mode.\\n"
|
|
if command -v nvidia-smi &> /dev/null && ! docker info 2>/dev/null | grep -q "nvidia"; then
|
|
echo -e "${YELLOW}#${RESET} Tip: Your GPU is detected but Docker runtime is not configured.\\n"
|
|
echo -e "${YELLOW}#${RESET} Try restarting Docker: ${WHITE_R}sudo systemctl restart docker${RESET}\\n"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
success_message() {
|
|
echo -e "${GREEN}#${RESET} Project NOMAD installation completed successfully!\\n"
|
|
echo -e "${GREEN}#${RESET} Installation files are located at /opt/project-nomad\\n\n"
|
|
echo -e "${GREEN}#${RESET} Project NOMAD's Command Center should automatically start whenever your device reboots. However, if you need to start it manually, you can always do so by running: ${WHITE_R}${NOMAD_DIR}/start_nomad.sh${RESET}\\n"
|
|
if [[ "${has_lan_address}" == 'true' ]]; then
|
|
echo -e "${GREEN}#${RESET} You can now access the management interface at http://localhost:8080 or http://${local_ip_address}:8080\\n"
|
|
else
|
|
# No LAN address was available (offline install on a host with no network).
|
|
echo -e "${GREEN}#${RESET} You can now access the management interface at http://localhost:8080\\n"
|
|
fi
|
|
echo -e "${GREEN}#${RESET} Thank you for supporting Project NOMAD!\\n"
|
|
}
|
|
|
|
###################################################################################################################################################################################################
|
|
# #
|
|
# Main Script #
|
|
# #
|
|
###################################################################################################################################################################################################
|
|
|
|
# The test suite sources this script to exercise individual functions. Every
|
|
# other invocation runs the installer normally.
|
|
if [[ "${NOMAD_INSTALLER_LIB_ONLY:-}" == '1' ]]; then
|
|
return 0 2>/dev/null || exit 0
|
|
fi
|
|
|
|
parse_installer_args "$@"
|
|
|
|
# Pre-flight checks
|
|
check_is_debian_based
|
|
check_is_x86_64
|
|
check_is_bash
|
|
check_has_sudo
|
|
if artifact_mode_enabled; then
|
|
# Artifact mode is fail-closed from here on: no dependency is acquired over
|
|
# the network, so the bundle is validated up front instead.
|
|
validate_artifact_bundle
|
|
# Read before anything is written, so the confirmation prompt can tell the
|
|
# user whether this is a fresh install or an in-place update.
|
|
detect_existing_installation
|
|
else
|
|
ensure_dependencies_installed
|
|
fi
|
|
check_is_debug_mode
|
|
|
|
# Main install
|
|
get_install_confirmation
|
|
accept_terms
|
|
if artifact_mode_enabled; then
|
|
install_packages_from_artifacts
|
|
check_docker_compose
|
|
setup_nvidia_container_toolkit_from_artifacts
|
|
else
|
|
ensure_docker_installed
|
|
check_docker_compose
|
|
setup_nvidia_container_toolkit
|
|
fi
|
|
get_local_ip
|
|
create_nomad_directory
|
|
setup_helper_scripts
|
|
setup_management_compose_file
|
|
if artifact_mode_enabled; then
|
|
load_artifact_images
|
|
seed_artifact_content
|
|
fi
|
|
start_management_containers
|
|
verify_gpu_setup
|
|
success_message
|
|
|
|
# free_space_check() {
|
|
# if [[ "$(df -B1 / | awk 'NR==2{print $4}')" -le '5368709120' ]]; then
|
|
# header_red
|
|
# echo -e "${YELLOW}#${RESET} You only have $(df -B1 / | awk 'NR==2{print $4}' | awk '{ split( "B KB MB GB TB PB EB ZB YB" , v ); s=1; while( $1>1024 && s<9 ){ $1/=1024; s++ } printf "%.1f %s", $1, v[s] }') of disk space available on \"/\"... \\n"
|
|
# while true; do
|
|
# read -rp $'\033[39m#\033[0m Do you want to proceed with running the script? (y/N) ' yes_no
|
|
# case "$yes_no" in
|
|
# [Nn]*|"")
|
|
# free_space_check_response="Cancel script"
|
|
# free_space_check_date="$(date +%s)"
|
|
# echo -e "${YELLOW}#${RESET} OK... Please free up disk space before running the script again..."
|
|
# cancel_script
|
|
# break;;
|
|
# [Yy]*)
|
|
# free_space_check_response="Proceed at own risk"
|
|
# free_space_check_date="$(date +%s)"
|
|
# echo -e "${YELLOW}#${RESET} OK... Proceeding with the script.. please note that failures may occur due to not enough disk space... \\n"; sleep 10
|
|
# break;;
|
|
# *) echo -e "\\n${RED}#${RESET} Invalid input, please answer Yes or No (y/n)...\\n"; sleep 3;;
|
|
# esac
|
|
# done
|
|
# if [[ -n "$(command -v jq)" ]]; then
|
|
# if [[ "$(dpkg-query --showformat='${version}' --show jq 2> /dev/null | sed -e 's/.*://' -e 's/-.*//g' -e 's/[^0-9.]//g' -e 's/\.//g' | sort -V | tail -n1)" -ge "16" && -e "${eus_dir}/db/db.json" ]]; then
|
|
# jq '.scripts."'"${script_name}"'" += {"warnings": {"low-free-disk-space": {"response": "'"${free_space_check_response}"'", "detected-date": "'"${free_space_check_date}"'"}}}' "${eus_dir}/db/db.json" > "${eus_dir}/db/db.json.tmp" 2>> "${eus_dir}/logs/eus-database-management.log"
|
|
# else
|
|
# jq '.scripts."'"${script_name}"'" = (.scripts."'"${script_name}"'" | . + {"warnings": {"low-free-disk-space": {"response": "'"${free_space_check_response}"'", "detected-date": "'"${free_space_check_date}"'"}}})' "${eus_dir}/db/db.json" > "${eus_dir}/db/db.json.tmp" 2>> "${eus_dir}/logs/eus-database-management.log"
|
|
# fi
|
|
# eus_database_move
|
|
# fi
|
|
# fi
|
|
# }
|