Add Phase 2A installer framework and host validation
This commit is contained in:
parent
451b5b28d7
commit
6ff3a79cea
7
.gitignore
vendored
7
.gitignore
vendored
@ -38,6 +38,7 @@ overrides/
|
|||||||
*.log
|
*.log
|
||||||
logs/
|
logs/
|
||||||
log/
|
log/
|
||||||
|
atlas-logs/
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
tmp/
|
tmp/
|
||||||
@ -83,6 +84,12 @@ releases/
|
|||||||
*.sql.gz
|
*.sql.gz
|
||||||
*.sql.zip
|
*.sql.zip
|
||||||
|
|
||||||
|
# Generated Atlas reports
|
||||||
|
reports/
|
||||||
|
atlas-reports/
|
||||||
|
*.report.json
|
||||||
|
*.report.txt
|
||||||
|
|
||||||
# Package caches
|
# Package caches
|
||||||
.npm/
|
.npm/
|
||||||
.pnpm-store/
|
.pnpm-store/
|
||||||
|
|||||||
36
README.md
36
README.md
@ -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.
|
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
|
Current commands:
|
||||||
install/01-base-tools.sh
|
|
||||||
install/02-dotnet.sh
|
```bash
|
||||||
install/03-sqlserver.sh
|
install/bootstrap.sh --help
|
||||||
install/04-nginx.sh
|
scripts/system/discover-system.sh
|
||||||
install/05-docker.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
|
## Change Control
|
||||||
|
|
||||||
@ -96,7 +111,8 @@ These scripts do not exist yet. Until they do, rebuild steps remain manual and a
|
|||||||
## Roadmap Summary
|
## Roadmap Summary
|
||||||
|
|
||||||
- Phase 1: Establish repository structure, standards, architecture documentation, rebuild guidance, disaster recovery guidance, operating model, and ADR process.
|
- 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
|
## Security Warning
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,8 @@ Unknowns must be written exactly as `TODO: Confirm during the relevant implement
|
|||||||
- Print concise progress messages.
|
- Print concise progress messages.
|
||||||
- Use consistent prefixes: `[INFO]`, `[WARN]`, `[ERROR]`, `[OK]`.
|
- Use consistent prefixes: `[INFO]`, `[WARN]`, `[ERROR]`, `[OK]`.
|
||||||
- Do not print secrets, tokens, passwords, connection strings, private keys, or certificate material.
|
- 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.
|
- Repository scripts may write temporary logs only under documented temporary paths and must clean them up where practical.
|
||||||
|
|
||||||
## Exit Codes
|
## 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.
|
- Do not change ownership recursively outside documented paths.
|
||||||
- Secrets outside the repository must use the narrowest practical permissions.
|
- Secrets outside the repository must use the narrowest practical permissions.
|
||||||
- Templates in the repository must not contain live secrets.
|
- Templates in the repository must not contain live secrets.
|
||||||
|
- Future persistent installer state and marker files belong under `/var/lib/atlas/`.
|
||||||
|
|
||||||
## Configuration Locations
|
## 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 avoid requiring secrets where practical.
|
||||||
- Tests must not mutate production data unless explicitly designed and approved.
|
- Tests must not mutate production data unless explicitly designed and approved.
|
||||||
- Run relevant tests before committing.
|
- 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
|
## Documentation Requirements
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
The repository is intended to grow into the version-controlled control plane for Atlas. Planned components include:
|
||||||
|
|
||||||
- Idempotent installation scripts under `install/`.
|
- Idempotent installation scripts under `install/`.
|
||||||
|
- Shared installer framework libraries under `install/lib/`.
|
||||||
- Managed configuration templates under `config/` and `templates/`.
|
- Managed configuration templates under `config/` and `templates/`.
|
||||||
- Operational scripts under `scripts/`.
|
- Operational scripts under `scripts/`.
|
||||||
- Smoke tests under `tests/`.
|
- Smoke tests under `tests/`.
|
||||||
@ -121,6 +122,9 @@ The intended storage and responsibility model is:
|
|||||||
|
|
||||||
- Source repositories: `/srv/repos`
|
- Source repositories: `/srv/repos`
|
||||||
- Infrastructure repository: `/srv/repos/AI-Development-Server`
|
- 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.`
|
- Build artefacts: `TODO: Confirm during the relevant implementation phase.`
|
||||||
- Deployed applications: `TODO: Confirm during the relevant implementation phase.`
|
- Deployed applications: `TODO: Confirm during the relevant implementation phase.`
|
||||||
- SQL data: `/sql`
|
- SQL data: `/sql`
|
||||||
|
|||||||
@ -6,6 +6,58 @@ This file records chronological Atlas build and infrastructure milestones. Do no
|
|||||||
|
|
||||||
### Date
|
### 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.
|
Not recorded.
|
||||||
|
|
||||||
### Phase
|
### Phase
|
||||||
|
|||||||
@ -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.
|
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
|
## Rollback Expectations
|
||||||
|
|
||||||
- Prefer changes that can be reversed through Git and idempotent scripts.
|
- Prefer changes that can be reversed through Git and idempotent scripts.
|
||||||
|
|||||||
@ -5,7 +5,10 @@ This guide describes the planned rebuild path for Atlas. At Phase 1, the reposit
|
|||||||
## Automation Status
|
## Automation Status
|
||||||
|
|
||||||
- Repository structure and documentation: present.
|
- 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.
|
- Configuration templates: not yet created.
|
||||||
- Operational scripts: not yet created.
|
- Operational scripts: not yet created.
|
||||||
- Smoke tests: not yet created.
|
- Smoke tests: not yet created.
|
||||||
@ -106,9 +109,11 @@ Planned bootstrap flow:
|
|||||||
1. Review [README.md](../README.md).
|
1. Review [README.md](../README.md).
|
||||||
2. Review [SERVER-STANDARDS.md](../SERVER-STANDARDS.md).
|
2. Review [SERVER-STANDARDS.md](../SERVER-STANDARDS.md).
|
||||||
3. Review this rebuild guide.
|
3. Review this rebuild guide.
|
||||||
4. Run base installation validation.
|
4. Run `scripts/system/discover-system.sh`.
|
||||||
5. Run ordered installation scripts from `install/` when they exist.
|
5. Run `scripts/system/validate-host.sh`.
|
||||||
6. Run smoke tests from `tests/` when they exist.
|
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.`
|
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:
|
Planned scripts may include:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
install/01-base-tools.sh
|
install/phases/10-dotnet.sh
|
||||||
install/02-dotnet.sh
|
install/phases/20-sql-server.sh
|
||||||
install/03-sqlserver.sh
|
install/phases/30-nginx.sh
|
||||||
install/04-nginx.sh
|
install/phases/40-docker.sh
|
||||||
install/05-docker.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
These scripts do not exist yet. When created, each script must:
|
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.
|
- Required installation phases have run successfully.
|
||||||
- Smoke tests pass.
|
- 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
|
## 9. Restoring Services and Data
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
This directory is reserved for ordered, idempotent installation scripts for Atlas.
|
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
|
## What Belongs Here
|
||||||
|
|
||||||
- Bootstrap scripts.
|
- Bootstrap scripts.
|
||||||
@ -9,6 +11,8 @@ This directory is reserved for ordered, idempotent installation scripts for Atla
|
|||||||
- Service installation scripts.
|
- Service installation scripts.
|
||||||
- Storage, runtime, and tooling setup scripts.
|
- Storage, runtime, and tooling setup scripts.
|
||||||
- Validation wrappers for installation phases.
|
- Validation wrappers for installation phases.
|
||||||
|
- Shared Bash framework libraries under `lib/`.
|
||||||
|
- Component manifests under `manifests/`.
|
||||||
|
|
||||||
## What Must Not Be Stored Here
|
## What Must Not Be Stored Here
|
||||||
|
|
||||||
@ -21,23 +25,34 @@ This directory is reserved for ordered, idempotent installation scripts for Atla
|
|||||||
|
|
||||||
## Naming Conventions
|
## 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
|
```text
|
||||||
01-base-tools.sh
|
phases/10-dotnet.sh
|
||||||
02-dotnet.sh
|
phases/20-sql-server.sh
|
||||||
03-sqlserver.sh
|
phases/30-nginx.sh
|
||||||
04-nginx.sh
|
phases/40-docker.sh
|
||||||
05-docker.sh
|
phases/90-validate-platform.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Scripts must follow [SERVER-STANDARDS.md](../SERVER-STANDARDS.md).
|
Scripts must follow [SERVER-STANDARDS.md](../SERVER-STANDARDS.md).
|
||||||
|
|
||||||
## Expected Future Structure
|
## 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
|
## Security Considerations
|
||||||
|
|
||||||
|
|||||||
101
install/bootstrap.sh
Executable file
101
install/bootstrap.sh
Executable 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
113
install/lib/common.sh
Normal 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
120
install/lib/files.sh
Normal 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
84
install/lib/logging.sh
Normal 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
44
install/lib/packages.sh
Normal 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
58
install/lib/services.sh
Normal 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
68
install/lib/validation.sh
Normal 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}'
|
||||||
|
}
|
||||||
|
|
||||||
32
install/manifests/baseline-commands.txt
Normal file
32
install/manifests/baseline-commands.txt
Normal 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
20
install/phases/README.md
Normal 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).
|
||||||
|
|
||||||
@ -10,6 +10,7 @@ This directory is for operational scripts used after Atlas is installed.
|
|||||||
- Maintenance scripts.
|
- Maintenance scripts.
|
||||||
- Monitoring scripts.
|
- Monitoring scripts.
|
||||||
- Service validation helpers.
|
- Service validation helpers.
|
||||||
|
- Read-only system discovery and host validation scripts.
|
||||||
|
|
||||||
## What Must Not Be Stored Here
|
## What Must Not Be Stored Here
|
||||||
|
|
||||||
@ -33,10 +34,12 @@ Current planned areas include:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
scripts/backup/
|
scripts/backup/
|
||||||
|
scripts/development/
|
||||||
scripts/deploy/
|
scripts/deploy/
|
||||||
scripts/maintenance/
|
scripts/maintenance/
|
||||||
scripts/monitoring/
|
scripts/monitoring/
|
||||||
scripts/restore/
|
scripts/restore/
|
||||||
|
scripts/system/
|
||||||
```
|
```
|
||||||
|
|
||||||
Additional areas may be added when a repeated operational need exists.
|
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.
|
- Do not store credentials in scripts.
|
||||||
- Make destructive actions explicit and documented.
|
- Make destructive actions explicit and documented.
|
||||||
- Include validation and rollback guidance for service-affecting scripts.
|
- 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.
|
||||||
|
|||||||
8
scripts/development/README.md
Normal file
8
scripts/development/README.md
Normal 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
331
scripts/system/discover-system.sh
Executable 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
273
scripts/system/validate-host.sh
Executable 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
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user