From 6368738adee85cf73c913a4cfc5ccbe0f8cc33be Mon Sep 17 00:00:00 2001 From: dttb Date: Sat, 18 Apr 2026 00:23:07 +0300 Subject: [PATCH] =?UTF-8?q?scripts:=20kb-audit=20+=20propose=20=E2=80=94?= =?UTF-8?q?=20=D0=B5=D0=B6=D0=B5=D0=BD=D0=B5=D0=B4=D0=B5=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20drift-=D0=B4=D0=B5=D1=82=D0=B5=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=20=D0=B4=D0=BB=D1=8F=20inventory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kb-audit.py: сравнивает живой pct list/qm list с proxmox-inventory.md → audit/YYYY-MM-DD-drift.md (только факты, без LLM) - kb-audit-propose.sh: прогоняет drift через Opus (Max OAuth на code-server) → audit/YYYY-MM-DD-proposed.md (patch на ревью) - scripts/README.md: архитектура и cron-конфиг Рекомендуемый cron на code-server: 0 6 * * 0 (воскр 06:00) Правки не применяются автоматом — только ревью + ручной git apply. --- audit/.gitkeep | 0 scripts/README.md | 46 +++++++++++ scripts/kb-audit-propose.sh | 71 +++++++++++++++++ scripts/kb-audit.py | 149 ++++++++++++++++++++++++++++++++++++ 4 files changed, 266 insertions(+) create mode 100644 audit/.gitkeep create mode 100644 scripts/README.md create mode 100644 scripts/kb-audit-propose.sh create mode 100644 scripts/kb-audit.py diff --git a/audit/.gitkeep b/audit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..68713b9 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,46 @@ +# scripts/ — инфра-скрипты vault + +## kb-audit.py +Факт-детектор drift-а: сравнивает живой `pct list`/`qm list` с `projects/dttb/proxmox-inventory.md`. +Пишет отчёт в `audit/YYYY-MM-DD-drift.md`. + +**Без LLM** — только факты. Галлюцинаций быть не может. + +Запуск: +```bash +python3 scripts/kb-audit.py +``` + +## kb-audit-propose.sh +Запускается **после** kb-audit.py. Берёт свежий drift + текущий inventory → отправляет в `claude -p` (Opus 4.7 через Max). +Получает предложенные правки → `audit/YYYY-MM-DD-proposed.md`. + +**Правки не применяются автоматом.** Ревью — ты, `git apply` — вручную. + +Запуск: +```bash +bash scripts/kb-audit-propose.sh +``` + +## Еженедельный cron (code-server LXC 132) +```cron +# воскресенье 06:00 — drift audit + Opus предложения +0 6 * * 0 /usr/bin/python3 /root/knowledge-base/scripts/kb-audit.py && /bin/bash /root/knowledge-base/scripts/kb-audit-propose.sh +``` + +## Архитектура +``` +pct list / qm list (Proxmox) + ↓ +kb-audit.py — фактовый diff + ↓ +audit/YYYY-MM-DD-drift.md (коммитится автоматом kb-autosync.sh) + ↓ +kb-audit-propose.sh — Opus предлагает patch + ↓ +audit/YYYY-MM-DD-proposed.md (коммитится) + ↓ +ты ревьюишь, применяешь руками + ↓ +коммит inventory, sync везде +``` diff --git a/scripts/kb-audit-propose.sh b/scripts/kb-audit-propose.sh new file mode 100644 index 0000000..2d9eb0d --- /dev/null +++ b/scripts/kb-audit-propose.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# kb-audit-propose — прогоняет последний drift-отчёт через Claude Opus, +# получает предложенные правки в inventory-файл. +# Запускать ПОСЛЕ kb-audit.py. +# Работает на code-server (где есть claude CLI с Max подпиской). + +set -u +VAULT="$(cd "$(dirname "$0")/.." && pwd)" +DATE=$(date +%Y-%m-%d) +DRIFT="$VAULT/audit/${DATE}-drift.md" +OUT="$VAULT/audit/${DATE}-proposed.md" + +if [ ! -f "$DRIFT" ]; then + echo "drift-отчёт не найден: $DRIFT. Запусти сначала kb-audit.py" >&2 + exit 1 +fi + +# если уже есть сегодняшний proposed — skip (не дёргаем Opus попусту) +if [ -f "$OUT" ] && [ "$OUT" -nt "$DRIFT" ]; then + echo "proposed уже свежее drift: $OUT" >&2 + exit 0 +fi + +cd "$VAULT" || exit 1 + +PROMPT="Ниже: а) отчёт drift-аудита инфраструктуры Proxmox, б) актуальный файл inventory. +Задача: предложи конкретные правки в projects/dttb/proxmox-inventory.md чтобы устранить drift. +НЕ правь файл сам. Выдай: +1. Краткое резюме (1-3 предложения) что изменилось +2. Список конкретных блоков для добавления/удаления/изменения (формат markdown diff блоков с пояснением) +3. Предупреждения если что-то неоднозначное + +Не выдумывай данных которых нет в drift-отчёте. Используй factual IP/hostname только из отчёта." + +{ + echo "$PROMPT" + echo "" + echo "---" + echo "## DRIFT-ОТЧЁТ" + echo "" + cat "$DRIFT" + echo "" + echo "---" + echo "## ТЕКУЩИЙ INVENTORY" + echo "" + cat "$VAULT/projects/dttb/proxmox-inventory.md" +} | claude -p --permission-mode plan > "$OUT.tmp" 2>&1 + +if [ -s "$OUT.tmp" ]; then + { + echo "---" + echo "date: $DATE" + echo "type: audit-proposed" + echo "source: kb-audit-propose.sh (Opus 4.7)" + echo "tags: [audit, proposed, inventory]" + echo "---" + echo "" + echo "# Предложенные правки inventory — $DATE" + echo "" + echo "Сгенерировано Claude Opus на основе [[${DATE}-drift|drift-отчёта]]." + echo "**Правки НЕ применены.** Ревью — ты. Apply — вручную." + echo "" + cat "$OUT.tmp" + } > "$OUT" + rm "$OUT.tmp" + echo "proposed saved: $OUT" +else + echo "Claude вернул пустой ответ" >&2 + rm -f "$OUT.tmp" + exit 2 +fi diff --git a/scripts/kb-audit.py b/scripts/kb-audit.py new file mode 100644 index 0000000..f4f514f --- /dev/null +++ b/scripts/kb-audit.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +""" +kb-audit — детектит drift между живой инфраструктурой и KB-инвентарями. +Пишет структурированный отчёт в audit/YYYY-MM-DD-drift.md. +Только факты — никаких LLM, галлюцинаций быть не может. + +Запускать из code-server (LXC 132) или любой машины с sshpass + доступом к Proxmox. +""" + +import re +import subprocess +import sys +from datetime import date +from pathlib import Path + +PROXMOX_HOST = "10.0.0.250" +PROXMOX_PASS = "1qaz!QAZ" + +VAULT = Path(__file__).resolve().parent.parent +INVENTORY = VAULT / "projects/dttb/proxmox-inventory.md" +OUT_DIR = VAULT / "audit" + + +def sh(cmd: str) -> str: + """SSH-выполнение команды на Proxmox.""" + full = [ + "sshpass", "-p", PROXMOX_PASS, + "ssh", "-o", "StrictHostKeyChecking=no", + "-o", "ConnectTimeout=10", + f"root@{PROXMOX_HOST}", + cmd, + ] + r = subprocess.run(full, capture_output=True, text=True, timeout=30) + return r.stdout + + +def parse_live(): + """Парсит pct list / qm list. Возвращает {id: {type, status, name}}.""" + live = {} + for raw_line in sh("pct list").splitlines()[1:]: + parts = raw_line.split(maxsplit=3) + if len(parts) >= 3: + vmid, status = parts[0], parts[1] + name = parts[-1] if len(parts) > 2 else "" + live[vmid] = {"type": "LXC", "status": status, "name": name} + for raw_line in sh("qm list").splitlines()[1:]: + # qm list: VMID NAME STATUS MEM BOOTDISK PID + parts = raw_line.split() + if len(parts) >= 3 and parts[0].isdigit(): + vmid, name, status = parts[0], parts[1], parts[2] + live[vmid] = {"type": "VM", "status": status, "name": name} + return live + + +def parse_inventory(path: Path): + """Парсит inventory-файл. Извлекает упомянутые VMID + связанный контекст.""" + text = path.read_text() + found = {} + # Ищем все упоминания "LXC NNN" или "VM NNN" в заголовках и таблицах + for m in re.finditer(r"(?:LXC|VM)\s+(\d{2,4})\b", text): + vmid = m.group(1) + if vmid not in found: + # контекст: 80 символов вокруг + start = max(0, m.start() - 20) + end = min(len(text), m.end() + 60) + found[vmid] = text[start:end].replace("\n", " ").strip() + return found + + +def compare(live: dict, inventory: dict): + live_ids = set(live.keys()) + inv_ids = set(inventory.keys()) + + only_live = sorted(live_ids - inv_ids, key=int) + only_inv = sorted(inv_ids - live_ids, key=int) + both = sorted(live_ids & inv_ids, key=int) + return only_live, only_inv, both + + +def main(): + today = date.today().isoformat() + OUT_DIR.mkdir(parents=True, exist_ok=True) + out = OUT_DIR / f"{today}-drift.md" + + live = parse_live() + inventory = parse_inventory(INVENTORY) + only_live, only_inv, common = compare(live, inventory) + + lines = [ + "---", + f"date: {today}", + "type: audit", + "source: kb-audit.py", + "tags: [audit, drift, infrastructure]", + "---", + "", + f"# KB drift audit — {today}", + "", + f"Сравнение живого `pct list` / `qm list` с [[../projects/dttb/proxmox-inventory|proxmox-inventory.md]]", + "", + f"- Живых гостей Proxmox: **{len(live)}**", + f"- Упомянуто в inventory: **{len(inventory)}**", + f"- В обоих: {len(common)} / только в live: {len(only_live)} / только в inventory: {len(only_inv)}", + "", + ] + + if only_live: + lines += ["## ⚠ В Proxmox есть, в inventory НЕТ (надо добавить)", ""] + lines += ["| VMID | Type | Status | Name |", "|---|---|---|---|"] + for vmid in only_live: + x = live[vmid] + lines += [f"| {vmid} | {x['type']} | {x['status']} | {x['name']} |"] + lines += [""] + + if only_inv: + lines += ["## 🗑 В inventory есть, в Proxmox НЕТ (удалён? переименован?)", ""] + lines += ["| VMID | Контекст из inventory |", "|---|---|"] + for vmid in only_inv: + ctx = inventory[vmid][:100] + lines += [f"| {vmid} | `...{ctx}...` |"] + lines += [""] + + if not only_live and not only_inv: + lines += ["## ✓ Inventory полностью совпадает с живой инфраструктурой", ""] + + lines += [ + "## Полный живой список", + "", + "| VMID | Type | Status | Name |", + "|---|---|---|---|", + ] + for vmid in sorted(live.keys(), key=int): + x = live[vmid] + lines += [f"| {vmid} | {x['type']} | {x['status']} | {x['name']} |"] + + lines += [ + "", + "---", + f"*Автоматически сгенерировано `scripts/kb-audit.py`. Применять правки — вручную после ревью.*", + ] + + out.write_text("\n".join(lines)) + print(f"drift report: {out}") + print(f" only_live: {len(only_live)}") + print(f" only_inv: {len(only_inv)}") + + +if __name__ == "__main__": + main()