From c8cf27df081ee1faca4abc5a9bb0006de4dbf3ba Mon Sep 17 00:00:00 2001 From: dttb Date: Sat, 18 Apr 2026 00:24:20 +0300 Subject: [PATCH] =?UTF-8?q?kb-audit:=20fix=20=D0=BF=D0=B0=D1=80=D1=81?= =?UTF-8?q?=D0=B5=D1=80=20=E2=80=94=20=D0=BB=D0=BE=D0=B2=D0=B8=D1=82=20tab?= =?UTF-8?q?le-rows=20=D0=B8=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB=20?= =?UTF-8?q?=F0=9F=97=91=EF=B8=8F=20=D1=83=D0=B4=D0=B0=D0=BB=D1=91=D0=BD?= =?UTF-8?q?=D0=BD=D1=8B=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/kb-audit.py | 48 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/scripts/kb-audit.py b/scripts/kb-audit.py index f4f514f..ddf9e13 100644 --- a/scripts/kb-audit.py +++ b/scripts/kb-audit.py @@ -53,20 +53,38 @@ def parse_live(): def parse_inventory(path: Path): - """Парсит inventory-файл. Извлекает упомянутые VMID + связанный контекст.""" + """Парсит inventory-файл. Извлекает упомянутые VMID + связанный контекст. + Ловит два формата: + 1. "LXC 132" / "VM 250" — в заголовках и тексте + 2. Table rows: | 132 | debian | ... | — в таблицах + """ 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) + + def add(vmid: str, idx: int): if vmid not in found: - # контекст: 80 символов вокруг - start = max(0, m.start() - 20) - end = min(len(text), m.end() + 60) + start = max(0, idx - 20) + end = min(len(text), idx + 80) found[vmid] = text[start:end].replace("\n", " ").strip() + + for m in re.finditer(r"(?:LXC|VM)\s+(\d{2,4})\b", text): + add(m.group(1), m.start()) + # table rows: строка начинается с `| NNN |` (игнорируем header-row с тире) + for m in re.finditer(r"^\s*\|\s*(\d{2,4})\s*\|", text, re.MULTILINE): + add(m.group(1), m.start()) return found +def find_deleted_section(path: Path) -> set: + """Ищет VMID в секции '## 🗑️ Удалённые' чтобы не флагать их как missing.""" + text = path.read_text() + # блок между '🗑️ Удалённые' и следующим '##' + m = re.search(r"##\s*🗑[^\n]*Удал[^\n]*\n(.*?)(?=\n##|\Z)", text, re.DOTALL) + if not m: + return set() + return set(re.findall(r"\|\s*(\d{2,4})\s*\|", m.group(1))) + + def compare(live: dict, inventory: dict): live_ids = set(live.keys()) inv_ids = set(inventory.keys()) @@ -84,7 +102,13 @@ def main(): live = parse_live() inventory = parse_inventory(INVENTORY) - only_live, only_inv, common = compare(live, inventory) + deleted = find_deleted_section(INVENTORY) + only_live, only_inv_raw, common = compare(live, inventory) + # разделяем "в inventory но не в live" на 2 группы: + # - known-deleted (есть в секции "🗑️ Удалённые") — это ок + # - truly missing (нет и в live, и не в секции deleted) — проблема + 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 = [ "---", @@ -100,7 +124,8 @@ def main(): "", f"- Живых гостей Proxmox: **{len(live)}**", f"- Упомянуто в inventory: **{len(inventory)}**", - f"- В обоих: {len(common)} / только в live: {len(only_live)} / только в inventory: {len(only_inv)}", + f"- В обоих: {len(common)} / только в live: {len(only_live)} / отсутствуют в live: {len(only_inv)}", + f"- Известны как удалённые: {len(known_deleted)} (в `## 🗑️ Удалённые`)", "", ] @@ -113,13 +138,16 @@ def main(): lines += [""] if only_inv: - lines += ["## 🗑 В inventory есть, в Proxmox НЕТ (удалён? переименован?)", ""] + 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 полностью совпадает с живой инфраструктурой", ""]