#!/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())