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
|
# Use internal Docker network URL or localhost
|
||||||
CITY_URL="${CITY_SERVICE_URL:-http://localhost:7001}"
|
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" || {
|
python3 scripts/check-invariants.py --base-url "$CITY_URL" || {
|
||||||
INVARIANTS_FAILED=1
|
INVARIANTS_FAILED=1
|
||||||
log_error "Infrastructure invariants check FAILED!"
|
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
|
else
|
||||||
log_warning "Python3 not found, skipping invariants check"
|
log_warning "Python3 not found, skipping invariants check"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user