- Добавлена полная поддержка 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
		
			
				
	
	
		
			144 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			144 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/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()
 |