Add Phase 2A installer framework and host validation

This commit is contained in:
Nick Beckley 2026-07-10 11:18:10 +00:00
parent 451b5b28d7
commit 6ff3a79cea
21 changed files with 1397 additions and 29 deletions

7
.gitignore vendored
View File

@ -38,6 +38,7 @@ overrides/
*.log
logs/
log/
atlas-logs/
# Temporary files
tmp/
@ -83,6 +84,12 @@ releases/
*.sql.gz
*.sql.zip
# Generated Atlas reports
reports/
atlas-reports/
*.report.json
*.report.txt
# Package caches
.npm/
.pnpm-store/

View File

@ -70,19 +70,34 @@ It must not contain secrets, passwords, tokens, private keys, live credentials,
Directory-level README files explain what belongs in each working area.
## Future Installation Model
## Installation Model
Future installation scripts will live under `install/`. They will be ordered, idempotent Bash scripts, for example:
Installation framework files live under `install/`. Phase 2A adds shared Bash libraries and a bootstrap scaffold, but component installation has not begun.
```text
install/01-base-tools.sh
install/02-dotnet.sh
install/03-sqlserver.sh
install/04-nginx.sh
install/05-docker.sh
Current commands:
```bash
install/bootstrap.sh --help
scripts/system/discover-system.sh
scripts/system/validate-host.sh
```
These scripts do not exist yet. Until they do, rebuild steps remain manual and are documented in [docs/REBUILD-GUIDE.md](docs/REBUILD-GUIDE.md).
Future component installation scripts will live under `install/phases/`. They will be ordered, idempotent Bash scripts, for example:
```text
install/phases/10-dotnet.sh
install/phases/20-sql-server.sh
install/phases/30-nginx.sh
install/phases/40-docker.sh
```
These component scripts do not exist yet. Until they do, rebuild steps remain manual and are documented in [docs/REBUILD-GUIDE.md](docs/REBUILD-GUIDE.md).
Generated logs and reports belong outside the repository:
- Installer logs: `/var/log/atlas/install/`
- Discovery and validation reports: `/var/log/atlas/reports/`
- Future installer state and markers: `/var/lib/atlas/`
## Change Control
@ -96,7 +111,8 @@ These scripts do not exist yet. Until they do, rebuild steps remain manual and a
## Roadmap Summary
- Phase 1: Establish repository structure, standards, architecture documentation, rebuild guidance, disaster recovery guidance, operating model, and ADR process.
- Future phases: Add idempotent installation scripts, configuration templates, operational scripts, smoke tests, backup automation, restore automation, deployment automation, and monitoring.
- Phase 2A: Add shared installer framework, system discovery, host validation, runtime directory conventions, and validation thresholds.
- Future phases: Add component installation scripts, configuration templates, operational scripts, smoke tests, backup automation, restore automation, deployment automation, and monitoring.
## Security Warning

View File

@ -39,7 +39,8 @@ Unknowns must be written exactly as `TODO: Confirm during the relevant implement
- Print concise progress messages.
- Use consistent prefixes: `[INFO]`, `[WARN]`, `[ERROR]`, `[OK]`.
- Do not print secrets, tokens, passwords, connection strings, private keys, or certificate material.
- Long-term logs must be stored outside this repository.
- Installer logs must be stored outside this repository under `/var/log/atlas/install/`.
- Generated discovery and validation reports must be stored outside this repository under `/var/log/atlas/reports/`.
- Repository scripts may write temporary logs only under documented temporary paths and must clean them up where practical.
## Exit Codes
@ -59,6 +60,7 @@ Unknowns must be written exactly as `TODO: Confirm during the relevant implement
- Do not change ownership recursively outside documented paths.
- Secrets outside the repository must use the narrowest practical permissions.
- Templates in the repository must not contain live secrets.
- Future persistent installer state and marker files belong under `/var/lib/atlas/`.
## Configuration Locations
@ -159,6 +161,7 @@ Unknowns must be written exactly as `TODO: Confirm during the relevant implement
- Tests must avoid requiring secrets where practical.
- Tests must not mutate production data unless explicitly designed and approved.
- Run relevant tests before committing.
- Host validation thresholds are `/` 10 GiB, `/srv` 20 GiB, and `/sql` 20 GiB unless an approved implementation phase changes them.
## Documentation Requirements

View File

@ -56,6 +56,7 @@ Branching policy is `TODO: Confirm during the relevant implementation phase.`
The repository is intended to grow into the version-controlled control plane for Atlas. Planned components include:
- Idempotent installation scripts under `install/`.
- Shared installer framework libraries under `install/lib/`.
- Managed configuration templates under `config/` and `templates/`.
- Operational scripts under `scripts/`.
- Smoke tests under `tests/`.
@ -121,6 +122,9 @@ The intended storage and responsibility model is:
- Source repositories: `/srv/repos`
- Infrastructure repository: `/srv/repos/AI-Development-Server`
- Installer logs: `/var/log/atlas/install/`
- Discovery and validation reports: `/var/log/atlas/reports/`
- Future installer state and markers: `/var/lib/atlas/`
- Build artefacts: `TODO: Confirm during the relevant implementation phase.`
- Deployed applications: `TODO: Confirm during the relevant implementation phase.`
- SQL data: `/sql`

View File

@ -6,6 +6,58 @@ This file records chronological Atlas build and infrastructure milestones. Do no
### Date
2026-07-10
### Phase
Phase 2A.
### Objective
Create the shared installation framework, system-discovery tooling, and host-validation conventions used by later Atlas installation scripts.
### Changes
- Added shared Bash installer libraries under `install/lib/`.
- Added `install/bootstrap.sh` as a read-only Phase 2A scaffold.
- Added baseline command manifest under `install/manifests/`.
- Added system discovery tooling under `scripts/system/`.
- Added host validation tooling under `scripts/system/`.
- Documented runtime directories for logs, reports, and future state.
- Documented free-space validation thresholds for `/`, `/srv`, and `/sql`.
- No .NET, SQL Server, nginx, or Docker installation was started.
### Validation
- Bash syntax validation passed for all Phase 2A shell scripts.
- `install/bootstrap.sh --help`, `--version`, and `--dry-run` passed.
- `scripts/system/discover-system.sh` passed in human-readable and JSON modes.
- Discovery JSON parsed successfully with `jq`.
- `scripts/system/validate-host.sh` passed with no required failures.
- Controlled failure-path tests returned the expected validation and usage exit codes.
- Generated logs and reports were written outside the repository.
### Problems
- No unresolved Phase 2A implementation problems recorded.
### Decisions
- Installer logs use `/var/log/atlas/install/`.
- Generated reports use `/var/log/atlas/reports/`.
- Future persistent installer state and marker files use `/var/lib/atlas/`.
- Phase 2A does not create speculative future component installer scripts.
### Git Commit
Not yet committed.
### Operator
Codex.
### Date
Not recorded.
### Phase

View File

@ -99,6 +99,15 @@ For documentation-only changes, validation should include:
For scripts and service changes, validation should also include syntax checks, dry runs where available, and smoke tests.
Phase 2A adds two read-only host checks that should be used before later installation phases:
```bash
scripts/system/discover-system.sh
scripts/system/validate-host.sh
```
Generated logs and reports should remain outside the repository unless a sanitised snapshot is deliberately reviewed and committed.
## Rollback Expectations
- Prefer changes that can be reversed through Git and idempotent scripts.

View File

@ -5,7 +5,10 @@ This guide describes the planned rebuild path for Atlas. At Phase 1, the reposit
## Automation Status
- Repository structure and documentation: present.
- Installation scripts: not yet created.
- Shared installation framework: present.
- System discovery script: present.
- Host validation script: present.
- Component installation scripts: not yet created.
- Configuration templates: not yet created.
- Operational scripts: not yet created.
- Smoke tests: not yet created.
@ -106,9 +109,11 @@ Planned bootstrap flow:
1. Review [README.md](../README.md).
2. Review [SERVER-STANDARDS.md](../SERVER-STANDARDS.md).
3. Review this rebuild guide.
4. Run base installation validation.
5. Run ordered installation scripts from `install/` when they exist.
6. Run smoke tests from `tests/` when they exist.
4. Run `scripts/system/discover-system.sh`.
5. Run `scripts/system/validate-host.sh`.
6. Run `install/bootstrap.sh --dry-run`.
7. Run ordered installation scripts from `install/phases/` when they exist.
8. Run smoke tests from `tests/` when they exist.
Bootstrap command sequence is `TODO: Confirm during the relevant implementation phase.`
@ -117,11 +122,10 @@ Bootstrap command sequence is `TODO: Confirm during the relevant implementation
Planned scripts may include:
```text
install/01-base-tools.sh
install/02-dotnet.sh
install/03-sqlserver.sh
install/04-nginx.sh
install/05-docker.sh
install/phases/10-dotnet.sh
install/phases/20-sql-server.sh
install/phases/30-nginx.sh
install/phases/40-docker.sh
```
These scripts do not exist yet. When created, each script must:
@ -147,7 +151,9 @@ Minimum rebuild validation should confirm:
- Required installation phases have run successfully.
- Smoke tests pass.
Validation scripts are `TODO: Confirm during the relevant implementation phase.`
Validation scripts are currently available at `scripts/system/discover-system.sh` and `scripts/system/validate-host.sh`.
Phase 2A host validation currently checks `/` has at least 10 GiB free, `/srv` has at least 20 GiB free, and `/sql` has at least 20 GiB free.
## 9. Restoring Services and Data

View File

@ -2,6 +2,8 @@
This directory is reserved for ordered, idempotent installation scripts for Atlas.
Phase 2A provides the shared Bash framework and a bootstrap scaffold. It does not install .NET, SQL Server, nginx, Docker, or other future platform components.
## What Belongs Here
- Bootstrap scripts.
@ -9,6 +11,8 @@ This directory is reserved for ordered, idempotent installation scripts for Atla
- Service installation scripts.
- Storage, runtime, and tooling setup scripts.
- Validation wrappers for installation phases.
- Shared Bash framework libraries under `lib/`.
- Component manifests under `manifests/`.
## What Must Not Be Stored Here
@ -21,23 +25,34 @@ This directory is reserved for ordered, idempotent installation scripts for Atla
## Naming Conventions
Use a two-digit numeric prefix followed by a short lower-case hyphenated name:
Future phase scripts should use a two-digit numeric prefix followed by a short lower-case hyphenated name:
```text
01-base-tools.sh
02-dotnet.sh
03-sqlserver.sh
04-nginx.sh
05-docker.sh
phases/10-dotnet.sh
phases/20-sql-server.sh
phases/30-nginx.sh
phases/40-docker.sh
phases/90-validate-platform.sh
```
Scripts must follow [SERVER-STANDARDS.md](../SERVER-STANDARDS.md).
## Expected Future Structure
Phase scripts may be added directly under this directory at first. If the directory grows, shared helpers may be added under a clearly named subdirectory.
Current Phase 2A structure:
Future structure is `TODO: Confirm during the relevant implementation phase.`
```text
bootstrap.sh
lib/
manifests/
phases/
```
Runtime output belongs outside the repository:
- `/var/log/atlas/install/`
- `/var/log/atlas/reports/`
- `/var/lib/atlas/`
## Security Considerations

101
install/bootstrap.sh Executable file
View File

@ -0,0 +1,101 @@
#!/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 "${SCRIPT_DIR}/lib/common.sh"
# shellcheck source=install/lib/logging.sh
source "${SCRIPT_DIR}/lib/logging.sh"
# shellcheck source=install/lib/packages.sh
source "${SCRIPT_DIR}/lib/packages.sh"
# shellcheck source=install/lib/services.sh
source "${SCRIPT_DIR}/lib/services.sh"
# shellcheck source=install/lib/files.sh
source "${SCRIPT_DIR}/lib/files.sh"
# shellcheck source=install/lib/validation.sh
source "${SCRIPT_DIR}/lib/validation.sh"
atlas_set_script_name "bootstrap.sh"
atlas_install_error_trap
atlas_install_cleanup_trap
RUN_VALIDATION=0
INIT_LOG=0
ATLAS_INSTALL_LOG_DIR="/var/log/atlas/install"
usage() {
cat <<'USAGE'
Usage: install/bootstrap.sh [options]
Phase 2A bootstrap scaffold. This command does not install packages,
alter services, change firewall rules, or modify system configuration.
Options:
--help Show this help text.
--version Show script/framework version.
--dry-run Show intended actions without making changes.
--verbose Print additional diagnostic output.
--log Initialise a timestamped installer log file.
--validate-host Run the read-only host validation script.
USAGE
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
--help)
usage
exit "${ATLAS_EXIT_OK}"
;;
--version)
atlas_print_version
exit "${ATLAS_EXIT_OK}"
;;
--dry-run)
atlas_enable_dry_run
;;
--verbose)
atlas_enable_verbose
;;
--log)
INIT_LOG=1
;;
--validate-host)
RUN_VALIDATION=1
;;
*)
atlas_usage_error "Unknown option: $1"
exit "${ATLAS_EXIT_USAGE}"
;;
esac
shift
done
if [[ "$INIT_LOG" == "1" ]]; then
atlas_init_log_file "$ATLAS_INSTALL_LOG_DIR" "bootstrap"
fi
atlas_log_info "Atlas bootstrap scaffold"
atlas_log_info "Repository root: ${REPO_ROOT}"
atlas_log_info "Dry-run mode: ${ATLAS_DRY_RUN}"
atlas_require_command bash
atlas_require_command git
atlas_require_command systemctl
atlas_require_command findmnt
atlas_require_command df
atlas_log_info "Planned future phase directory: ${SCRIPT_DIR}/phases"
atlas_log_info "No component installers are invoked during Phase 2A"
if [[ "$RUN_VALIDATION" == "1" ]]; then
atlas_log_info "Running read-only host validation"
"${REPO_ROOT}/scripts/system/validate-host.sh"
else
atlas_log_info "Host validation not requested; use --validate-host to run it"
fi
atlas_log_ok "Bootstrap scaffold completed"

113
install/lib/common.sh Normal file
View File

@ -0,0 +1,113 @@
#!/usr/bin/env bash
if [[ -n "${ATLAS_COMMON_SH_LOADED:-}" ]]; then
return 0
fi
ATLAS_COMMON_SH_LOADED=1
ATLAS_FRAMEWORK_VERSION="2.0.0"
ATLAS_EXIT_OK=0
ATLAS_EXIT_GENERAL=1
ATLAS_EXIT_USAGE=2
ATLAS_EXIT_PREREQ=3
ATLAS_EXIT_VALIDATION=4
ATLAS_EXIT_UNSAFE=5
ATLAS_EXIT_INTERNAL=10
ATLAS_DRY_RUN=0
ATLAS_VERBOSE=0
ATLAS_SCRIPT_NAME="$(basename "${0:-atlas-script}")"
atlas_strict_mode() {
set -Eeuo pipefail
}
atlas_set_script_name() {
ATLAS_SCRIPT_NAME="$1"
}
atlas_enable_dry_run() {
ATLAS_DRY_RUN=1
}
atlas_enable_verbose() {
ATLAS_VERBOSE=1
}
atlas_is_dry_run() {
[[ "${ATLAS_DRY_RUN}" == "1" ]]
}
atlas_is_verbose() {
[[ "${ATLAS_VERBOSE}" == "1" ]]
}
atlas_usage_error() {
local message="$1"
printf 'ERROR: %s\n' "$message" >&2
return 0
}
atlas_require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
printf 'ERROR: required command not found: %s\n' "$command_name" >&2
return "${ATLAS_EXIT_PREREQ}"
fi
}
atlas_have_command() {
command -v "$1" >/dev/null 2>&1
}
atlas_require_sudo_noninteractive() {
if [[ "${EUID}" -eq 0 ]]; then
return 0
fi
sudo -n true >/dev/null 2>&1
}
atlas_run() {
if atlas_is_dry_run; then
printf '[DRY-RUN] %s\n' "$*"
return 0
fi
"$@"
}
atlas_sudo_run() {
if [[ "${EUID}" -eq 0 ]]; then
atlas_run "$@"
else
atlas_run sudo -n "$@"
fi
}
atlas_print_version() {
printf '%s %s\n' "${ATLAS_SCRIPT_NAME}" "${ATLAS_FRAMEWORK_VERSION}"
}
atlas_install_error_trap() {
trap 'atlas_on_error $? $LINENO "$BASH_COMMAND"' ERR
}
atlas_install_cleanup_trap() {
trap 'atlas_on_exit' EXIT
}
atlas_on_error() {
local exit_code="$1"
local line_no="$2"
local command_text="$3"
if declare -F atlas_log_error >/dev/null 2>&1; then
atlas_log_error "Command failed at line ${line_no} with exit ${exit_code}: ${command_text}"
else
printf 'ERROR: command failed at line %s with exit %s\n' "$line_no" "$exit_code" >&2
fi
return "$exit_code"
}
atlas_on_exit() {
:
}

120
install/lib/files.sh Normal file
View File

@ -0,0 +1,120 @@
#!/usr/bin/env bash
if [[ -n "${ATLAS_FILES_SH_LOADED:-}" ]]; then
return 0
fi
ATLAS_FILES_SH_LOADED=1
atlas_ensure_directory() {
local path="$1"
local mode="${2:-0750}"
local owner="${3:-}"
if [[ -d "$path" ]]; then
atlas_log_verbose "Directory already exists: ${path}"
return 0
fi
if [[ "${ATLAS_DRY_RUN:-0}" == "1" ]]; then
atlas_log_info "Dry-run mode: would create directory ${path} with mode ${mode}"
return 0
fi
if [[ -n "$owner" ]]; then
atlas_sudo_run install -d -m "$mode" -o "${owner%%:*}" -g "${owner##*:}" "$path"
else
atlas_sudo_run install -d -m "$mode" "$path"
fi
}
atlas_backup_file() {
local path="$1"
local backup_dir="${2:-$(dirname "$path")}"
local timestamp
timestamp="$(date -u '+%Y%m%dT%H%M%SZ')"
if [[ ! -e "$path" ]]; then
return 0
fi
local backup_path="${backup_dir}/$(basename "$path").${timestamp}.bak"
if [[ "${ATLAS_DRY_RUN:-0}" == "1" ]]; then
atlas_log_info "Dry-run mode: would back up ${path} to ${backup_path}"
printf '%s\n' "$backup_path"
return 0
fi
if [[ ! -d "$backup_dir" ]]; then
if [[ -w "$(dirname "$backup_dir")" ]]; then
install -d -m 0750 "$backup_dir"
else
atlas_sudo_run install -d -m 0750 "$backup_dir"
fi
fi
if [[ -w "$backup_dir" && -r "$path" ]]; then
cp -a "$path" "$backup_path"
else
atlas_sudo_run cp -a "$path" "$backup_path"
fi
atlas_log_info "Backed up ${path} to ${backup_path}"
printf '%s\n' "$backup_path"
}
atlas_replace_file_if_changed() {
local source="$1"
local destination="$2"
local mode="${3:-0644}"
local owner="${4:-}"
local backup_dir="${5:-$(dirname "$destination")}"
if [[ ! -f "$source" ]]; then
atlas_log_error "Replacement source is not a file: ${source}"
return "${ATLAS_EXIT_USAGE:-2}"
fi
if [[ -f "$destination" ]] && cmp -s "$source" "$destination"; then
atlas_log_ok "No change required for ${destination}"
return 0
fi
if [[ "${ATLAS_DRY_RUN:-0}" == "1" ]]; then
if [[ -e "$destination" ]]; then
atlas_log_info "Dry-run mode: would back up and replace ${destination}"
else
atlas_log_info "Dry-run mode: would create ${destination}"
fi
return 0
fi
if [[ -e "$destination" ]]; then
atlas_backup_file "$destination" "$backup_dir" >/dev/null
fi
local destination_dir tmp_path
destination_dir="$(dirname "$destination")"
if [[ -w "$destination_dir" ]]; then
tmp_path="$(mktemp "${destination}.tmp.XXXXXX")"
else
tmp_path="$(mktemp)"
fi
cp "$source" "$tmp_path"
chmod "$mode" "$tmp_path"
if [[ -n "$owner" ]]; then
if [[ "${EUID}" -eq 0 ]]; then
chown "$owner" "$tmp_path"
elif [[ -w "$destination_dir" ]]; then
chown "$owner" "$tmp_path"
else
sudo -n chown "$owner" "$tmp_path"
fi
fi
if [[ -w "$destination_dir" ]]; then
mv "$tmp_path" "$destination"
else
atlas_sudo_run install -m "$mode" "$tmp_path" "$destination"
rm -f "$tmp_path"
fi
atlas_log_ok "Updated ${destination}"
}

84
install/lib/logging.sh Normal file
View File

@ -0,0 +1,84 @@
#!/usr/bin/env bash
if [[ -n "${ATLAS_LOGGING_SH_LOADED:-}" ]]; then
return 0
fi
ATLAS_LOGGING_SH_LOADED=1
ATLAS_LOG_FILE="${ATLAS_LOG_FILE:-}"
ATLAS_LOG_TO_FILE=0
atlas_timestamp() {
date -u '+%Y-%m-%dT%H:%M:%SZ'
}
atlas_log_line() {
local level="$1"
shift
local message="$*"
local line
line="$(printf '%s [%s] %s' "$(atlas_timestamp)" "$level" "$message")"
printf '%s\n' "$line"
if [[ "${ATLAS_LOG_TO_FILE}" == "1" && -n "${ATLAS_LOG_FILE}" ]]; then
printf '%s\n' "$line" >>"${ATLAS_LOG_FILE}" 2>/dev/null || ATLAS_LOG_TO_FILE=0
fi
}
atlas_log_info() {
atlas_log_line "INFO" "$@"
}
atlas_log_warn() {
atlas_log_line "WARN" "$@"
}
atlas_log_error() {
atlas_log_line "ERROR" "$@" >&2
}
atlas_log_ok() {
atlas_log_line "OK" "$@"
}
atlas_log_verbose() {
if [[ "${ATLAS_VERBOSE:-0}" == "1" ]]; then
atlas_log_line "DEBUG" "$@"
fi
}
atlas_init_log_file() {
local log_dir="$1"
local name="${2:-${ATLAS_SCRIPT_NAME:-atlas}}"
local timestamp
timestamp="$(date -u '+%Y%m%dT%H%M%SZ')"
ATLAS_LOG_FILE="${log_dir}/${name}-${timestamp}.log"
if [[ "${ATLAS_DRY_RUN:-0}" == "1" ]]; then
atlas_log_info "Dry-run mode: would initialise log file ${ATLAS_LOG_FILE}"
return 0
fi
if [[ ! -d "$log_dir" ]]; then
if command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
sudo -n install -d -m 0750 -o "$(id -u)" -g "$(id -g)" "$log_dir" 2>/dev/null || {
atlas_log_warn "File logging disabled; cannot create ${log_dir}"
return 0
}
else
atlas_log_warn "File logging disabled; ${log_dir} does not exist and sudo is unavailable"
return 0
fi
fi
if [[ -w "$log_dir" ]]; then
: >"${ATLAS_LOG_FILE}" 2>/dev/null && chmod 0640 "${ATLAS_LOG_FILE}" 2>/dev/null && ATLAS_LOG_TO_FILE=1 || ATLAS_LOG_TO_FILE=0
elif command -v sudo >/dev/null 2>&1 && sudo -n true >/dev/null 2>&1; then
sudo -n touch "${ATLAS_LOG_FILE}" 2>/dev/null && sudo -n chown "$(id -u):$(id -g)" "${ATLAS_LOG_FILE}" 2>/dev/null && sudo -n chmod 0640 "${ATLAS_LOG_FILE}" 2>/dev/null && ATLAS_LOG_TO_FILE=1 || ATLAS_LOG_TO_FILE=0
fi
if [[ "${ATLAS_LOG_TO_FILE}" == "1" ]]; then
atlas_log_info "Logging to ${ATLAS_LOG_FILE}"
else
atlas_log_warn "File logging unavailable; continuing with console logging"
fi
}

44
install/lib/packages.sh Normal file
View File

@ -0,0 +1,44 @@
#!/usr/bin/env bash
if [[ -n "${ATLAS_PACKAGES_SH_LOADED:-}" ]]; then
return 0
fi
ATLAS_PACKAGES_SH_LOADED=1
atlas_package_installed() {
local package_name="$1"
dpkg-query -W -f='${Status}' "$package_name" 2>/dev/null | grep -q '^install ok installed$'
}
atlas_package_status() {
local package_name="$1"
if atlas_package_installed "$package_name"; then
printf 'installed\n'
else
printf 'missing\n'
fi
}
atlas_install_packages() {
if [[ "$#" -eq 0 ]]; then
atlas_log_warn "No packages requested"
return 0
fi
if [[ "${ATLAS_DRY_RUN:-0}" == "1" ]]; then
atlas_log_info "Dry-run mode: would install packages: $*"
return 0
fi
atlas_log_info "Installing packages: $*"
atlas_sudo_run apt-get install -y "$@"
}
atlas_pending_update_count() {
apt list --upgradable 2>/dev/null | awk 'NR > 1 {count++} END {print count + 0}'
}
atlas_reboot_required() {
[[ -f /var/run/reboot-required ]]
}

58
install/lib/services.sh Normal file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
if [[ -n "${ATLAS_SERVICES_SH_LOADED:-}" ]]; then
return 0
fi
ATLAS_SERVICES_SH_LOADED=1
atlas_service_exists() {
local service_name="$1"
systemctl list-unit-files --type=service --no-legend "$service_name" 2>/dev/null | awk '{print $1}' | grep -qx "$service_name"
}
atlas_service_active() {
local service_name="$1"
systemctl is-active --quiet "$service_name" 2>/dev/null
}
atlas_service_status() {
local service_name="$1"
if ! atlas_service_exists "$service_name"; then
printf 'missing\n'
elif atlas_service_active "$service_name"; then
printf 'active\n'
else
printf 'inactive\n'
fi
}
atlas_service_enable() {
local service_name="$1"
if ! atlas_service_exists "$service_name"; then
atlas_log_warn "Service not found: ${service_name}"
return "${ATLAS_EXIT_PREREQ:-3}"
fi
atlas_log_info "Enabling service ${service_name}"
atlas_sudo_run systemctl enable "$service_name"
}
atlas_service_start() {
local service_name="$1"
if ! atlas_service_exists "$service_name"; then
atlas_log_warn "Service not found: ${service_name}"
return "${ATLAS_EXIT_PREREQ:-3}"
fi
atlas_log_info "Starting service ${service_name}"
atlas_sudo_run systemctl start "$service_name"
}
atlas_service_restart() {
local service_name="$1"
if ! atlas_service_exists "$service_name"; then
atlas_log_warn "Service not found: ${service_name}"
return "${ATLAS_EXIT_PREREQ:-3}"
fi
atlas_log_info "Restarting service ${service_name}"
atlas_sudo_run systemctl restart "$service_name"
}

68
install/lib/validation.sh Normal file
View File

@ -0,0 +1,68 @@
#!/usr/bin/env bash
if [[ -n "${ATLAS_VALIDATION_SH_LOADED:-}" ]]; then
return 0
fi
ATLAS_VALIDATION_SH_LOADED=1
ATLAS_VALIDATION_PASS=0
ATLAS_VALIDATION_WARNING=0
ATLAS_VALIDATION_FAIL=0
ATLAS_VALIDATION_SKIPPED=0
atlas_validation_reset() {
ATLAS_VALIDATION_PASS=0
ATLAS_VALIDATION_WARNING=0
ATLAS_VALIDATION_FAIL=0
ATLAS_VALIDATION_SKIPPED=0
}
atlas_validation_result() {
local status="$1"
local message="$2"
case "$status" in
PASS)
ATLAS_VALIDATION_PASS=$((ATLAS_VALIDATION_PASS + 1))
;;
WARNING)
ATLAS_VALIDATION_WARNING=$((ATLAS_VALIDATION_WARNING + 1))
;;
FAIL)
ATLAS_VALIDATION_FAIL=$((ATLAS_VALIDATION_FAIL + 1))
;;
SKIPPED)
ATLAS_VALIDATION_SKIPPED=$((ATLAS_VALIDATION_SKIPPED + 1))
;;
*)
status="FAIL"
ATLAS_VALIDATION_FAIL=$((ATLAS_VALIDATION_FAIL + 1))
;;
esac
printf '%-7s %s\n' "$status" "$message"
}
atlas_validation_summary() {
printf 'Summary: PASS=%s WARNING=%s FAIL=%s SKIPPED=%s\n' \
"$ATLAS_VALIDATION_PASS" \
"$ATLAS_VALIDATION_WARNING" \
"$ATLAS_VALIDATION_FAIL" \
"$ATLAS_VALIDATION_SKIPPED"
}
atlas_validation_exit_code() {
if [[ "$ATLAS_VALIDATION_FAIL" -gt 0 ]]; then
return "${ATLAS_EXIT_VALIDATION:-4}"
fi
return 0
}
atlas_bytes_available() {
local path="$1"
df -B1 --output=avail "$path" 2>/dev/null | awk 'NR == 2 {print $1}'
}
atlas_gib_to_bytes() {
local gib="$1"
awk -v gib="$gib" 'BEGIN {printf "%.0f\n", gib * 1024 * 1024 * 1024}'
}

View File

@ -0,0 +1,32 @@
# Atlas baseline command manifest
#
# Format:
# command-name|required|purpose
#
# The first field is a command name that should be resolvable through PATH.
# It is not a Debian package name. Package metadata belongs in a separate
# manifest if later installer phases need it.
#
# Lines beginning with # and blank lines are ignored.
bash|required|Shell used by Atlas automation
git|required|Repository operations
git-lfs|required|Large-file support for repositories that require it
node|required|Node.js runtime for Codex and JavaScript tooling
npm|required|Node.js package manager supplied with the Node.js installation
curl|required|HTTP client used by diagnostics and future repository setup
wget|required|Download client used by diagnostics and future repository setup
jq|required|JSON validation and report inspection
rsync|required|File synchronisation helper
tar|required|Archive handling
openssl|required|TLS and cryptographic diagnostics
python3|required|Portable JSON and text processing fallback
systemctl|required|systemd service inspection and management
ss|required|Listening socket inspection
findmnt|required|Mount inspection
df|required|Filesystem capacity inspection
awk|required|Text processing
sed|required|Text processing
grep|required|Text searching
sudo|required|Non-interactive privileged command execution

20
install/phases/README.md Normal file
View File

@ -0,0 +1,20 @@
# Installation Phases
This directory is reserved for future ordered Atlas installation phases.
Phase 2A intentionally does not create speculative .NET, SQL Server, nginx, or Docker installer scripts. Future scripts should be added only when their implementation phase is approved.
## Naming Convention
Use a two-digit phase number and lower-case hyphenated component name:
```text
10-dotnet.sh
20-sql-server.sh
30-nginx.sh
40-docker.sh
90-validate-platform.sh
```
Each phase must source the shared framework under `install/lib/`, support dry-run behaviour where practical, and follow [../../SERVER-STANDARDS.md](../../SERVER-STANDARDS.md).

View File

@ -10,6 +10,7 @@ This directory is for operational scripts used after Atlas is installed.
- Maintenance scripts.
- Monitoring scripts.
- Service validation helpers.
- Read-only system discovery and host validation scripts.
## What Must Not Be Stored Here
@ -33,10 +34,12 @@ Current planned areas include:
```text
scripts/backup/
scripts/development/
scripts/deploy/
scripts/maintenance/
scripts/monitoring/
scripts/restore/
scripts/system/
```
Additional areas may be added when a repeated operational need exists.
@ -47,3 +50,4 @@ Additional areas may be added when a repeated operational need exists.
- Do not store credentials in scripts.
- Make destructive actions explicit and documented.
- Include validation and rollback guidance for service-affecting scripts.
- Discovery and validation scripts must not dump environment variables, private keys, tokens, or complete sensitive configuration files.

View File

@ -0,0 +1,8 @@
# Development Scripts
This directory is reserved for development workflow helpers that do not belong to installation, deployment, backup, restore, monitoring, maintenance, or system validation.
Do not store secrets, generated logs, package caches, or one-off experiments here.
Future structure is `TODO: Confirm during the relevant implementation phase.`

331
scripts/system/discover-system.sh Executable file
View File

@ -0,0 +1,331 @@
#!/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"
atlas_set_script_name "discover-system.sh"
OUTPUT_FORMAT="human"
OUTPUT_PATH=""
usage() {
cat <<'USAGE'
Usage: scripts/system/discover-system.sh [options]
Read-only Atlas host discovery. Default output is human-readable text on stdout.
Options:
--help Show this help text.
--version Show script/framework version.
--json Emit valid JSON only.
--output PATH Write the report to PATH instead of stdout.
--verbose Include limited extra non-secret diagnostics.
USAGE
}
while [[ "$#" -gt 0 ]]; do
case "$1" in
--help)
usage
exit "${ATLAS_EXIT_OK}"
;;
--version)
atlas_print_version
exit "${ATLAS_EXIT_OK}"
;;
--json)
OUTPUT_FORMAT="json"
;;
--output)
if [[ "$#" -lt 2 || -z "$2" ]]; then
atlas_usage_error "--output requires a path"
exit "${ATLAS_EXIT_USAGE}"
fi
OUTPUT_PATH="$2"
shift
;;
--verbose)
atlas_enable_verbose
;;
*)
atlas_usage_error "Unknown option: $1"
exit "${ATLAS_EXIT_USAGE}"
;;
esac
shift
done
have() {
command -v "$1" >/dev/null 2>&1
}
value_or_unavailable() {
local command_name="$1"
shift
if have "$command_name"; then
"$@" 2>/dev/null || printf 'unavailable\n'
else
printf 'command unavailable: %s\n' "$command_name"
fi
}
first_line_or_unavailable() {
value_or_unavailable "$@" | sed -n '1p'
}
os_name() {
if [[ -r /etc/os-release ]]; then
. /etc/os-release
printf '%s\n' "${PRETTY_NAME:-unknown}"
else
printf 'unknown\n'
fi
}
pending_updates() {
if have apt; then
apt list --upgradable 2>/dev/null | awk 'NR > 1 {count++} END {print count + 0}'
else
printf 'unknown\n'
fi
}
reboot_required() {
if [[ -f /var/run/reboot-required ]]; then
printf 'yes\n'
else
printf 'no\n'
fi
}
component_version() {
local command_name="$1"
shift
if have "$command_name"; then
"$command_name" "$@" 2>&1 | sed -n '1p'
else
printf 'not installed\n'
fi
}
mount_status() {
local path="$1"
if have findmnt && findmnt -rn --target "$path" >/dev/null 2>&1; then
printf 'mounted\n'
elif [[ -e "$path" ]]; then
printf 'exists-not-mounted\n'
else
printf 'missing\n'
fi
}
local_host_overrides_present() {
if [[ -r /etc/hosts ]] && awk '($1 !~ /^#/ && $1 !~ /^(127\.|::1)/ && NF > 1) {found=1} END {exit found ? 0 : 1}' /etc/hosts; then
printf 'yes\n'
else
printf 'no\n'
fi
}
firewall_summary() {
if have ufw; then
sudo -n ufw status 2>/dev/null | sed -n '1p' || printf 'ufw status unavailable\n'
else
printf 'ufw not installed\n'
fi
}
time_sync_status() {
if have timedatectl; then
timedatectl show -p NTPSynchronized --value 2>/dev/null | awk '{print ($1 == "yes") ? "yes" : "no"}'
else
printf 'unknown\n'
fi
}
write_output() {
local content="$1"
if [[ -z "$OUTPUT_PATH" ]]; then
printf '%s\n' "$content"
return 0
fi
local output_dir
output_dir="$(dirname "$OUTPUT_PATH")"
if [[ ! -d "$output_dir" ]]; then
if [[ "$output_dir" == /var/log/atlas/reports* ]] && have sudo && sudo -n true >/dev/null 2>&1; then
sudo -n install -d -m 0750 -o "$(id -u)" -g "$(id -g)" "$output_dir"
else
install -d -m 0750 "$output_dir"
fi
fi
if [[ -w "$output_dir" ]]; then
printf '%s\n' "$content" >"$OUTPUT_PATH"
chmod 0640 "$OUTPUT_PATH"
elif have sudo && sudo -n true >/dev/null 2>&1; then
local tmp_file
tmp_file="$(mktemp)"
printf '%s\n' "$content" >"$tmp_file"
sudo -n install -m 0640 "$tmp_file" "$OUTPUT_PATH"
rm -f "$tmp_file"
else
printf 'ERROR: output path is not writable: %s\n' "$OUTPUT_PATH" >&2
return "${ATLAS_EXIT_USAGE}"
fi
}
human_report() {
{
printf 'Atlas System Discovery Report\n'
printf 'Generated: %s\n\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
printf 'Operating System\n'
printf ' Distribution: %s\n' "$(os_name)"
printf ' Kernel: %s\n' "$(uname -r)"
printf ' Architecture: %s\n' "$(uname -m)"
printf ' dpkg architecture: %s\n\n' "$(value_or_unavailable dpkg dpkg --print-architecture | sed -n '1p')"
printf 'Hardware\n'
printf ' CPU: %s\n' "$(first_line_or_unavailable lscpu lscpu | sed 's/^Architecture:.*/Architecture: see operating system section/')"
if have lscpu; then
lscpu | awk -F: '/Model name|CPU\\(s\\)|Core\\(s\\) per socket|Thread\\(s\\) per core/ {gsub(/^[ \t]+/, "", $2); printf " %s: %s\n", $1, $2}'
fi
printf ' Memory and swap:\n'
value_or_unavailable free free -h | sed 's/^/ /'
printf '\n'
printf 'Storage\n'
printf ' /sql status: %s\n' "$(mount_status /sql)"
printf ' /srv status: %s\n' "$(mount_status /srv)"
if have df; then
df -hT / /sql /srv 2>/dev/null | sed 's/^/ /'
fi
printf '\n'
printf 'Package State\n'
printf ' Pending package updates: %s\n' "$(pending_updates)"
printf ' Reboot required: %s\n\n' "$(reboot_required)"
printf 'Platform Components\n'
printf ' Bash: %s\n' "$(component_version bash --version)"
printf ' Git: %s\n' "$(component_version git --version)"
printf ' Git LFS: %s\n' "$(component_version git-lfs --version)"
printf ' Node.js: %s\n' "$(component_version node --version)"
printf ' npm: %s\n' "$(component_version npm --version)"
printf ' .NET: %s\n' "$(component_version dotnet --version)"
printf ' SQL Server command: %s\n' "$(component_version sqlservr --version)"
printf ' nginx: %s\n' "$(component_version nginx -v)"
printf ' Docker: %s\n' "$(component_version docker --version)"
printf ' Docker Compose: %s\n\n' "$(if have docker; then docker compose version 2>/dev/null | sed -n '1p'; else printf 'not installed'; fi)"
printf 'Services\n'
printf ' Failed systemd services:\n'
if have systemctl; then
systemctl --failed --no-legend --plain 2>/dev/null | awk 'NF {print " " $0} END {if (NR == 0) print " none"}'
else
printf ' systemctl unavailable\n'
fi
printf '\n'
printf 'Network\n'
printf ' Hostname: %s\n' "$(hostname)"
printf ' Local host overrides present: %s\n' "$(local_host_overrides_present)"
printf ' Listening TCP ports:\n'
if have ss; then
ss -ltn 2>/dev/null | sed 's/^/ /'
else
printf ' ss unavailable\n'
fi
printf ' Firewall: %s\n' "$(firewall_summary)"
printf '\n'
printf 'Time and DNS\n'
printf ' Time zone: %s\n' "$(if have timedatectl; then timedatectl show -p Timezone --value 2>/dev/null; else printf 'unknown'; fi)"
printf ' Time synchronised: %s\n' "$(time_sync_status)"
printf ' DNS configured: %s\n' "$(if have resolvectl && resolvectl dns >/dev/null 2>&1; then printf 'yes'; else printf 'unknown'; fi)"
}
}
json_report() {
if ! have jq; then
printf 'ERROR: --json requires jq\n' >&2
return "${ATLAS_EXIT_PREREQ}"
fi
local failed_services listening_ports filesystems
failed_services="$(if have systemctl; then systemctl --failed --no-legend --plain 2>/dev/null | awk '{print $1}'; fi)"
listening_ports="$(if have ss; then ss -ltnH 2>/dev/null | awk '{print $4}'; fi)"
filesystems="$(if have df; then df -B1 --output=target,fstype,size,used,avail,pcent / /sql /srv 2>/dev/null | awk 'NR > 1 {print $0}'; fi)"
jq -n \
--arg generated_at "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \
--arg os "$(os_name)" \
--arg kernel "$(uname -r)" \
--arg arch "$(uname -m)" \
--arg dpkg_arch "$(value_or_unavailable dpkg dpkg --print-architecture | sed -n '1p')" \
--arg cpu_model "$(if have lscpu; then lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}'; else printf 'unknown'; fi)" \
--arg memory "$(if have free; then free -h | awk '/^Mem:/ {print $2}'; else printf 'unknown'; fi)" \
--arg swap "$(if have free; then free -h | awk '/^Swap:/ {print $2}'; else printf 'unknown'; fi)" \
--arg sql_status "$(mount_status /sql)" \
--arg srv_status "$(mount_status /srv)" \
--arg pending_updates "$(pending_updates)" \
--arg reboot_required "$(reboot_required)" \
--arg bash_version "$(component_version bash --version)" \
--arg git_version "$(component_version git --version)" \
--arg node_version "$(component_version node --version)" \
--arg dotnet_version "$(component_version dotnet --version)" \
--arg sqlserver_version "$(component_version sqlservr --version)" \
--arg nginx_version "$(component_version nginx -v)" \
--arg docker_version "$(component_version docker --version)" \
--arg hostname "$(hostname)" \
--arg host_overrides "$(local_host_overrides_present)" \
--arg firewall "$(firewall_summary)" \
--arg timezone "$(if have timedatectl; then timedatectl show -p Timezone --value 2>/dev/null; else printf 'unknown'; fi)" \
--arg timesync "$(time_sync_status)" \
--arg dns_configured "$(if have resolvectl && resolvectl dns >/dev/null 2>&1; then printf 'yes'; else printf 'unknown'; fi)" \
--arg failed_services "$failed_services" \
--arg listening_ports "$listening_ports" \
--arg filesystems "$filesystems" \
'{
generated_at: $generated_at,
operating_system: {distribution: $os, kernel: $kernel, architecture: $arch, dpkg_architecture: $dpkg_arch},
hardware: {cpu_model: $cpu_model, memory_total: $memory, swap_total: $swap},
storage: {
sql_status: $sql_status,
srv_status: $srv_status,
filesystems: ($filesystems | split("\n") | map(select(length > 0)))
},
package_state: {pending_updates: ($pending_updates | tonumber? // $pending_updates), reboot_required: $reboot_required},
platform_components: {
bash: $bash_version,
git: $git_version,
node: $node_version,
dotnet: $dotnet_version,
sql_server: $sqlserver_version,
nginx: $nginx_version,
docker: $docker_version
},
services: {failed_systemd_units: ($failed_services | split("\n") | map(select(length > 0)))},
network: {
hostname: $hostname,
local_host_overrides_present: $host_overrides,
listening_tcp_addresses: ($listening_ports | split("\n") | map(select(length > 0))),
firewall: $firewall
},
time_and_dns: {timezone: $timezone, time_synchronised: $timesync, dns_configured: $dns_configured}
}'
}
if [[ "$OUTPUT_FORMAT" == "json" ]]; then
report="$(json_report)"
else
report="$(human_report)"
fi
write_output "$report"

273
scripts/system/validate-host.sh Executable file
View File

@ -0,0 +1,273 @@
#!/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