273 lines
8.5 KiB
Bash
Executable File

#!/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
}
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
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