#!/usr/bin/env bash set -Eeuo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" # shellcheck source=install/lib/common.sh source "${REPO_ROOT}/install/lib/common.sh" # shellcheck source=install/lib/validation.sh source "${REPO_ROOT}/install/lib/validation.sh" atlas_set_script_name "validate-host.sh" EXPECTED_HOSTNAME="atlas" EXPECTED_REMOTE="git@git.catherinelynwood.com:atlas-bot/AI-Development-Server.git" EXPECTED_BRANCH="main" EXPECTED_UPSTREAM="origin/main" EXPECTED_REPO_PATH="/srv/repos/AI-Development-Server" MIN_ROOT_FREE_GIB=10 MIN_SRV_FREE_GIB=20 MIN_SQL_FREE_GIB=20 # Critical-service policy for Phase 2A: # A failed unit is critical only if it affects SSH access, network identity, # DNS resolution, time synchronisation, or required Atlas mounts. CRITICAL_FAILED_UNIT_PATTERN='^(ssh|sshd|systemd-networkd|systemd-resolved|chrony)\.service$|^(sql|srv)\.mount$' usage() { cat <<'USAGE' Usage: scripts/system/validate-host.sh [options] Read-only Atlas host validation. Options: --help Show this help text. --version Show script/framework version. --expected-hostname NAME Override expected hostname for controlled tests. --min-root-gib N Override minimum free GiB for /. --min-srv-gib N Override minimum free GiB for /srv. --min-sql-gib N Override minimum free GiB for /sql. Exit codes: 0 All required checks passed. Advisory warnings may exist. 2 Invalid invocation. 4 One or more required validation checks failed. 10 Internal script error. USAGE } numeric_arg() { local name="$1" local value="$2" if [[ ! "$value" =~ ^[0-9]+$ ]]; then atlas_usage_error "${name} requires a non-negative integer" exit "${ATLAS_EXIT_USAGE}" fi } while [[ "$#" -gt 0 ]]; do case "$1" in --help) usage exit "${ATLAS_EXIT_OK}" ;; --version) atlas_print_version exit "${ATLAS_EXIT_OK}" ;; --expected-hostname) [[ "$#" -ge 2 ]] || { atlas_usage_error "--expected-hostname requires a value"; exit "${ATLAS_EXIT_USAGE}"; } EXPECTED_HOSTNAME="$2" shift ;; --min-root-gib) [[ "$#" -ge 2 ]] || { atlas_usage_error "--min-root-gib requires a value"; exit "${ATLAS_EXIT_USAGE}"; } numeric_arg "--min-root-gib" "$2" MIN_ROOT_FREE_GIB="$2" shift ;; --min-srv-gib) [[ "$#" -ge 2 ]] || { atlas_usage_error "--min-srv-gib requires a value"; exit "${ATLAS_EXIT_USAGE}"; } numeric_arg "--min-srv-gib" "$2" MIN_SRV_FREE_GIB="$2" shift ;; --min-sql-gib) [[ "$#" -ge 2 ]] || { atlas_usage_error "--min-sql-gib requires a value"; exit "${ATLAS_EXIT_USAGE}"; } numeric_arg "--min-sql-gib" "$2" MIN_SQL_FREE_GIB="$2" shift ;; *) atlas_usage_error "Unknown option: $1" exit "${ATLAS_EXIT_USAGE}" ;; esac shift done check_command() { local command_name="$1" if command -v "$command_name" >/dev/null 2>&1; then atlas_validation_result PASS "Command available: ${command_name}" else atlas_validation_result FAIL "Required command missing: ${command_name}" fi } check_mount() { local path="$1" local root_source mount_source if [[ -d "$path" ]]; then atlas_validation_result PASS "${path} exists" else atlas_validation_result FAIL "${path} does not exist" return fi if findmnt -rn --target "$path" >/dev/null 2>&1; then atlas_validation_result PASS "${path} is mounted" else atlas_validation_result FAIL "${path} is not mounted" return fi root_source="$(findmnt -rn --target / -o SOURCE 2>/dev/null || true)" mount_source="$(findmnt -rn --target "$path" -o SOURCE 2>/dev/null || true)" if [[ -n "$mount_source" && "$mount_source" != "$root_source" ]]; then atlas_validation_result PASS "${path} is a separate mounted filesystem" else atlas_validation_result FAIL "${path} is not a separate mounted filesystem" fi if [[ -w "$path" ]]; then atlas_validation_result PASS "${path} is writable by the operational user" else atlas_validation_result FAIL "${path} is not writable by the operational user" fi } check_free_space() { local path="$1" local min_gib="$2" local available required available="$(atlas_bytes_available "$path" || true)" required="$(atlas_gib_to_bytes "$min_gib")" if [[ -z "$available" ]]; then atlas_validation_result FAIL "Could not determine free space for ${path}" elif (( available >= required )); then atlas_validation_result PASS "${path} has at least ${min_gib} GiB free" else atlas_validation_result FAIL "${path} has less than ${min_gib} GiB free" fi } check_git_state() { if [[ -d "${EXPECTED_REPO_PATH}/.git" ]]; then atlas_validation_result PASS "Repository exists at ${EXPECTED_REPO_PATH}" else atlas_validation_result FAIL "Repository missing at ${EXPECTED_REPO_PATH}" return fi local remote_url branch upstream remote_url="$(git -C "$EXPECTED_REPO_PATH" remote get-url origin 2>/dev/null || true)" branch="$(git -C "$EXPECTED_REPO_PATH" branch --show-current 2>/dev/null || true)" upstream="$(git -C "$EXPECTED_REPO_PATH" rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)" if [[ -n "$remote_url" ]]; then atlas_validation_result PASS "Git remote origin is configured" else atlas_validation_result FAIL "Git remote origin is not configured" fi if [[ "$remote_url" == "$EXPECTED_REMOTE" ]]; then atlas_validation_result PASS "Expected remote repository is configured" else atlas_validation_result FAIL "Expected remote repository is not configured" fi if [[ "$branch" == "$EXPECTED_BRANCH" ]]; then atlas_validation_result PASS "Current branch is ${EXPECTED_BRANCH}" else atlas_validation_result FAIL "Current branch is ${branch:-unknown}, expected ${EXPECTED_BRANCH}" fi if [[ "$upstream" == "$EXPECTED_UPSTREAM" ]]; then atlas_validation_result PASS "Upstream is ${EXPECTED_UPSTREAM}" else atlas_validation_result FAIL "Upstream is ${upstream:-unset}, expected ${EXPECTED_UPSTREAM}" fi } check_failed_services() { if ! command -v systemctl >/dev/null 2>&1; then atlas_validation_result SKIPPED "systemctl unavailable; failed-service check skipped" return fi local failed_units critical_units warning_units failed_units="$(systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}' || true)" if [[ -z "$failed_units" ]]; then atlas_validation_result PASS "No failed systemd units" return fi critical_units="$(printf '%s\n' "$failed_units" | grep -E "$CRITICAL_FAILED_UNIT_PATTERN" || true)" warning_units="$(printf '%s\n' "$failed_units" | grep -Ev "$CRITICAL_FAILED_UNIT_PATTERN" || true)" if [[ -n "$critical_units" ]]; then atlas_validation_result FAIL "Critical failed systemd units: $(printf '%s' "$critical_units" | paste -sd ',')" fi if [[ -n "$warning_units" ]]; then atlas_validation_result WARNING "Non-critical failed systemd units: $(printf '%s' "$warning_units" | paste -sd ',')" fi } check_docker_sql_state() { if ! command -v docker >/dev/null 2>&1; then atlas_validation_result SKIPPED "Docker is not installed" return fi if systemctl is-active --quiet docker; then atlas_validation_result PASS "Docker service is active" else atlas_validation_result FAIL "Docker service is not active" fi if docker compose version >/dev/null 2>&1; then atlas_validation_result PASS "Docker Compose plugin is available" else atlas_validation_result FAIL "Docker Compose plugin is not available" fi local sql_health sql_health="$(sudo -n docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' atlas-sql-server 2>/dev/null || true)" if [[ -z "$sql_health" ]]; then atlas_validation_result SKIPPED "SQL Server container is not present" elif [[ "$sql_health" == "healthy" ]]; then atlas_validation_result PASS "SQL Server container is healthy" else atlas_validation_result FAIL "SQL Server container health is ${sql_health}" fi } atlas_validation_reset printf 'Atlas Host Validation\n' printf 'Free-space thresholds: /=%s GiB, /srv=%s GiB, /sql=%s GiB\n\n' \ "$MIN_ROOT_FREE_GIB" "$MIN_SRV_FREE_GIB" "$MIN_SQL_FREE_GIB" if [[ "$(hostname)" == "$EXPECTED_HOSTNAME" ]]; then atlas_validation_result PASS "Hostname is ${EXPECTED_HOSTNAME}" else atlas_validation_result FAIL "Hostname is $(hostname), expected ${EXPECTED_HOSTNAME}" fi case "$(uname -m)" in x86_64|amd64) atlas_validation_result PASS "Architecture is supported: $(uname -m)" ;; *) atlas_validation_result FAIL "Architecture is unsupported: $(uname -m)" ;; esac check_mount /sql check_mount /srv check_git_state while IFS='|' read -r command_name requirement _purpose; do [[ -z "$command_name" || "$command_name" =~ ^# ]] && continue if [[ "$requirement" == "required" ]]; then check_command "$command_name" fi done <"${REPO_ROOT}/install/manifests/baseline-commands.txt" if sudo -n true >/dev/null 2>&1; then atlas_validation_result PASS "Passwordless non-interactive sudo works" else atlas_validation_result FAIL "Passwordless non-interactive sudo does not work" fi check_free_space / "$MIN_ROOT_FREE_GIB" check_free_space /srv "$MIN_SRV_FREE_GIB" check_free_space /sql "$MIN_SQL_FREE_GIB" check_failed_services check_docker_sql_state if [[ -f /var/run/reboot-required ]]; then atlas_validation_result WARNING "Reboot is pending" else atlas_validation_result PASS "No reboot pending" fi printf '\n' atlas_validation_summary atlas_validation_exit_code