#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ СВЕРКА АРЕНЫ — только чтение. Содержательная часть заказа CLAUDE-ARENA-02, выполненная через согласованный канал (gcloud IAP), без приватных ключей, без прямого ssh, без привязки host key. Ни одна команда здесь ничего не меняет. A. хост — что за машина, сколько живёт, чем занята B. рантайм — сервисы, процессы, порты C. исходники — что лежит, когда менялось D. писатели — кто и куда пишет в Postgres Запуск: sudo python3 recon.py [A|B|C|D|all] """ import os, re, subprocess, sys, time SEC = (sys.argv[1] if len(sys.argv) > 1 else "all").upper() def sh(cmd, timeout=25): try: r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout) return (r.stdout or r.stderr or "").strip() except Exception as e: return f"<{type(e).__name__}>" def head(t): print(f"\n{'═'*66}\n{t}\n{'═'*66}") # ─────────────────────────── A. ХОСТ ─────────────────────────── if SEC in ("A", "ALL"): head("A. ХОСТ") MD = "curl -s -m 5 -H Metadata-Flavor:Google http://metadata.google.internal/computeMetadata/v1/instance/" Q = { "hostname": "hostname", "ядро": "uname -r", "система": ". /etc/os-release && echo $PRETTY_NAME", "аптайм": "uptime -p", "загрузка": "cut -d' ' -f1-3 /proc/loadavg", "время UTC": "date -u +%Y-%m-%dT%H:%M:%SZ", "CPU": "nproc", "память": "free -m | awk '/^Mem:/{print $2\" всего, \"$3\" занято, \"$7\" доступно (МБ)\"}'", "swap": "free -m | awk '/^Swap:/{print $2\" всего, \"$3\" занято (МБ)\"}'", "диск /": "df -h / | awk 'NR==2{print $3\" из \"$2\" (\"$5\")\"}'", "загружен": "who -b | tr -s ' '", } for label, cmd in Q.items(): print(" %-12s %s" % (label, sh(cmd))) ip = sh(MD + "network-interfaces/0/access-configs/0/external-ip") mt = sh(MD + "machine-type").rsplit("/", 1)[-1] pre = sh(MD + "scheduling/preemptible") print(" %-12s %s" % ("внешний IP", ip)) print(" %-12s %s" % ("тип машины", mt)) note = " ← спот: Google может забрать в любой момент" if pre.upper() == "TRUE" else "" print(" %-12s %s%s" % ("вытесняемая", pre, note)) # ────────────────────────── B. РАНТАЙМ ────────────────────────── if SEC in ("B", "ALL"): head("B. РАНТАЙМ") units = sh("systemctl list-units --type=service --state=running --no-legend --no-pager " "| awk '{print $1}' | grep -E 'arena|grid|arb|tg|caddy|postgres|grafana' | sort") print(f" живых профильных сервисов: {len(units.splitlines()) if units else 0}") for u in units.splitlines(): since = sh(f"systemctl show {u} -p ActiveEnterTimestamp --value") mem = sh(f"systemctl show {u} -p MemoryCurrent --value") try: mem = f"{int(mem)/1048576:.0f} МБ" if mem.isdigit() and int(mem) > 0 else "—" except Exception: mem = "—" print(f" {u:34} с {since[:19] or '—':19} {mem:>9}") failed = sh("systemctl list-units --type=service --state=failed --no-legend --no-pager | awk '{print $1}'") print(f"\n упавших сервисов: {len(failed.splitlines()) if failed else 0}") for f in failed.splitlines()[:8]: print(f" ✖ {f}") print(f"\n python-процессов: {sh('pgrep -c python3')}") top = sh("ps -eo comm,rss --sort=-rss | awk 'NR>1{a[$1]+=$2} END{for(k in a) print a[k], k}' | sort -rn | head -6") for ln in top.splitlines(): p = ln.split(None, 1) if len(p) == 2: print(f" {p[1]:22} {int(p[0])/1024:>8.0f} МБ") print("\n слушающие порты:") ports = sh("ss -lntp 2>/dev/null | awk 'NR>1{print $4, $6}' | sort -u") for ln in ports.splitlines()[:16]: m = re.search(r'users:\(\("([^"]+)"', ln) addr = ln.split()[0] print(f" {addr:26} {m.group(1) if m else ''}") # ───────────────────────── C. ИСХОДНИКИ ───────────────────────── if SEC in ("C", "ALL"): head("C. ИСХОДНИКИ") for d in ("/opt/arena", "/opt/arena/grid_lab", "/opt/sovereign-v5"): if not os.path.isdir(d): print(f" {d:26} нет") continue n_py = sh(f"find {d} -maxdepth 1 -name '*.py' | wc -l") n_db = sh(f"find {d} -maxdepth 1 -name '*.db' | wc -l") size = sh(f"du -sh {d} 2>/dev/null | cut -f1") print(f" {d:26} {n_py:>4} py · {n_db:>3} db · {size}") print("\n менялось за последние 3 суток:") recent = sh("find /opt/arena /opt/sovereign-v5 -maxdepth 2 -name '*.py' -mtime -3 " "-printf '%TY-%Tm-%Td %TH:%TM %p\\n' 2>/dev/null | sort -r | head -12") print("\n".join(" " + l for l in recent.splitlines()) or " —") print("\n git:") for d in ("/opt/arena", "/opt/sovereign-v5"): if os.path.isdir(os.path.join(d, ".git")): br = sh(f"git -C {d} rev-parse --abbrev-ref HEAD") last = sh(f"git -C {d} log -1 --format='%h %ad %s' --date=short") dirty = sh(f"git -C {d} status --porcelain | wc -l") print(f" {d:24} {br} · {last[:52]} · изменённых файлов: {dirty}") else: print(f" {d:24} не под git") # ────────────────────────── D. ПИСАТЕЛИ ───────────────────────── if SEC in ("D", "ALL"): head("D. КТО ПИШЕТ В POSTGRES") PSQL = "sudo -u postgres psql -F'|' -tA -c " def q(sql): return sh(PSQL + '"' + sql + '"', timeout=30) print(" соединений всего / активных: " + q("SELECT count(*)||' / '||count(*) FILTER (WHERE state='active') FROM pg_stat_activity;")) print(" лимит max_connections: " + q("SELECT setting FROM pg_settings WHERE name='max_connections';")) print("\n соединения по клиентам:") for ln in q("SELECT coalesce(nullif(application_name,''),'<без имени>')||'|'||" "coalesce(state,'-')||'|'||count(*) FROM pg_stat_activity " "WHERE datname IS NOT NULL GROUP BY 1,2 ORDER BY 3 DESC LIMIT 8;").splitlines(): p = [x.strip() for x in ln.split("|")] if len(p) >= 3: print(" %-30s %-10s %s" % (p[0][:30], p[1], p[2])) print("\n таблицы по объёму записи:") print(" %-34s %>12s %>12s".replace(">", "") % ("таблица", "записей", "строк")) for ln in q("SELECT relname||'|'||(n_tup_ins+n_tup_upd+n_tup_del)||'|'||n_live_tup " "FROM pg_stat_user_tables WHERE n_tup_ins+n_tup_upd+n_tup_del>0 " "ORDER BY 1 DESC LIMIT 12;").splitlines(): p = [x.strip() for x in ln.split("|")] if len(p) == 3 and p[1].isdigit(): print(" %-34s %12s %12s" % (p[0][:34], format(int(p[1]), ",d"), format(int(p[2]), ",d"))) print()