Netpulse_SasS/db/import/extract_noc.py
byrsapty f162dcab19 Імпорт профілів збору конфігу з NOC Project
147 профілів на 67 вендорів у ncm.profiles: Cisco, Huawei, Juniper,
MikroTik, Eltex, D-Link, HP, Brocade, Extreme, Alcatel, Allied Telesis,
Qtech, ZTE, BDCOM та інші.

Розбір через ast, без виконання: NOC-скрипт тягне половину свого
фреймворку, і імпортувати його означало б або принести весь NOC, або
підсунути заглушки, які мовчки змінюють поведінку.

З кожного get_config.py береться перша команда збору, гілка startup,
strip_first_lines і регекси платформ. Альтернативи лишаються в
коментарі. Шум (exit, sh, changeto system, terminal width 200)
відсіюється явним списком: Cisco.ASA після фільтра дає рівно
more system:running-config, Juniper.JUNOSe — рівно
show running-configuration замість чотирьох команд із сусідніх гілок.

Пейджер додається лише мережевому CLI: Eltex SMG і TAU знімають конфіг
через cat, і terminal datadump у bash просто впав би.

Важливо: команди — з NOC, а промпти й вимкнення пейджера — ні. У дампі
всі __init__.py порожні (359 із 364), а NOC тримає pattern_prompt саме
там. Вони проставлені за родиною вендора з типових значень і потребують
перевірки на живому залізі.

Не витягнулись 23 з 170, і не через помилку: 21 знімає конфіг по HTTP
(камери, відеокодери, MikroTik SwOS, HP iLO2), 2 — порожні заглушки в
самому NOC.

Виконувати профілі поки нікому: агентського модуля ncm (SSH/Telnet)
немає, це Етап 7.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 02:34:55 +03:00

160 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Витяг команд збору конфігу з профілів NOC.
Файли розбираються модулем ast — без виконання. Це принципово: NOC-скрипт
імпортує половину фреймворку, і спроба його імпортувати означала б або
тягнути весь NOC, або підсовувати заглушки, які мовчки змінюють поведінку.
AST бачить рівно те, що написано в тексті.
"""
import ast
import json
import pathlib
import re
import sys
from collections import Counter
ROOT = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")
def literal(node):
"""Рядковий літерал або None. f-рядки й конкатенації свідомо не беремо."""
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value
return None
class Walker(ast.NodeVisitor):
"""Збирає виклики self.cli(...) і strip_first_lines(...) у межах методу."""
def __init__(self):
self.cli = [] # (команда, під_умовою_startup)
self.strip = None
self.snmp = False
self.dynamic = False # є cli() з нелітеральним аргументом
self._startup_depth = 0
def visit_If(self, node):
# Гілка policy == "s" — це startup-config.
src = ast.unparse(node.test) if hasattr(ast, "unparse") else ""
is_startup = '"s"' in src or "'s'" in src
if is_startup:
self._startup_depth += 1
for n in node.body:
self.visit(n)
self._startup_depth -= 1
for n in node.orelse:
self.visit(n)
else:
self.generic_visit(node)
def visit_Call(self, node):
fn = node.func
name = getattr(fn, "attr", None) or getattr(fn, "id", None)
# cli_clean — той самий виклик під іншою назвою (Ericsson).
if name in ("cli", "cli_clean") and node.args:
cmd = literal(node.args[0])
if cmd is None:
self.dynamic = True
else:
self.cli.append((cmd, self._startup_depth > 0))
elif name == "strip_first_lines" and len(node.args) >= 2:
n = node.args[1]
if isinstance(n, ast.Constant) and isinstance(n.value, int):
self.strip = n.value
elif name in ("get", "set", "getnext") and getattr(fn, "value", None) is not None:
if getattr(fn.value, "attr", "") == "snmp":
self.snmp = True
self.generic_visit(node)
def platform_filter(decorators):
"""Регекс платформи з @BaseScript.match(platform__regex=...)."""
for d in decorators:
if not isinstance(d, ast.Call):
continue
for kw in d.keywords or []:
if kw.arg and "platform" in kw.arg:
v = literal(kw.value)
if v:
return v
return None
def parse_file(path):
try:
tree = ast.parse(path.read_text(encoding="utf-8", errors="replace"))
except SyntaxError as e:
return {"error": f"синтаксис: {e}"}
variants = []
for cls in [n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)]:
for fn in [n for n in cls.body if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]:
if not fn.name.startswith("execute"):
continue
w = Walker()
for stmt in fn.body:
w.visit(stmt)
if not w.cli and not w.snmp:
continue
variants.append({
"method": fn.name,
"platform_regex": platform_filter(fn.decorator_list),
"running": [c for c, s in w.cli if not s],
"startup": [c for c, s in w.cli if s],
"strip_first_lines": w.strip,
"uses_snmp": w.snmp,
"dynamic_cli": w.dynamic,
})
return {"variants": variants}
def main():
files = sorted(ROOT.rglob("get_config.py"))
out, stats = [], Counter()
for f in files:
rel = f.relative_to(ROOT)
parts = rel.parts
vendor = parts[0]
platform = ".".join(parts[1:-1]) if len(parts) > 2 else ""
res = parse_file(f)
if "error" in res:
stats["помилка розбору"] += 1
continue
variants = res["variants"]
if not variants:
stats["без команд"] += 1
continue
has_cmd = any(v["running"] or v["startup"] for v in variants)
if not has_cmd:
stats["лише snmp/динаміка"] += 1
continue
stats["витягнуто"] += 1
if len(variants) > 1:
stats[" з них із варіантами платформ"] += 1
if any(v["dynamic_cli"] for v in variants):
stats[" з них із нелітеральними командами"] += 1
out.append({
"vendor": vendor,
"platform": platform,
"source": str(rel).replace("\\", "/"),
"variants": variants,
})
print(f"файлів get_config.py: {len(files)}", file=sys.stderr)
for k, v in stats.most_common():
print(f" {k}: {v}", file=sys.stderr)
json.dump(out, sys.stdout, ensure_ascii=False, indent=1)
if __name__ == "__main__":
main()