- kb_audit_helpers.py — общие функции parse_live/inventory/deleted - kb-audit-apply.py — применяет только structural факт-правки: * new VMID → добавить в "🔴 Остановленные" (только для stopped) * missing VMID → переместить в "🗑️ Удалённые" с датой - Коммитит как kb-audit-bot <kb-audit@dttb.ru> — фильтруемо в git log - Safety: live<5 хостов → abort - Не трогает описания/IP/назначения — только структурные поля из pct list Cron обновлён: audit → apply → propose (остаток для ручного ревью)
106 lines
3.9 KiB
Python
Executable File
106 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
kb-audit — детектит drift между живой инфраструктурой и KB-инвентарями.
|
||
Пишет структурированный отчёт в audit/YYYY-MM-DD-drift.md.
|
||
Только факты — никаких LLM, галлюцинаций быть не может.
|
||
|
||
Запускать из code-server (LXC 132) или любой машины с sshpass + доступом к Proxmox.
|
||
"""
|
||
|
||
import sys
|
||
from datetime import date
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from kb_audit_helpers import parse_live, parse_inventory, find_deleted_section, INVENTORY, VAULT # type: ignore
|
||
|
||
OUT_DIR = VAULT / "audit"
|
||
|
||
|
||
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)
|
||
deleted = find_deleted_section(INVENTORY)
|
||
only_live, only_inv_raw, common = compare(live, inventory)
|
||
only_inv = [v for v in only_inv_raw if v not in deleted]
|
||
known_deleted = [v for v in only_inv_raw if v in deleted]
|
||
|
||
lines = [
|
||
"---",
|
||
f"date: {today}",
|
||
"type: audit",
|
||
"source: kb-audit.py",
|
||
"tags: [audit, drift, infrastructure]",
|
||
"---",
|
||
"",
|
||
f"# KB drift audit — {today}",
|
||
"",
|
||
"Сравнение живого `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)} / отсутствуют в live: {len(only_inv)}",
|
||
f"- Известны как удалённые: {len(known_deleted)} (в `## 🗑️ Удалённые`)",
|
||
"",
|
||
]
|
||
|
||
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 known_deleted:
|
||
lines += [f"## ✓ Удалённые хосты (задокументированы): {', '.join(sorted(known_deleted, key=int))}", ""]
|
||
|
||
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 += [
|
||
"",
|
||
"---",
|
||
"*Автоматически сгенерировано `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()
|