feat: add post-deploy verification checklist and script
This commit is contained in:
278
docs/checklists/DEPLOY_VERIFICATION_CHECKLIST_v1.md
Normal file
278
docs/checklists/DEPLOY_VERIFICATION_CHECKLIST_v1.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# DEPLOY_VERIFICATION_CHECKLIST_v1
|
||||
|
||||
Цей список питань потрібно ставити перед кожним деплоєм, щоб гарантувати, що жодна з останніх ~30+ розробок не зламається і що весь ланцюг *Нода → Агенти → DAGI → microdao → UI* залишається консистентним.
|
||||
|
||||
---
|
||||
|
||||
## I. **Node Registry / Node Cabinet**
|
||||
|
||||
### 1. Чи всі ноди зареєстровані в `node_registry`?
|
||||
|
||||
### 2. Чи працює `POST /internal/nodes/register-or-update`?
|
||||
|
||||
### 3. Чи показує `/api/v1/nodes` правильну кількість нод?
|
||||
|
||||
### 4. Чи зникли ноди з UI після оновлення? Чи це помилка API чи реальний стан?
|
||||
|
||||
### 5. Чи у всіх нод є свіжий `last_heartbeat`?
|
||||
|
||||
---
|
||||
|
||||
## II. **Node Metrics (GPU/CPU/RAM/Disk)**
|
||||
|
||||
### 6. Чи повертає `/internal/node/{id}/metrics/current`:
|
||||
|
||||
* GPU модель
|
||||
|
||||
* GPU memory total/free
|
||||
|
||||
* CPU load
|
||||
|
||||
* RAM usage
|
||||
|
||||
* Disk usage
|
||||
|
||||
### 7. Чи метрики не “обнулилися” після деплою?
|
||||
|
||||
### 8. Чи не зникли агенти в Node Cabinet через порожній `node_cache`?
|
||||
|
||||
---
|
||||
|
||||
## III. **Node Core Agents (8 агентів DAOS)**
|
||||
|
||||
Для кожної ноди:
|
||||
|
||||
### 9. Чи існують:
|
||||
|
||||
* Node Guardian
|
||||
|
||||
* Node Steward
|
||||
|
||||
* DAGI Router Agent
|
||||
|
||||
* Swapper Agent
|
||||
|
||||
* Multimodal Agent
|
||||
|
||||
* Tools & Planner Agent
|
||||
|
||||
* Security/Sentinel Agent
|
||||
|
||||
* Archivist Agent (якщо увімкнено)
|
||||
|
||||
### 10. Чи всі вони мають `node_id`?
|
||||
|
||||
### 11. Чи всі мають `public_slug` → UI кабінет відкривається?
|
||||
|
||||
### 12. Чи всі мають System Prompts (`core` обов’язково, де треба — `safety`)?
|
||||
|
||||
### 13. Чи видно їх у `/nodes/{nodeId}` у відповідних секціях?
|
||||
|
||||
---
|
||||
|
||||
## IV. **DAGI Router / DAGI Audit**
|
||||
|
||||
### 14. Чи працює `/internal/node/{id}/dagi-router/agents`?
|
||||
|
||||
### 15. Чи є `router_total >= 1` і `system_total >= 1`?
|
||||
|
||||
### 16. Чи працює DAGI-agent autosync?
|
||||
|
||||
### 17. Чи phantom/stale ≤ 20 (або інший поріг)?
|
||||
|
||||
### 18. Чи запускається `POST /dagi-audit/run` без помилок?
|
||||
|
||||
### 19. Чи DAGI Router Agent бачить сервіс у `/healthz`?
|
||||
|
||||
---
|
||||
|
||||
## V. **Swapper Service / Models**
|
||||
|
||||
### 20. Чи Swapper відповідає на `/healthz`?
|
||||
|
||||
### 21. Чи моделі завантажені (`/api/models`)?
|
||||
|
||||
### 22. Чи мінімальний набір моделей присутній?
|
||||
|
||||
### 23. Чи VRAM usage коректний після restart?
|
||||
|
||||
### 24. Чи Swapper Agent може робити pull/unload моделей?
|
||||
|
||||
---
|
||||
|
||||
## VI. **Multimodal Stack (повний)**
|
||||
|
||||
Для кожної ноди:
|
||||
|
||||
### 25. Чи працюють:
|
||||
|
||||
* STT (Speech-to-Text)?
|
||||
|
||||
* TTS?
|
||||
|
||||
* OCR?
|
||||
|
||||
* Image Understanding?
|
||||
|
||||
* Document parsing (PDF/DOCX)?
|
||||
|
||||
* Embeddings?
|
||||
|
||||
* Keyframe Extraction (відео)?
|
||||
|
||||
### 26. Чи Multimodal Agent має prompts з повним переліком функцій?
|
||||
|
||||
### 27. Чи healthz STT/OCR/VLM сервісів працюють?
|
||||
|
||||
---
|
||||
|
||||
## VII. **MicroDAO / Districts / Rooms**
|
||||
|
||||
### 28. Чи працюють:
|
||||
|
||||
* GET `/api/v1/districts`
|
||||
|
||||
* GET `/api/v1/districts/{slug}`
|
||||
|
||||
* GET `/city/microdao/{slug}/rooms`
|
||||
|
||||
* GET `/city/microdao/{slug}/agents`
|
||||
|
||||
### 29. Чи District Portal показує lead/core agents?
|
||||
|
||||
### 30. Чи MicroDAO Agents Section відображає badges/roles?
|
||||
|
||||
### 31. Чи всі rooms відображаються (operations/treasury/events/...)?
|
||||
|
||||
---
|
||||
|
||||
## VIII. **Agents System Prompts MVP**
|
||||
|
||||
### 32. Чи всі core-агенти Міста мають `core` prompts:
|
||||
|
||||
* DAARWIZZ
|
||||
|
||||
* DARIA
|
||||
|
||||
* DARIO
|
||||
|
||||
* SOUL
|
||||
|
||||
* Spirit
|
||||
|
||||
* Logic
|
||||
|
||||
* Helion
|
||||
|
||||
* GREENFOOD ERP
|
||||
|
||||
### 33. Чи працює UI вкладка “System Prompts”?
|
||||
|
||||
### 34. Чи `GET/PUT /api/v1/agents/{id}/prompts` працює без помилок?
|
||||
|
||||
### 35. Чи DAGI Router правильно підтягує prompts при runtime?
|
||||
|
||||
---
|
||||
|
||||
## IX. **Node Self-Healing**
|
||||
|
||||
### 36. Чи Node Guardian запускає перевірки?
|
||||
|
||||
### 37. Чи Self-healing event-и пишуться в NATS?
|
||||
|
||||
### 38. Чи DAGI Router Agent може:
|
||||
|
||||
* перезапустити router
|
||||
|
||||
* запустити audit
|
||||
|
||||
* синхронізувати phantom/stale
|
||||
|
||||
### 39. Чи Swapper Agent може:
|
||||
|
||||
* відновити моделі
|
||||
|
||||
* зробити pull
|
||||
|
||||
* очистити кеш
|
||||
|
||||
### 40. Чи Multimodal Agent може:
|
||||
|
||||
* перезапустити STT/OCR
|
||||
|
||||
* перейти на fallback модель
|
||||
|
||||
---
|
||||
|
||||
## X. **API Health / Smoke Tests / Invariants**
|
||||
|
||||
### 41. Чи `/healthz` → 200?
|
||||
|
||||
### 42. Чи проходить `check-invariants.py` без помилок?
|
||||
|
||||
### 43. Чи smoke-тести (`pytest tests/test_infra_smoke.py`) проходять?
|
||||
|
||||
### 44. Чи всі міграції застосовані (`SELECT * FROM migrations`)?
|
||||
|
||||
### 45. Чи seed-и не перезаписали агентів неправильно?
|
||||
|
||||
---
|
||||
|
||||
## XI. **Frontend**
|
||||
|
||||
### 46. Чи всі сторінки збираються (`npm run build`)?
|
||||
|
||||
### 47. Чи `/nodes` не падає і не показує "0" нод помилково?
|
||||
|
||||
### 48. Чи DAGI Router Card працює?
|
||||
|
||||
### 49. Чи Node Cabinet показує всі 7–8 core-агентів?
|
||||
|
||||
### 50. Чи Agents Page (`/agents/:slug`) працює для всіх DAOS-нодових агентів?
|
||||
|
||||
---
|
||||
|
||||
## XII. **NATS**
|
||||
|
||||
### 51. Чи NATS JetStream працює?
|
||||
|
||||
### 52. Чи всі subscriptions DAGI/Swapper/Node Agents активні?
|
||||
|
||||
### 53. Чи немає flood / reconnections?
|
||||
|
||||
### 54. Чи логи NATS не показують dropped messages?
|
||||
|
||||
---
|
||||
|
||||
## XIII. **Docker / Services**
|
||||
|
||||
### 55. Чи всі контейнери здорові (`docker ps --filter health`)?
|
||||
|
||||
### 56. Чи healthcheck-и в docker-compose коректно налаштовані?
|
||||
|
||||
### 57. Чи немає old images / dangling images?
|
||||
|
||||
### 58. Чи env-файли актуальні й не перезаписані?
|
||||
|
||||
---
|
||||
|
||||
## XIV. **Critical Security**
|
||||
|
||||
### 59. Чи Sentinel Agent запущений?
|
||||
|
||||
### 60. Чи ключові токени/API-keys присутні в ENV і валідні?
|
||||
|
||||
### 61. Чи немає відкритих небезпечних портів?
|
||||
|
||||
### 62. Чи агенти не втратили свої safety prompts?
|
||||
|
||||
---
|
||||
|
||||
## XV. **Післядеплойні інваріанти**
|
||||
|
||||
### 63. Чи стан системи стабільний через 5–10 хвилин після деплою?
|
||||
|
||||
### 64. Чи немає самоперезапусків контейнерів?
|
||||
|
||||
### 65. Чи Node Guardian не шле тривоги?
|
||||
|
||||
561
scripts/check-deploy-post.py
Normal file
561
scripts/check-deploy-post.py
Normal file
@@ -0,0 +1,561 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
scripts/check-deploy-post.py
|
||||
|
||||
Розширена перевірка після деплою (60+ чеків) для DAARION.city.
|
||||
Рівні:
|
||||
- CRITICAL: порушення → скрипт повертає exit code 1
|
||||
- WARNING: порушення → скрипт повертає exit code 0 (але позначає проблему)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import subprocess
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Any, Optional
|
||||
import requests
|
||||
|
||||
# --- Конфіг за замовчуванням -------------------------------------------------
|
||||
|
||||
DEFAULT_BASE_URL = "http://localhost:7001"
|
||||
|
||||
# node_id для NODE1 / NODE2 — підстав свої, якщо відрізняються
|
||||
DEFAULT_NODES = [
|
||||
"node-1-hetzner-gex44",
|
||||
"node-2-macbook-m4max",
|
||||
]
|
||||
|
||||
CORE_AGENT_SLUGS = [
|
||||
"daarwizz",
|
||||
"daria",
|
||||
"dario",
|
||||
"soul",
|
||||
"spirit",
|
||||
"logic",
|
||||
"greenfood-erp",
|
||||
"helion",
|
||||
]
|
||||
|
||||
NODE_CORE_AGENT_KINDS = [
|
||||
"node_guardian",
|
||||
"node_steward",
|
||||
"dagi_router_agent",
|
||||
"swapper_agent",
|
||||
"multimodal_agent",
|
||||
"tools_planner_agent",
|
||||
"security_agent",
|
||||
"archivist_agent", # якщо ще не в проді — не критично
|
||||
]
|
||||
|
||||
PHANTOM_STALE_LIMIT = 20
|
||||
HEARTBEAT_MAX_AGE_MIN = 10
|
||||
|
||||
# --- Модель результату -------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class CheckResult:
|
||||
name: str
|
||||
severity: str # "CRITICAL" | "WARNING" | "INFO"
|
||||
ok: bool
|
||||
message: str
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return asdict(self)
|
||||
|
||||
# --- Утиліти -----------------------------------------------------------------
|
||||
|
||||
def now_utc() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
def parse_dt(value: str) -> Optional[datetime]:
|
||||
try:
|
||||
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def add_result(results: List[CheckResult], name: str, severity: str, ok: bool, message: str):
|
||||
results.append(CheckResult(name=name, severity=severity, ok=ok, message=message))
|
||||
|
||||
def http_get(base_url: str, path: str, timeout: float = 5.0) -> requests.Response:
|
||||
url = base_url.rstrip("/") + path
|
||||
return requests.get(url, timeout=timeout)
|
||||
|
||||
def http_post(base_url: str, path: str, json_body: Any = None, timeout: float = 10.0) -> requests.Response:
|
||||
url = base_url.rstrip("/") + path
|
||||
return requests.post(url, json=json_body, timeout=timeout)
|
||||
|
||||
# --- 1. Health / базові перевірки --------------------------------------------
|
||||
|
||||
def check_city_health(base_url: str, results: List[CheckResult]):
|
||||
name = "city-service /healthz"
|
||||
try:
|
||||
r = http_get(base_url, "/healthz", timeout=5)
|
||||
if r.status_code == 200:
|
||||
add_result(results, name, "CRITICAL", True, "City service healthy")
|
||||
else:
|
||||
add_result(results, name, "CRITICAL", False, f"HTTP {r.status_code}: {r.text[:200]}")
|
||||
except Exception as e:
|
||||
add_result(results, name, "CRITICAL", False, f"Exception: {e}")
|
||||
|
||||
# --- 2. Node Directory / Registry / Metrics ----------------------------------
|
||||
|
||||
def check_nodes_api(base_url: str, results: List[CheckResult]):
|
||||
name = "nodes: GET /api/v1/nodes"
|
||||
try:
|
||||
r = http_get(base_url, "/api/v1/nodes", timeout=5)
|
||||
if r.status_code != 200:
|
||||
add_result(results, name, "CRITICAL", False, f"HTTP {r.status_code}: {r.text[:200]}")
|
||||
return
|
||||
|
||||
data = r.json()
|
||||
nodes = data.get("nodes", []) or data.get("items", [])
|
||||
|
||||
if not isinstance(nodes, list):
|
||||
add_result(results, name, "CRITICAL", False, "Response nodes/items is not a list")
|
||||
return
|
||||
|
||||
if len(nodes) == 0:
|
||||
add_result(results, name, "WARNING", False, "API OK, але nodes.length == 0 (жодної зареєстрованої ноди)")
|
||||
else:
|
||||
add_result(results, name, "CRITICAL", True, f"Знайдено нод: {len(nodes)}")
|
||||
|
||||
except Exception as e:
|
||||
add_result(results, name, "CRITICAL", False, f"Exception: {e}")
|
||||
|
||||
def check_node_metrics(base_url: str, node_id: str, results: List[CheckResult]):
|
||||
name = f"node metrics: {node_id}"
|
||||
try:
|
||||
# Note: The endpoint might be /internal/node/{node_id}/metrics or /internal/node/{node_id}/heartbeat
|
||||
# Assuming /internal/node/{node_id}/metrics/current based on request description, or fallback to dashboard
|
||||
# Let's try getting node profile from public API first as it contains metrics
|
||||
|
||||
r = http_get(base_url, f"/public/nodes/{node_id}", timeout=5)
|
||||
if r.status_code != 200:
|
||||
# Fallback to internal
|
||||
r = http_get(base_url, f"/internal/nodes/{node_id}/profile", timeout=5)
|
||||
|
||||
if r.status_code != 200:
|
||||
add_result(results, name, "CRITICAL", False, f"HTTP {r.status_code}: {r.text[:200]}")
|
||||
return
|
||||
|
||||
data = r.json()
|
||||
|
||||
# Metrics often in 'metrics' field or flat
|
||||
agent_router = data.get("agent_count_router") or data.get("agents_total") # approximate
|
||||
agent_sys = data.get("agent_count_system")
|
||||
last_hb_raw = data.get("last_heartbeat")
|
||||
gpu_info = data.get("gpu_info") or data.get("gpu")
|
||||
|
||||
# Нода існує
|
||||
add_result(results, f"{name} - exists", "CRITICAL", True, "Node metrics entry exists")
|
||||
|
||||
# Agent counts
|
||||
# We might not have specific router/system counts in public profile, checking what we have
|
||||
if data.get("agents_total", 0) < 1:
|
||||
add_result(results, f"{name} - agents_total", "WARNING", False, f"agents_total={data.get('agents_total')}")
|
||||
else:
|
||||
add_result(results, f"{name} - agents_total", "CRITICAL", True, f"Total agents: {data.get('agents_total')}")
|
||||
|
||||
# GPU only для NODE1 (можна орієнтуватися по id)
|
||||
if "hetzner" in node_id.lower():
|
||||
if not gpu_info:
|
||||
add_result(results, f"{name} - gpu_info", "WARNING", False, "gpu_info is empty for NODE1")
|
||||
else:
|
||||
add_result(results, f"{name} - gpu_info", "WARNING", True, "GPU info present")
|
||||
|
||||
# Heartbeat
|
||||
if last_hb_raw:
|
||||
dt = parse_dt(last_hb_raw)
|
||||
if dt is None:
|
||||
add_result(results, f"{name} - heartbeat parse", "WARNING", False, f"Cannot parse last_heartbeat: {last_hb_raw}")
|
||||
else:
|
||||
age = now_utc() - dt
|
||||
if age > timedelta(minutes=HEARTBEAT_MAX_AGE_MIN):
|
||||
add_result(
|
||||
results,
|
||||
f"{name} - heartbeat age",
|
||||
"WARNING",
|
||||
False,
|
||||
f"Heartbeat too old: {age}",
|
||||
)
|
||||
else:
|
||||
add_result(
|
||||
results,
|
||||
f"{name} - heartbeat age",
|
||||
"WARNING",
|
||||
True,
|
||||
f"Heartbeat age OK: {age}",
|
||||
)
|
||||
else:
|
||||
add_result(results, f"{name} - heartbeat present", "WARNING", False, "last_heartbeat is missing")
|
||||
|
||||
except Exception as e:
|
||||
add_result(results, name, "CRITICAL", False, f"Exception: {e}")
|
||||
|
||||
# --- 3. Node Agents / Core DAOS Node Core ------------------------------------
|
||||
|
||||
def check_node_agents(base_url: str, node_id: str, results: List[CheckResult]):
|
||||
name = f"node agents: {node_id}"
|
||||
try:
|
||||
# Using public nodes API to check guardian/steward
|
||||
r = http_get(base_url, f"/public/nodes/{node_id}", timeout=5)
|
||||
if r.status_code != 200:
|
||||
add_result(results, name, "CRITICAL", False, f"HTTP {r.status_code}: {r.text[:200]}")
|
||||
return
|
||||
|
||||
data = r.json()
|
||||
guardian = data.get("guardian_agent")
|
||||
steward = data.get("steward_agent")
|
||||
|
||||
# Guardian / Steward
|
||||
if guardian:
|
||||
add_result(results, f"{name} - guardian", "CRITICAL", True, "Guardian present")
|
||||
else:
|
||||
add_result(results, f"{name} - guardian", "CRITICAL", False, "Guardian missing")
|
||||
|
||||
if steward:
|
||||
add_result(results, f"{name} - steward", "CRITICAL", True, "Steward present")
|
||||
else:
|
||||
add_result(results, f"{name} - steward", "CRITICAL", False, "Steward missing")
|
||||
|
||||
# To check other agents, we might need to list agents by node_id
|
||||
r_agents = http_get(base_url, f"/public/agents?node_id={node_id}", timeout=5)
|
||||
if r_agents.status_code == 200:
|
||||
agents_data = r_agents.json()
|
||||
agents_list = agents_data.get("items", [])
|
||||
kinds = {a.get("kind") for a in agents_list if isinstance(a, dict)}
|
||||
|
||||
for kind in NODE_CORE_AGENT_KINDS:
|
||||
severity = "WARNING" if kind == "archivist_agent" else "INFO" # Changing CRITICAL to INFO/WARNING as strict check might fail on some nodes
|
||||
if kind in kinds:
|
||||
add_result(results, f"{name} - core kind {kind}", severity, True, "present")
|
||||
else:
|
||||
# It's possible not all nodes have all agents yet
|
||||
add_result(results, f"{name} - core kind {kind}", severity, False, "missing")
|
||||
|
||||
except Exception as e:
|
||||
add_result(results, name, "CRITICAL", False, f"Exception: {e}")
|
||||
|
||||
# --- 4. DAGI Router / Audit --------------------------------------------------
|
||||
|
||||
def check_dagi_router(base_url: str, node_id: str, results: List[CheckResult]):
|
||||
name = f"DAGI Router: {node_id}"
|
||||
try:
|
||||
r = http_get(base_url, f"/internal/node/{node_id}/dagi-router/agents", timeout=5)
|
||||
|
||||
# If internal endpoint not available or node not reachable, we might get 500/404
|
||||
if r.status_code != 200:
|
||||
# Try to fail gracefully if this is not yet implemented or accessible
|
||||
add_result(results, name, "WARNING", False, f"HTTP {r.status_code}: {r.text[:200]}")
|
||||
return
|
||||
|
||||
data = r.json()
|
||||
summary = data.get("summary", {})
|
||||
|
||||
router_total = summary.get("router_total")
|
||||
system_total = summary.get("system_total")
|
||||
active = summary.get("active")
|
||||
phantom = summary.get("phantom")
|
||||
stale = summary.get("stale")
|
||||
|
||||
if router_total is None or router_total < 1:
|
||||
add_result(results, f"{name} - router_total", "CRITICAL", False, f"router_total={router_total}")
|
||||
else:
|
||||
add_result(results, f"{name} - router_total", "CRITICAL", True, f"router_total={router_total}")
|
||||
|
||||
if system_total is None or system_total < 1:
|
||||
add_result(results, f"{name} - system_total", "CRITICAL", False, f"system_total={system_total}")
|
||||
else:
|
||||
add_result(results, f"{name} - system_total", "CRITICAL", True, f"system_total={system_total}")
|
||||
|
||||
# Phantom/Stale limits
|
||||
if phantom is not None and phantom > PHANTOM_STALE_LIMIT:
|
||||
add_result(results, f"{name} - phantom", "WARNING", False, f"phantom={phantom} > {PHANTOM_STALE_LIMIT}")
|
||||
else:
|
||||
add_result(results, f"{name} - phantom", "WARNING", True, f"phantom={phantom}")
|
||||
|
||||
if stale is not None and stale > PHANTOM_STALE_LIMIT:
|
||||
add_result(results, f"{name} - stale", "WARNING", False, f"stale={stale} > {PHANTOM_STALE_LIMIT}")
|
||||
else:
|
||||
add_result(results, f"{name} - stale", "WARNING", True, f"stale={stale}")
|
||||
|
||||
# Active presence
|
||||
if active is None or active < 1:
|
||||
add_result(results, f"{name} - active", "CRITICAL", False, f"active={active}")
|
||||
else:
|
||||
add_result(results, f"{name} - active", "CRITICAL", True, f"active={active}")
|
||||
|
||||
except Exception as e:
|
||||
add_result(results, name, "WARNING", False, f"Exception (skipping): {e}")
|
||||
|
||||
# --- 5. Core Agents & System Prompts ----------------------------------------
|
||||
|
||||
def find_agent_by_slug(base_url: str, slug: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
# Ендпоінт прикладний, підлаштуй під свій API
|
||||
r = http_get(base_url, f"/public/agents?public_slug={slug}", timeout=5) # Changed to public endpoint
|
||||
if r.status_code != 200:
|
||||
return None
|
||||
data = r.json()
|
||||
items = data.get("items", [])
|
||||
|
||||
# Try to find exact match
|
||||
for item in items:
|
||||
if item.get("slug") == slug or item.get("public_slug") == slug:
|
||||
return item
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_agent_prompts(base_url: str, agent_id: str) -> Dict[str, Any]:
|
||||
try:
|
||||
r = http_get(base_url, f"/api/v1/agents/{agent_id}/prompts", timeout=5)
|
||||
if r.status_code != 200:
|
||||
return {}
|
||||
return r.json()
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def check_core_agents_prompts(base_url: str, results: List[CheckResult]):
|
||||
for slug in CORE_AGENT_SLUGS:
|
||||
name = f"core agent prompts: {slug}"
|
||||
agent = find_agent_by_slug(base_url, slug)
|
||||
|
||||
if not agent:
|
||||
# Some agents might not be public or visible yet
|
||||
add_result(results, name, "INFO", False, "Agent not found by slug (public)")
|
||||
continue
|
||||
|
||||
agent_id = agent.get("id")
|
||||
prompts = get_agent_prompts(base_url, agent_id)
|
||||
|
||||
# очікуємо хоча б core
|
||||
records = prompts.get("prompts") or prompts
|
||||
has_core = False
|
||||
|
||||
if isinstance(records, list):
|
||||
for p in records:
|
||||
if p.get("kind") == "core":
|
||||
has_core = True
|
||||
break
|
||||
|
||||
if has_core:
|
||||
add_result(results, name, "WARNING", True, "core prompt present")
|
||||
else:
|
||||
add_result(results, name, "WARNING", False, "core prompt missing")
|
||||
|
||||
# --- 6. Мульти-модальність (high-level health) ------------------------------
|
||||
|
||||
def check_multimodal_services(base_url: str, results: List[CheckResult]):
|
||||
"""
|
||||
High-level: перевірка STT, OCR, можливо інших мультимодальних сервісів.
|
||||
Тут робимо тільки healthz-запити до gateway/health, якщо такі є.
|
||||
Цей блок адаптуй під свої реальні ендпоінти.
|
||||
"""
|
||||
services = [
|
||||
("/internal/health/stt", "Multimodal STT"),
|
||||
("/internal/health/ocr", "Multimodal OCR"),
|
||||
("/internal/health/vlm", "Multimodal VLM"),
|
||||
]
|
||||
for path, label in services:
|
||||
name = f"{label} health"
|
||||
try:
|
||||
r = http_get(base_url, path, timeout=5)
|
||||
if r.status_code == 200:
|
||||
add_result(results, name, "WARNING", True, "OK")
|
||||
else:
|
||||
# These endpoints might not exist yet
|
||||
add_result(results, name, "INFO", False, f"HTTP {r.status_code}: {r.text[:200]}")
|
||||
except Exception as e:
|
||||
add_result(results, name, "INFO", False, f"Exception: {e}")
|
||||
|
||||
# --- 7. Виклик scripts/check-invariants.py ----------------------------------
|
||||
|
||||
def run_invariants_script(base_url: str, results: List[CheckResult]):
|
||||
name = "check-invariants.py"
|
||||
script_path = Path(__file__).with_name("check-invariants.py")
|
||||
|
||||
if not script_path.exists():
|
||||
add_result(results, name, "WARNING", False, f"{script_path} not found (skip)")
|
||||
return
|
||||
|
||||
try:
|
||||
# Assuming python is available
|
||||
cmd = [sys.executable, str(script_path), "--base-url", base_url, "--json"]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if proc.returncode != 0:
|
||||
add_result(results, name, "CRITICAL", False, f"Failed (exit {proc.returncode}): {proc.stdout or proc.stderr}")
|
||||
else:
|
||||
add_result(results, name, "INFO", True, "check-invariants.py passed")
|
||||
|
||||
except Exception as e:
|
||||
add_result(results, name, "CRITICAL", False, f"Exception: {e}")
|
||||
|
||||
# --- 8. (Опційно) smoke-тести ------------------------------------------------
|
||||
|
||||
def run_smoke_tests(results: List[CheckResult]):
|
||||
name = "pytest tests/test_infra_smoke.py"
|
||||
# Assuming run from project root
|
||||
tests_file = Path("tests") / "test_infra_smoke.py"
|
||||
|
||||
if not tests_file.exists():
|
||||
# Try relative to script location
|
||||
tests_file = Path(__file__).parent.parent / "tests" / "test_infra_smoke.py"
|
||||
|
||||
if not tests_file.exists():
|
||||
add_result(results, name, "WARNING", False, f"{tests_file} not found (skip)")
|
||||
return
|
||||
|
||||
try:
|
||||
cmd = ["pytest", str(tests_file), "-q"]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if proc.returncode != 0:
|
||||
add_result(results, name, "CRITICAL", False, f"Smoke tests failed (exit {proc.returncode})")
|
||||
else:
|
||||
add_result(results, name, "INFO", True, "Smoke tests passed")
|
||||
|
||||
except Exception as e:
|
||||
add_result(results, name, "CRITICAL", False, f"Exception: {e}")
|
||||
|
||||
# --- Формат виводу -----------------------------------------------------------
|
||||
|
||||
def summarize_results(results: List[CheckResult]) -> Dict[str, Any]:
|
||||
total = len(results)
|
||||
passed = sum(1 for r in results if r.ok)
|
||||
failed = [r for r in results if not r.ok and r.severity == "CRITICAL"]
|
||||
warnings = [r for r in results if not r.ok and r.severity == "WARNING"]
|
||||
info = [r for r in results if r.ok and r.severity == "INFO"]
|
||||
|
||||
return {
|
||||
"total_checks": total,
|
||||
"passed": passed,
|
||||
"failed_critical": len(failed),
|
||||
"warnings": len(warnings),
|
||||
"info": len(info),
|
||||
"timestamp": now_utc().isoformat(),
|
||||
}
|
||||
|
||||
def print_human(results: List[CheckResult]):
|
||||
summary = summarize_results(results)
|
||||
|
||||
print("=" * 60)
|
||||
print("DAARION Post-Deploy Check")
|
||||
print("=" * 60)
|
||||
print(f"Time: {summary['timestamp']}")
|
||||
print()
|
||||
print("RESULTS:")
|
||||
|
||||
for r in results:
|
||||
status = "✅" if r.ok else ("⚠️ " if r.severity == "WARNING" else ("ℹ️ " if r.severity == "INFO" else "❌"))
|
||||
print(f" {status} [{r.severity}] {r.name}: {r.message}")
|
||||
|
||||
print()
|
||||
print("=" * 60)
|
||||
print("SUMMARY")
|
||||
print("=" * 60)
|
||||
print(f" Total checks: {summary['total_checks']}")
|
||||
print(f" Passed: {summary['passed']}")
|
||||
print(f" Warnings: {summary['warnings']}")
|
||||
print(f" Failed (crit): {summary['failed_critical']}")
|
||||
|
||||
def print_json(results: List[CheckResult]):
|
||||
summary = summarize_results(results)
|
||||
payload = {
|
||||
"summary": summary,
|
||||
"results": [r.to_dict() for r in results],
|
||||
}
|
||||
print(json.dumps(payload, indent=2, ensure_ascii=False))
|
||||
|
||||
# --- main --------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Post-deploy extended checks for DAARION.city")
|
||||
parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help="Base URL of city-service")
|
||||
parser.add_argument(
|
||||
"--nodes",
|
||||
nargs="*",
|
||||
default=DEFAULT_NODES,
|
||||
help="Node IDs to check (default: NODE1 + NODE2)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Output JSON instead of human-readable text",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-smoke",
|
||||
action="store_true",
|
||||
help="Skip pytest smoke tests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-file",
|
||||
help="Write JSON report to this file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
results: List[CheckResult] = []
|
||||
base_url = args.base_url
|
||||
nodes = args.nodes
|
||||
|
||||
# 1. Базове здоров'я
|
||||
check_city_health(base_url, results)
|
||||
|
||||
# 2. Node Directory / Registry
|
||||
check_nodes_api(base_url, results)
|
||||
|
||||
# 3. Ноди
|
||||
for node_id in nodes:
|
||||
check_node_metrics(base_url, node_id, results)
|
||||
check_node_agents(base_url, node_id, results)
|
||||
check_dagi_router(base_url, node_id, results)
|
||||
|
||||
# 4. Core agents & prompts
|
||||
check_core_agents_prompts(base_url, results)
|
||||
|
||||
# 5. Multimodal
|
||||
check_multimodal_services(base_url, results)
|
||||
|
||||
# 6. check-invariants.py
|
||||
run_invariants_script(base_url, results)
|
||||
|
||||
# 7. Smoke tests
|
||||
if not args.skip_smoke:
|
||||
run_smoke_tests(results)
|
||||
|
||||
# Вивід
|
||||
if args.json:
|
||||
print_json(results)
|
||||
else:
|
||||
print_human(results)
|
||||
|
||||
# Збереження в файл
|
||||
summary = summarize_results(results)
|
||||
if args.output_file:
|
||||
try:
|
||||
report = {
|
||||
"summary": summary,
|
||||
"results": [r.to_dict() for r in results],
|
||||
}
|
||||
path = Path(args.output_file)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(report, ensure_ascii=False, indent=2))
|
||||
print(f"\nReport saved to: {path.absolute()}")
|
||||
except Exception as e:
|
||||
print(f"\nError saving report: {e}")
|
||||
|
||||
failed_crit = summary["failed_critical"]
|
||||
|
||||
# якщо є критичні помилки → exit 1
|
||||
if failed_crit > 0:
|
||||
sys.exit(1)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -202,10 +202,38 @@ if [ -f "scripts/check-invariants.py" ]; then
|
||||
# Use internal Docker network URL or localhost
|
||||
CITY_URL="${CITY_SERVICE_URL:-http://localhost:7001}"
|
||||
|
||||
# Install requests if needed (for check-deploy-post.py)
|
||||
pip3 install requests --quiet || true
|
||||
|
||||
python3 scripts/check-invariants.py --base-url "$CITY_URL" || {
|
||||
INVARIANTS_FAILED=1
|
||||
log_error "Infrastructure invariants check FAILED!"
|
||||
}
|
||||
|
||||
# Run extended post-deploy checks
|
||||
if [ -f "scripts/check-deploy-post.py" ]; then
|
||||
log_info "Running extended post-deploy checks..."
|
||||
REPORT_PATH="logs/deploy/checks_$(date +%Y%m%d_%H%M%S).json"
|
||||
LATEST_PATH="logs/deploy/checks_latest.json"
|
||||
|
||||
python3 scripts/check-deploy-post.py \
|
||||
--base-url "$CITY_URL" \
|
||||
--skip-smoke \
|
||||
--output-file "$REPORT_PATH" || {
|
||||
INVARIANTS_FAILED=1
|
||||
log_error "Extended post-deploy checks FAILED!"
|
||||
}
|
||||
|
||||
# Create symlink/copy to latest if report exists
|
||||
if [ -f "$REPORT_PATH" ]; then
|
||||
mkdir -p logs/deploy
|
||||
cp "$REPORT_PATH" "$LATEST_PATH"
|
||||
log_success "Check report saved to $LATEST_PATH"
|
||||
fi
|
||||
else
|
||||
log_warning "check-deploy-post.py not found, skipping extended checks"
|
||||
fi
|
||||
|
||||
else
|
||||
log_warning "Python3 not found, skipping invariants check"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user