369 lines
12 KiB
Bash
Executable File
369 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
|
|
# shellcheck source=install/lib/common.sh
|
|
source "${REPO_ROOT}/install/lib/common.sh"
|
|
|
|
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
|
|
}
|
|
|
|
docker_status() {
|
|
if ! have docker; then
|
|
printf 'not installed\n'
|
|
elif sudo -n docker info >/dev/null 2>&1 || docker info >/dev/null 2>&1; then
|
|
printf 'active\n'
|
|
else
|
|
printf 'installed-not-active-or-not-accessible\n'
|
|
fi
|
|
}
|
|
|
|
sql_container_health() {
|
|
if ! have docker; then
|
|
printf 'docker unavailable\n'
|
|
else
|
|
sudo -n docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' atlas-sql-server 2>/dev/null || printf 'not present\n'
|
|
fi
|
|
}
|
|
|
|
nginx_status() {
|
|
if ! have nginx; then
|
|
printf 'not installed\n'
|
|
elif systemctl is-active --quiet nginx 2>/dev/null && sudo -n nginx -t >/dev/null 2>&1; then
|
|
printf 'active-config-valid\n'
|
|
else
|
|
printf 'installed-needs-attention\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 ' Docker status: %s\n' "$(docker_status)"
|
|
printf ' SQL Server container health: %s\n\n' "$(sql_container_health)"
|
|
printf ' nginx status: %s\n\n' "$(nginx_status)"
|
|
|
|
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 docker_status "$(docker_status)" \
|
|
--arg sql_container_health "$(sql_container_health)" \
|
|
--arg nginx_status "$(nginx_status)" \
|
|
--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,
|
|
docker_status: $docker_status,
|
|
sql_server_container_health: $sql_container_health,
|
|
nginx_status: $nginx_status
|
|
},
|
|
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"
|