Initial commit: PFI fleet inventory, stacks, tooling, and backup pipeline
Captures the full workspace state built up to this point:
- CLAUDE.md + README.md describing conventions and the four-host fleet
(ana-ml2, ana-docker, nh3-docker, esh-docker-vm).
- Per-host notes under servers/<host>/ with ssh-target fallback files
and latest system-details snapshots (two in-compose credential leaks
scrubbed; the upstream compose files still need to move those to .env).
- scripts/: server_inspect.sh (read-only remote diagnostic),
refresh-server-info.sh (dir-driven discovery + snapshot capture with
validation warnings), add-host.sh, sync-stacks.sh (pull
compose/conf trees), deploy-stack.sh (push with per-file diff + prompt).
- stacks/: canonical compose for backrest, beszel, dozzle, llama-swap,
rest-server-ana, rest-server-nh3, vllm-qwen3, plus the retired
infinity reference. All use the .env-driven + traefik-net + homepage
label pattern.
- configs/restic/ana-docker/: first resticprofile config + pre-backup
hook (Synapse pg_dump, Seafile mysqldump, Vaultwarden SQLite); templates
for the other three hosts to come.
- docs/pfi/: general infrastructure reference carried over.
- .gitignore excludes .env, stacks-mirror/, and assorted secret/state
filenames to prevent re-leaks on later commits.
This commit is contained in:
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
# server_inspect.sh — collect server details for writing Docker Compose files.
|
||||
#
|
||||
# Conventions this script assumes and reports on:
|
||||
# - Compose files: /opt/docker/compose/<stack>/{docker-compose.yml,compose.yaml}
|
||||
# - Config mounts: /opt/docker/conf/<stack>/...
|
||||
# - Named volumes preferred over bind mounts for persistent state.
|
||||
#
|
||||
# Safe: read-only. No modifications are made.
|
||||
# Usage:
|
||||
# bash server_inspect.sh # print to stdout
|
||||
# bash server_inspect.sh /tmp/report.txt # also save to file
|
||||
|
||||
set -u
|
||||
|
||||
OUT="${1:-}"
|
||||
if [ -n "$OUT" ]; then exec > >(tee "$OUT") 2>&1; fi
|
||||
|
||||
hr() { printf '\n===== %s =====\n\n' "$*"; }
|
||||
sub() { printf '\n----- %s -----\n' "$*"; }
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# --- HOST --------------------------------------------------------------------
|
||||
hr "HOST"
|
||||
echo "Hostname: $(hostname -f 2>/dev/null || hostname)"
|
||||
echo "Date: $(date -Iseconds)"
|
||||
echo "Uptime: $(uptime -p 2>/dev/null || uptime)"
|
||||
if [ -f /etc/os-release ]; then
|
||||
. /etc/os-release
|
||||
echo "OS: ${PRETTY_NAME:-unknown}"
|
||||
fi
|
||||
echo "Kernel: $(uname -r)"
|
||||
echo "Arch: $(uname -m)"
|
||||
|
||||
# --- HARDWARE ----------------------------------------------------------------
|
||||
hr "HARDWARE"
|
||||
if [ -f /proc/cpuinfo ]; then
|
||||
echo "CPU cores: $(grep -c ^processor /proc/cpuinfo)"
|
||||
echo "CPU model: $(awk -F: '/model name/ {print $2; exit}' /proc/cpuinfo | sed 's/^[[:space:]]*//')"
|
||||
fi
|
||||
if [ -f /proc/meminfo ]; then
|
||||
awk '/^MemTotal:|^MemAvailable:/ {printf "%-11s %.1f GB\n", $1, $2/1024/1024}' /proc/meminfo
|
||||
fi
|
||||
|
||||
# --- GPUS --------------------------------------------------------------------
|
||||
hr "GPUS"
|
||||
if have nvidia-smi; then
|
||||
nvidia-smi --query-gpu=index,name,memory.total,memory.free,driver_version --format=csv
|
||||
else
|
||||
echo "nvidia-smi not present (no NVIDIA GPUs or driver not installed)"
|
||||
fi
|
||||
|
||||
# --- FILESYSTEM --------------------------------------------------------------
|
||||
hr "FILESYSTEMS (df)"
|
||||
df -h -x tmpfs -x devtmpfs -x overlay 2>/dev/null
|
||||
|
||||
hr "PERSISTENT MOUNTS (/etc/fstab, non-comment)"
|
||||
if [ -r /etc/fstab ]; then
|
||||
grep -vE '^\s*(#|$)' /etc/fstab
|
||||
fi
|
||||
|
||||
hr "TARGETED DATA PATHS"
|
||||
for path in /tank /models /opt /opt/docker /opt/docker/compose /opt/docker/conf \
|
||||
/var/lib/docker /data /srv; do
|
||||
if [ -d "$path" ]; then
|
||||
size=$(du -sh "$path" 2>/dev/null | awk '{print $1}')
|
||||
echo "$path (total: ${size:-?})"
|
||||
ls -la --time-style=long-iso "$path" 2>/dev/null | sed 's/^/ /' | head -30
|
||||
echo
|
||||
fi
|
||||
done
|
||||
|
||||
# --- DOCKER ------------------------------------------------------------------
|
||||
hr "DOCKER"
|
||||
if ! have docker; then
|
||||
echo "docker not installed"
|
||||
else
|
||||
docker version --format 'Server: {{.Server.Version}} Client: {{.Client.Version}}' 2>/dev/null \
|
||||
|| echo "docker daemon not reachable by current user"
|
||||
|
||||
sub "docker info"
|
||||
docker info --format 'Containers: {{.Containers}} (running {{.ContainersRunning}}, paused {{.ContainersPaused}}, stopped {{.ContainersStopped}})
|
||||
Images: {{.Images}}
|
||||
Runtimes: {{.Runtimes}}
|
||||
Default runtime: {{.DefaultRuntime}}
|
||||
Storage driver: {{.Driver}}
|
||||
Root dir: {{.DockerRootDir}}
|
||||
Server version: {{.ServerVersion}}' 2>/dev/null
|
||||
|
||||
sub "running containers"
|
||||
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}' 2>/dev/null
|
||||
|
||||
sub "all containers"
|
||||
docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' 2>/dev/null
|
||||
|
||||
sub "networks"
|
||||
docker network ls --format 'table {{.Name}}\t{{.Driver}}\t{{.Scope}}' 2>/dev/null
|
||||
|
||||
sub "networks (external, non-default — worth knowing for compose external: true)"
|
||||
docker network ls --filter driver=bridge --format '{{.Name}}' 2>/dev/null \
|
||||
| grep -vE '^(bridge|host|none)$' || true
|
||||
|
||||
sub "named volumes"
|
||||
docker volume ls --format 'table {{.Name}}\t{{.Driver}}' 2>/dev/null
|
||||
|
||||
sub "compose projects currently running"
|
||||
docker ps --format '{{.Label "com.docker.compose.project"}}' 2>/dev/null \
|
||||
| sort -u | grep -v '^$' || echo "(none)"
|
||||
fi
|
||||
|
||||
# --- COMPOSE FILES -----------------------------------------------------------
|
||||
hr "COMPOSE FILES (/opt/docker/compose/)"
|
||||
if [ -d /opt/docker/compose ]; then
|
||||
find /opt/docker/compose -maxdepth 3 -type f \
|
||||
\( -name 'docker-compose.y*ml' -o -name 'compose.y*ml' \) 2>/dev/null \
|
||||
| sort | while read -r f; do
|
||||
printf '\n>>> %s\n' "$f"
|
||||
cat "$f"
|
||||
done
|
||||
else
|
||||
echo "/opt/docker/compose not present"
|
||||
fi
|
||||
|
||||
# --- CONFIG LAYOUT -----------------------------------------------------------
|
||||
hr "CONFIG LAYOUT (/opt/docker/conf/ — top 200 entries)"
|
||||
if [ -d /opt/docker/conf ]; then
|
||||
find /opt/docker/conf -maxdepth 4 2>/dev/null | sort | head -200
|
||||
else
|
||||
echo "/opt/docker/conf not present"
|
||||
fi
|
||||
|
||||
# --- PORTS -------------------------------------------------------------------
|
||||
hr "LISTENING PORTS"
|
||||
if have ss; then
|
||||
ss -tlnH 2>/dev/null | awk '{print $4}' | sort -u
|
||||
elif have netstat; then
|
||||
netstat -tln 2>/dev/null | awk 'NR>2 {print $4}' | sort -u
|
||||
else
|
||||
echo "ss and netstat both unavailable"
|
||||
fi
|
||||
|
||||
# --- HF / MODEL CACHES -------------------------------------------------------
|
||||
hr "MODEL / HUGGINGFACE CACHES"
|
||||
for path in /tank/aimodels/huggingface /tank/aimodels/llm ~/.cache/huggingface \
|
||||
/data/huggingface /opt/huggingface; do
|
||||
if [ -d "$path" ]; then
|
||||
size=$(du -sh "$path" 2>/dev/null | awk '{print $1}')
|
||||
echo "$path (${size:-?})"
|
||||
if [ -d "$path/hub" ]; then
|
||||
echo " hub entries:"
|
||||
ls "$path/hub" 2>/dev/null | sed 's/^/ /' | head -30
|
||||
fi
|
||||
echo
|
||||
fi
|
||||
done
|
||||
|
||||
# --- SYSTEMD SERVICES (docker-adjacent) --------------------------------------
|
||||
hr "DOCKER-ADJACENT SYSTEMD SERVICES"
|
||||
if have systemctl; then
|
||||
systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null \
|
||||
| awk '{print $1, $4}' \
|
||||
| grep -Ei 'docker|container|traefik|nvidia' || echo "(none matching)"
|
||||
fi
|
||||
|
||||
hr "DONE"
|
||||
echo "Paste the above back into the chat, or pass a path as argv[1] to save."
|
||||
Reference in New Issue
Block a user