Files
hx-ki.com2/COM2_REPAIR_ONE_SHOT.sh
2026-03-06 15:22:40 +00:00

169 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
DIR="/opt/hx-ki/com2-stack"
F="$DIR/docker-compose.yml"
NET="hxki-internal"
TS="$(date +%Y%m%d-%H%M%S)"
BK="$DIR/backup-$TS"
mkdir -p "$BK"
echo "=== COM2 REPAIR ONE-SHOT ==="
echo "Autorität: $F"
[ -f "$F" ] || { echo "FEHLT: $F"; exit 1; }
cp -a "$F" "$BK/docker-compose.yml.pre"
# 0) hxki-internal muss als external existieren
docker network inspect "$NET" >/dev/null 2>&1 || docker network create "$NET" >/dev/null
# 1) Service hxki-web wiederherstellen aus letztem Backup das hxki-web enthält
LATEST_WEB_SRC="$(ls -1t "$DIR"/backup-*/docker-compose.yml.orig 2>/dev/null | head -n1 || true)"
if ! grep -qE '^\s{2}hxki-web:\s*$' "$F"; then
echo "[1] hxki-web fehlt -> versuche Restore aus Backup"
if [ -z "${LATEST_WEB_SRC:-}" ] || ! grep -qE '^\s{2}hxki-web:\s*$' "$LATEST_WEB_SRC"; then
echo "FAIL: Kein Backup mit hxki-web gefunden. Lege eine Quelle bereit (z.B. altes Compose)."
echo " Erwartet: $DIR/backup-*/docker-compose.yml.orig mit ' hxki-web:' drin."
exit 1
fi
python3 - <<PY
import re, pathlib
dst = pathlib.Path("$F").read_text()
src = pathlib.Path("$LATEST_WEB_SRC").read_text()
def extract_service(text, name):
m = re.search(r'(?ms)^services:\s*\n(.*?)(?=^\S|\Z)', text)
if not m: raise SystemExit("FEHLT services: im Source")
block = m.group(1)
pat = rf'(?ms)^\s{{2}}{re.escape(name)}:\s*\n(?:^\s{{4}}.*\n|^\s{{6}}.*\n|^\s{{8}}.*\n|^\s*\n)*?(?=^\s{{2}}[A-Za-z0-9_.-]+:\s*$|\Z)'
mm = re.search(pat, block)
if not mm: raise SystemExit(f"Service {name} nicht im Source gefunden")
return mm.group(0)
web = extract_service(src, "hxki-web")
m = re.search(r'(?ms)^services:\s*\n', dst)
if not m: raise SystemExit("FEHLT services: im Ziel")
# direkt nach "services:\n" einfügen
dst2 = re.sub(r'(?ms)^(services:\s*\n)', r'\1' + web + "\n", dst, count=1)
pathlib.Path("$F").write_text(dst2)
print("OK: hxki-web aus Backup wieder eingefügt.")
PY
else
echo "[1] OK: hxki-web ist vorhanden"
fi
# 2) Caddy Ports nach außen sicherstellen (damit es "reagiert")
python3 - <<PY
import re, pathlib
p = pathlib.Path("$F")
s = p.read_text()
def ensure_ports_for(service, ports_lines):
# finde service block
m = re.search(r'(?ms)^services:\s*\n(.*?)(?=^\S|\Z)', s)
if not m: raise SystemExit("FEHLT services:")
block = m.group(1)
pat = rf'(?ms)^\s{{2}}{re.escape(service)}:\s*\n(?:^\s{{4}}.*\n|^\s{{6}}.*\n|^\s{{8}}.*\n|^\s*\n)*?(?=^\s{{2}}[A-Za-z0-9_.-]+:\s*$|\Z)'
mm = re.search(pat, block)
if not mm: raise SystemExit(f"FEHLT Service {service}")
sb = mm.group(0)
if re.search(r'(?m)^\s{4}ports:\s*$', sb):
# ports block vorhanden -> ergänze fehlende mappings
def has(line): return re.search(r'(?m)^\s{6}-\s*' + re.escape(line) + r'\s*$', sb)
add = [l for l in ports_lines if not has(l)]
if add:
sb2 = re.sub(r'(?m)^(\s{4}ports:\s*\n)', r'\1' + ''.join([f' - "{l}"\n' for l in add]), sb, count=1)
else:
sb2 = sb
else:
# kein ports block -> vor networks oder am ende einfügen
ins = " ports:\n" + ''.join([f' - "{l}"\n' for l in ports_lines])
if re.search(r'(?m)^\s{4}networks:\s*', sb):
sb2 = re.sub(r'(?m)^(\s{4}networks:\s*)', ins + r'\1', sb, count=1)
else:
sb2 = sb.rstrip() + "\n" + ins
return pat, sb, sb2
ports = ["80:80","443:443","443:443/udp","2019:2019"]
pat, old, new = ensure_ports_for("hx-caddy", ports)
if old != new:
s2 = re.sub(pat, new, s, count=1)
p.write_text(s2)
print("OK: hx-caddy ports nach außen gesetzt/ergänzt.")
else:
print("OK: hx-caddy ports waren schon da.")
PY
# 3) n8n muss im Container auf 0.0.0.0 lauschen (damit Caddy reinkommt)
python3 - <<PY
import re, pathlib
p = pathlib.Path("$F")
s = p.read_text()
def patch_env(service, key, val):
m = re.search(r'(?ms)^services:\s*\n(.*?)(?=^\S|\Z)', s)
if not m: raise SystemExit("FEHLT services:")
block = m.group(1)
pat = rf'(?ms)^\s{{2}}{re.escape(service)}:\s*\n(?:^\s{{4}}.*\n|^\s{{6}}.*\n|^\s{{8}}.*\n|^\s*\n)*?(?=^\s{{2}}[A-Za-z0-9_.-]+:\s*$|\Z)'
mm = re.search(pat, block)
if not mm: raise SystemExit(f"FEHLT Service {service}")
sb = mm.group(0)
if re.search(r'(?m)^\s{6}'+re.escape(key)+r':', sb):
sb2 = re.sub(r'(?m)^(\s{6}'+re.escape(key)+r':\s*).*$' , r'\1'+val, sb)
else:
if re.search(r'(?m)^\s{4}environment:\s*$', sb):
sb2 = re.sub(r'(?m)^(\s{4}environment:\s*\n)', r'\1' + f' {key}: {val}\n', sb, count=1)
else:
# env block vor volumes/networks einfügen
env = " environment:\n" + f" {key}: {val}\n"
if re.search(r'(?m)^\s{4}volumes:\s*', sb):
sb2 = re.sub(r'(?m)^(\s{4}volumes:\s*)', env + r'\1', sb, count=1)
elif re.search(r'(?m)^\s{4}networks:\s*', sb):
sb2 = re.sub(r'(?m)^(\s{4}networks:\s*)', env + r'\1', sb, count=1)
else:
sb2 = sb.rstrip() + "\n" + env
return pat, sb, sb2
pat, old, new = patch_env("hxki-n8n", "N8N_LISTEN_ADDRESS", "0.0.0.0")
if old != new:
s2 = re.sub(pat, new, s, count=1)
p.write_text(s2)
print("OK: hxki-n8n N8N_LISTEN_ADDRESS=0.0.0.0 gesetzt.")
else:
print("OK: hxki-n8n N8N_LISTEN_ADDRESS war schon ok.")
PY
# 4) Validate (harte Wahrheit)
echo "[V] Validate: docker compose config"
docker compose -f "$F" config >/dev/null
echo "OK: Compose valide."
# 5) Orchester neu starten
cd "$DIR"
docker compose down --remove-orphans || true
docker compose up -d --remove-orphans
# 6) n8n Listen-Check im Container
echo "[T] n8n lauscht?"
for i in {1..60}; do
if docker exec hxki-n8n sh -lc "ss -lnt | grep -q ':5678'"; then
echo "OK: n8n lauscht auf 5678."
break
fi
sleep 1
done
# 7) Caddy -> n8n intern
echo "[T] Caddy -> n8n?"
docker exec -it hx-caddy sh -lc 'wget -qO- http://hxki-n8n:5678/ >/dev/null && echo OK_CADDY_TO_N8N || echo FAIL_CADDY_TO_N8N'
# 8) Status
docker ps --format 'NAME={{.Names}} STATUS={{.Status}} PORTS={{.Ports}}'
echo "=== ENDE COM2 REPAIR ONE-SHOT ==="