feat: Завершена реализация универсальной лаборатории

- Добавлена полная поддержка Istio service mesh с Kiali
- Интегрированы Helm charts (nginx, prometheus-stack)
- Созданы Grafana дашборды для Istio мониторинга
- Добавлен HTML генератор отчетов с красивым дизайном
- Созданы скрипты для снапшотов и восстановления
- Добавлена поддержка Istio Bookinfo demo
- Обновлена документация с полным описанием возможностей

Компоненты:
- Istio с Telemetry и Traffic Policy
- Prometheus + Grafana с автопровижинингом дашбордов
- HTML отчеты с анализом статусов
- Снапшоты и восстановление состояния
- Полная интеграция с Kubernetes

Автор: Сергей Антропов
Сайт: https://devops.org.ru
This commit is contained in:
2025-10-22 13:08:55 +03:00
parent b4881da7c5
commit 33ada54c12
13 changed files with 712 additions and 2 deletions

12
scripts/cleanup.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Очистка лаборатории
# Автор: Сергей Антропов
# Сайт: https://devops.org.ru
set -euo pipefail
echo "[cleanup] removing lab containers/volumes/networks"
docker ps -aq --filter "label=ansible.lab=true" | xargs -r docker rm -f
docker volume ls -q --filter "name=_docker$" | xargs -r docker volume rm
docker network rm labnet >/dev/null 2>&1 || true
echo "done."

143
scripts/report_html.py Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Генератор HTML отчетов для универсальной лаборатории
Автор: Сергей Антропов
Сайт: https://devops.org.ru
"""
import sys
import json
import html
import datetime
def main():
if len(sys.argv) < 3:
print("Usage: report_html.py <input.json> <output.html>")
sys.exit(1)
inp, outp = sys.argv[1], sys.argv[2]
try:
with open(inp, 'r', encoding='utf-8') as f:
data = json.load(f)
except Exception as e:
data = {"error": f"failed to read json: {e}"}
ts = data.get("timestamp", datetime.datetime.utcnow().isoformat())
idemp_raw = data.get("idempotence_raw", "")
haproxy_sel = data.get("haproxy_select1", "")
helm_ingress_toolbox = data.get("helm_ingress_toolbox_raw", "")
istio_kiali = data.get("istio_kiali_raw", "")
istio_bookinfo = data.get("istio_bookinfo_raw", "")
k8s_overview = data.get("k8s_overview_raw", "")
def badge(label, ok):
color = "#10b981" if ok else "#ef4444"
return f'<span class="badge" style="background:{color}">{html.escape(label)}</span>'
# Анализ статусов
idempotent_ok = ("changed=0" in idemp_raw)
haproxy_ok = (haproxy_sel.strip() == "1") if haproxy_sel else None
ingress_ok = ("ingress" in helm_ingress_toolbox.lower()) or ("curl http://localhost" in helm_ingress_toolbox.lower())
toolbox_ok = ("deploy/toolbox" in helm_ingress_toolbox.lower()) or ("rollout status" in helm_ingress_toolbox.lower())
istio_ok = ("istio-system" in istio_kiali.lower()) and ("istiod" in istio_kiali.lower())
kiali_ok = ("kiali" in istio_kiali.lower())
bookinfo_ok = ("bookinfo" in istio_bookinfo.lower()) or ("productpage" in istio_bookinfo.lower())
k8s_ok = ("nodes" in k8s_overview.lower()) and ("pods" in k8s_overview.lower())
def maybe_badge(label, status):
if status is None:
return f'<span class="badge" style="background:#6b7280">{html.escape(label)}: n/a</span>'
return badge(f"{label}", bool(status))
html_doc = f"""<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Lab Report</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<style>
:root {{ --bg:#0f172a; --card:#111827; --muted:#94a3b8; --fg:#e5e7eb; --accent:#38bdf8; }}
* {{ box-sizing:border-box; }}
body {{ margin:0; font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial;
background:linear-gradient(180deg,#0b1220,#0f172a); color:var(--fg); }}
.container {{ max-width:1100px; margin:40px auto; padding:0 16px; }}
.hdr {{ display:flex; align-items:center; gap:14px; flex-wrap:wrap; }}
.hdr h1 {{ margin:0; font-size:28px; }}
.time {{ color:var(--muted); font-size:14px; }}
.grid {{ display:grid; grid-template-columns:1fr; gap:16px; margin-top:20px; }}
.card {{ background:rgba(17,24,39,.7); border:1px solid rgba(148,163,184,.15);
border-radius:14px; padding:16px; box-shadow:0 10px 30px rgba(0,0,0,.25); backdrop-filter: blur(6px); }}
.card h2 {{ margin:0 0 10px 0; font-size:18px; color:#c7d2fe; }}
pre {{ margin:0; padding:12px; background:#0b1220; border-radius:10px; overflow:auto; font-size:12px; line-height:1.4; }}
.badge {{ display:inline-block; padding:4px 10px; border-radius:999px; font-size:12px; color:white; }}
.kv {{ display:flex; flex-wrap:wrap; gap:8px; margin:8px 0 0 0; }}
footer {{ margin:24px 0 40px; color:var(--muted); font-size:12px; text-align:center; }}
a {{ color: var(--accent); text-decoration:none; }} a:hover {{ text-decoration:underline; }}
</style>
</head>
<body>
<div class="container">
<div class="hdr">
<h1>Ansible Lab Report</h1>
<div class="time">generated: {html.escape(ts)}</div>
</div>
<div class="grid">
<div class="card">
<h2>Summary</h2>
<div class="kv">
{badge("Idempotent", idempotent_ok)}
{maybe_badge("HAProxy SELECT=1", haproxy_ok)}
{maybe_badge("Ingress ready", ingress_ok)}
{maybe_badge("Toolbox ready", toolbox_ok)}
{maybe_badge("Istio ready", istio_ok)}
{maybe_badge("Kiali ready", kiali_ok)}
{maybe_badge("Bookinfo ready", bookinfo_ok)}
{maybe_badge("K8s ready", k8s_ok)}
</div>
</div>
<div class="card">
<h2>Idempotence (raw)</h2>
<pre>{html.escape(idemp_raw) if idemp_raw else "n/a"}</pre>
</div>
<div class="card">
<h2>Helm + Ingress + Toolbox (raw)</h2>
<pre>{html.escape(helm_ingress_toolbox) if helm_ingress_toolbox else "n/a"}</pre>
</div>
<div class="card">
<h2>Istio / Kiali (raw)</h2>
<pre>{html.escape(istio_kiali) if istio_kiali else "n/a"}</pre>
</div>
<div class="card">
<h2>Istio Bookinfo (raw)</h2>
<pre>{html.escape(istio_bookinfo) if istio_bookinfo else "n/a"}</pre>
</div>
<div class="card">
<h2>K8s Overview (raw)</h2>
<pre>{html.escape(k8s_overview) if k8s_overview else "n/a"}</pre>
</div>
<div class="card">
<h2>HAProxy SELECT 1</h2>
<pre>{html.escape(haproxy_sel) if haproxy_sel else "n/a"}</pre>
</div>
</div>
<footer>HTML from /ansible/reports/lab-health.json · kubeconfigs in reports/kubeconfigs/</footer>
</div>
</body>
</html>"""
with open(outp, "w", encoding="utf-8") as f:
f.write(html_doc)
print(outp)
if __name__ == "__main__":
main()

25
scripts/restore.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# Восстановление лаборатории из снапшотов
# Автор: Сергей Антропов
# Сайт: https://devops.org.ru
set -euo pipefail
IN_DIR="snapshots"
if [ ! -d "$IN_DIR" ]; then
echo "No snapshots dir"
exit 1
fi
for f in "$IN_DIR"/*.image; do
if [ ! -f "$f" ]; then
continue
fi
name=$(basename "$f" .image)
img=$(cat "$f")
echo "[restore] $name from $img"
docker rm -f "$name" >/dev/null 2>&1 || true
docker run -d --name "$name" "$img" >/dev/null
done
echo "Restored from $IN_DIR/"

26
scripts/snapshot.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Снапшот лаборатории
# Автор: Сергей Антропов
# Сайт: https://devops.org.ru
set -euo pipefail
OUT_DIR="snapshots"
mkdir -p "$OUT_DIR"
# Найти все контейнеры лаборатории
ids=$(docker ps -q --filter "label=ansible.lab=true")
if [ -z "$ids" ]; then
echo "No lab containers to snapshot"
exit 0
fi
for id in $ids; do
name=$(docker inspect --format '{{.Name}}' "$id" | sed 's#^/##')
img="lab-snap-$name:latest"
echo "[snapshot] $name -> $img"
docker commit "$id" "$img" >/dev/null
echo "$img" > "$OUT_DIR/$name.image"
done
echo "Snapshots saved to $OUT_DIR/"