scripts: kb-audit + propose — еженедельный drift-детектор для inventory

- 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.
This commit is contained in:
dttb
2026-04-18 00:23:07 +03:00
parent 13469e8ec3
commit 6368738ade
4 changed files with 266 additions and 0 deletions

0
audit/.gitkeep Normal file
View File

46
scripts/README.md Normal file
View File

@@ -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 везде
```

View File

@@ -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

149
scripts/kb-audit.py Normal file
View File

@@ -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()