118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Tuple, Optional
|
|
|
|
import aiohttp
|
|
|
|
|
|
@dataclass
|
|
class NodeCheckResult:
|
|
base_url: str
|
|
ok: bool
|
|
status: str
|
|
details: Dict[str, Any]
|
|
|
|
|
|
async def fetch_json(session: aiohttp.ClientSession, method: str, url: str, **kwargs) -> Tuple[int, Dict[str, Any]]:
|
|
try:
|
|
async with session.request(method, url, **kwargs) as resp:
|
|
status = resp.status
|
|
try:
|
|
data = await resp.json(content_type=None)
|
|
except Exception:
|
|
text = await resp.text()
|
|
data = {"raw": text}
|
|
return status, data
|
|
except Exception as e:
|
|
return 0, {"error": str(e)}
|
|
|
|
|
|
async def check_node(session: aiohttp.ClientSession, base_url: str, timeout: int = 8) -> NodeCheckResult:
|
|
"""
|
|
Диагностика удаленной ноды:
|
|
- /api/system/health
|
|
- /api/node/network/status
|
|
- /api/node/network/ping
|
|
- /api/node/v3/network/stats (если доступно)
|
|
- /api/node/content/sync (контракт проверки методом OPTIONS)
|
|
"""
|
|
details: Dict[str, Any] = {}
|
|
ok = True
|
|
status_summary: List[str] = []
|
|
|
|
async def _probe(path: str, method: str = "GET", payload: Optional[Dict[str, Any]] = None):
|
|
url = f"{base_url.rstrip('/')}{path}"
|
|
kwargs: Dict[str, Any] = {}
|
|
if payload is not None:
|
|
kwargs["json"] = payload
|
|
s, d = await fetch_json(session, method, url, **kwargs)
|
|
details[path] = {"status": s, "data": d}
|
|
return s, d
|
|
|
|
# Health
|
|
s, _ = await _probe("/api/system/health")
|
|
status_summary.append(f"health={s}")
|
|
ok = ok and (s in (200, 503)) # 503 допустим как сигнал деградации
|
|
|
|
# Network status
|
|
s, _ = await _probe("/api/node/network/status")
|
|
status_summary.append(f"net.status={s}")
|
|
ok = ok and (s in (200, 401, 404, 405))
|
|
|
|
# Ping
|
|
s, _ = await _probe("/api/node/network/ping")
|
|
status_summary.append(f"net.ping={s}")
|
|
ok = ok and (s in (200, 401, 404, 405))
|
|
|
|
# v3 stats (если есть)
|
|
s, _ = await _probe("/api/node/v3/network/stats")
|
|
status_summary.append(f"v3.stats={s}")
|
|
ok = ok and (s in (200, 401, 404, 405))
|
|
|
|
# content sync contract presence via OPTIONS
|
|
s, _ = await _probe("/api/node/content/sync", method="OPTIONS")
|
|
status_summary.append(f"content.sync.options={s}")
|
|
ok = ok and (s in (200, 204, 405))
|
|
|
|
return NodeCheckResult(
|
|
base_url=base_url,
|
|
ok=ok,
|
|
status=";".join(status_summary),
|
|
details=details,
|
|
)
|
|
|
|
|
|
async def run(nodes: List[str], parallel: int = 8, timeout: int = 8) -> List[NodeCheckResult]:
|
|
connector = aiohttp.TCPConnector(limit=parallel, ttl_dns_cache=60)
|
|
timeout_cfg = aiohttp.ClientTimeout(total=timeout)
|
|
async with aiohttp.ClientSession(connector=connector, timeout=timeout_cfg) as session:
|
|
tasks = [check_node(session, n, timeout=timeout) for n in nodes]
|
|
return await asyncio.gather(*tasks)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Проверка доступности и базового контракта сети нод")
|
|
parser.add_argument("nodes", nargs="+", help="Базовые URL нод, например http://localhost:8000")
|
|
parser.add_argument("-n", "--parallel", type=int, default=8, help="Параллельность запросов")
|
|
parser.add_argument("-t", "--timeout", type=int, default=8, help="Таймаут запроса в секундах")
|
|
parser.add_argument("--json", action="store_true", help="Вывести результат в JSON")
|
|
args = parser.parse_args()
|
|
|
|
results = asyncio.run(run(args.nodes, parallel=args.parallel, timeout=args.timeout))
|
|
|
|
if args.json:
|
|
print(json.dumps([r.__dict__ for r in results], ensure_ascii=False, indent=2))
|
|
else:
|
|
print("Network diagnostics:")
|
|
for r in results:
|
|
mark = "OK" if r.ok else "FAIL"
|
|
print(f"- {r.base_url}: {mark} | {r.status}")
|
|
|
|
return 0 if all(r.ok for r in results) else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main()) |