"""Метрики CPU, памяти и I/O узлов kind через ``docker stats`` / ``podman stats``. Имена контейнеров совпадают с префиксом кластера (см. ``list_kind_cluster_container_names``). Автор: Сергей Антропов Сайт: https://devops.org.ru """ from __future__ import annotations import logging import os import re import shutil import subprocess from core.cluster_lifecycle import _sort_kind_node_containers, list_registered_kind_clusters from models.schemas import KindClusterResources, KindNodeResourceStat logger = logging.getLogger("kind_k8s.container_resource_stats") # Одна строка на контейнер (совместимо с Docker и Podman). _STAT_FORMAT = "{{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.NetIO}}\t{{.BlockIO}}\t{{.PIDs}}" def _container_cli_bin() -> str: return (os.environ.get("CONTAINER_CLI") or "docker").strip() or "docker" def _list_running_container_names(cli: str) -> set[str]: """Имена **работающих** контейнеров (``ps`` без ``-a``).""" if not shutil.which(cli): return set() p = subprocess.run( [cli, "ps", "--format", "{{.Names}}"], capture_output=True, text=True, timeout=30, ) if p.returncode != 0: logger.debug("%s ps: код %s", cli, p.returncode) return set() return {n.strip() for n in (p.stdout or "").splitlines() if n.strip()} def _all_container_names_ps_a(cli: str) -> tuple[list[str], str | None]: """Все имена контейнеров (``ps -a``). При ошибке — ([], сообщение).""" p = subprocess.run( [cli, "ps", "-a", "--format", "{{.Names}}"], capture_output=True, text=True, timeout=30, ) if p.returncode != 0: err = (p.stderr or p.stdout or "").strip() or str(p.returncode) return [], f"{cli} ps -a: {err}" lines = [n.strip() for n in (p.stdout or "").splitlines() if n.strip()] return lines, None def _parse_pids(raw: str) -> int | None: s = raw.strip() if not s: return None try: return int(re.sub(r"\D", "", s) or "0") except ValueError: return None def _stats_for_container_names(cli: str, names: list[str]) -> list[KindNodeResourceStat]: """Вызов `` stats --no-stream`` для списка имён (только запущенные).""" if not names: return [] cmd = [cli, "stats", "--no-stream", "--format", _STAT_FORMAT, *names] p = subprocess.run(cmd, capture_output=True, text=True, timeout=60) if p.returncode != 0: err = (p.stderr or p.stdout or "").strip() or str(p.returncode) logger.warning("%s stats: %s", cli, err[:500]) return [] out: list[KindNodeResourceStat] = [] for line in (p.stdout or "").splitlines(): line = line.strip() if not line: continue parts = line.split("\t") while len(parts) < 7: parts.append("") name, cpu, mem_use, mem_pct, net_io, blk_io, pids_raw = parts[:7] out.append( KindNodeResourceStat( container_name=name.strip(), cpu_percent=cpu.strip() or None, memory_usage=mem_use.strip() or None, memory_percent=mem_pct.strip() or None, net_io=net_io.strip() or None, block_io=blk_io.strip() or None, pids=_parse_pids(pids_raw), ) ) return out def collect_kind_clusters_resource_stats() -> tuple[list[KindClusterResources], str | None]: """ Для каждого кластера из ``kind get clusters`` — метрики **запущенных** узлов. Один вызов ``ps -a`` на все имена; префикс узлов совпадает с ``list_kind_cluster_container_names``. Возвращает (список блоков, сообщение об ошибке уровня движка или ``None``). """ cli = _container_cli_bin() if not shutil.which(cli): return [], f"CLI «{cli}» не найден в PATH — метрики узлов недоступны" all_lines, ps_err = _all_container_names_ps_a(cli) if ps_err: return [], ps_err running = _list_running_container_names(cli) if not running: logger.debug("Нет запущенных контейнеров по %s ps", cli) kind_names = list_registered_kind_clusters() blocks: list[KindClusterResources] = [] for cluster_name in kind_names: prefix = f"{cluster_name}-" node_names = _sort_kind_node_containers([n for n in all_lines if n.startswith(prefix)]) active = [n for n in node_names if n in running] if not active: blocks.append( KindClusterResources( cluster_name=cluster_name, nodes=[], note="узлы остановлены или контейнеры отсутствуют", ) ) continue stats = _stats_for_container_names(cli, active) # Сохраняем порядок control-plane первым, как в kind order = {n: i for i, n in enumerate(node_names)} stats.sort(key=lambda s: order.get(s.container_name, 99)) blocks.append(KindClusterResources(cluster_name=cluster_name, nodes=stats, note=None)) return blocks, None