Netpulse_SasS/db/import/gen_profiles.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

271 lines
11 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
"""
Генерує міграцію ncm.profiles із витягу NOC.
Що звідки береться — важливо розрізняти:
* КОМАНДИ збору конфігу — з NOC (файли get_config.py). Це головна
цінність: сотні годин чужої роботи з реальним залізом.
* ПРОМПТИ, вимкнення пейджера, шаблони маскування — НЕ з NOC.
У переданому дампі всі __init__.py порожні (359 із 364), а саме там
NOC тримає pattern_prompt і command_disable_pager. Тому вони
проставляються за родиною вендора з типових значень і позначені в
міграції як такі, що потребують перевірки на живому залізі.
"""
import json
import pathlib
import re
import sys
SRC = pathlib.Path(sys.argv[1])
DST = pathlib.Path(sys.argv[2])
# Родини CLI. Ключ — префікс вендора; значення описує, як поводиться
# консоль. Промпти — типові для родини, не витягнуті з NOC.
FAMILIES = {
"cisco": {
"pager": "terminal length 0",
"prompt": r"[>#]\s*$",
"enable": True,
"scrub": [r"^Building configuration", r"^Current configuration : \d+ bytes",
r"^ntp clock-period"],
"redact": [r"(password|secret) \d? ?\S+", r"snmp-server community \S+"],
},
"huawei": {
"pager": "screen-length 0 temporary",
"prompt": r"[\]>]\s*$",
"enable": False,
"scrub": [r"^!Last configuration was updated"],
"redact": [r"(password|cipher) \S+", r"snmp-agent community \S+"],
},
"juniper": {
"pager": "set cli screen-length 0",
"prompt": r"[%>#]\s*$",
"enable": False,
"scrub": [r"^## Last commit:"],
"redact": [r"encrypted-password \S+", r'"\$9\$\S+"'],
},
"mikrotik": {
"pager": "",
"prompt": r"\] > $",
"enable": False,
"scrub": [r"^# \w{3}/\d{2}/\d{4}"],
"redact": [r"password=\S+"],
},
"eltex": {
"pager": "terminal datadump",
"prompt": r"[>#]\s*$",
"enable": True,
"scrub": [],
"redact": [r"(password|encrypted) \S+", r"snmp-server community \S+"],
},
"dlink": {
"pager": "disable clipaging",
"prompt": r"[>#:]\s*$",
"enable": False,
"scrub": [r"^#-+", r"^# \w+ Configuration"],
"redact": [r"password \S+", r"community \S+"],
},
"extreme": {
"pager": "disable clipaging",
"prompt": r"[>#]\s*$",
"enable": False,
"scrub": [],
"redact": [r"password \S+"],
},
"hp": {
"pager": "no page",
"prompt": r"[>#]\s*$",
"enable": True,
"scrub": [],
"redact": [r"password \S+", r"community \S+"],
},
"zyxel": {
"pager": "",
"prompt": r"[>#]\s*$",
"enable": False,
"scrub": [],
"redact": [r"password \S+"],
},
"raisecom": {
"pager": "terminal page-break disable",
"prompt": r"[>#]\s*$",
"enable": True,
"scrub": [],
"redact": [r"password \S+"],
},
}
# Кілька вендорів мають CLI, успадкований від відомої родини.
ALIASES = {
"bdcom": "cisco", "h3c": "huawei", "hpe": "hp", "aruba": "hp",
"3com": "huawei", "alcatel": "cisco", "arista": "cisco",
"brocade": "cisco", "force10": "cisco", "dcn": "cisco",
"edgecore": "cisco", "qtech": "cisco", "snr": "cisco",
"iskratel": "cisco", "maipu": "cisco", "orion": "cisco",
"ruckus": "cisco", "supertel": "cisco", "tplink": "cisco",
"upvel": "cisco", "vyatta": "juniper", "junipernetworks": "juniper",
"nateks": "cisco", "polygon": "cisco", "rotek": "cisco",
"snrs": "cisco", "zte": "huawei", "cdata": "huawei",
}
# Команди, які не є збором конфігу: службові переходи в оболонку,
# завершення сесії, зміна ширини терміналу. Тягнути їх у профіль
# означало б виконувати на залізі те, що до конфігу не має стосунку.
NOISE = {
"exit", "quit", "sh", "shell", "end", "changeto system",
"terminal width 200", "show context",
# Слід чужої інсталяції в дампі NOC, а не команда пристрою.
"python /app/zabbix/config_fetcher.py",
}
def slug(*parts):
v = "-".join(p for p in parts if p)
v = re.sub(r"[^a-zA-Z0-9]+", "-", v).strip("-").lower()
return v[:60]
def family_of(vendor):
# Цифри лишаємо: без них «3Com» стає «com», а «H3C» — «hc», і жоден
# із них не знаходить своєї родини.
v = re.sub(r"[^a-z0-9]", "", vendor.lower())
if v in FAMILIES:
return FAMILIES[v], v
if v in ALIASES:
return FAMILIES[ALIASES[v]], ALIASES[v]
return None, None
def sql_str(v):
return "'" + v.replace("'", "''") + "'"
def sql_json(obj):
return sql_str(json.dumps(obj, ensure_ascii=False))
def pick_commands(variants):
"""Головна команда збору + попередні, без шуму й дублів."""
running, startup, pre = [], [], []
for v in variants:
for c in v["running"]:
c = c.strip().rstrip("\r")
if not c or c in NOISE:
continue
# «undo terminal monitor», «no monitor», «terminal pager 0» —
# це підготовка консолі, а не сам конфіг.
if re.search(r"monitor|pager|screen-length|datadump|clipaging", c):
if c not in pre:
pre.append(c)
elif c not in running:
running.append(c)
for c in v["startup"]:
c = c.strip().rstrip("\r")
if c and c not in NOISE and c not in startup:
startup.append(c)
return pre, running, startup
def main():
data = json.loads(SRC.read_text(encoding="utf-8"))
rows, skipped, no_family = [], [], []
for d in data:
pre, running, startup = pick_commands(d["variants"])
if not running:
skipped.append(f"{d['vendor']}.{d['platform']}")
continue
fam, fam_key = family_of(d["vendor"])
if fam is None:
fam = {"pager": "", "prompt": r"[>#]\s*$", "enable": False,
"scrub": [], "redact": [r"password \S+", r"community \S+"]}
no_family.append(d["vendor"])
# Частина пристроїв «мережеві» лише зовні: конфіг знімається
# шелом (cat/tail). Додавати їм «terminal datadump» означало б
# відправити в bash команду мережевого CLI — вона просто впаде.
main_cmd = running[0]
shellish = bool(re.match(r"(cat|tail|less|more /|python|sh|/)", main_cmd))
commands = []
if fam["pager"] and not shellish:
commands.append(fam["pager"])
commands.extend(pre)
# Беремо ПЕРШУ команду як основну: у профілях із варіантами
# платформ решта — це альтернативи для інших моделей, і виконувати
# їх усі підряд означало б чотири рази знімати той самий конфіг.
commands.append(main_cmd)
alts = running[1:] + startup
name = f"{d['vendor']} {d['platform']}".strip()
rows.append({
"key": slug(d["vendor"], d["platform"]),
"name": name,
"vendor": d["vendor"].lower(),
"commands": commands,
"prompt": fam["prompt"],
"enable": fam["enable"],
"scrub": fam["scrub"],
"redact": fam["redact"],
"alts": alts,
"source": d["source"],
"family": fam_key or "generic",
})
rows.sort(key=lambda r: r["key"])
out = []
out.append("-- =====================================================================")
out.append("-- NetPulse :: 0014_ncm_profiles.sql")
out.append("-- Профілі збору конфігу, імпортовані з NOC Project.")
out.append("--")
out.append("-- Команди взято з get_config.py відповідного профілю NOC — це")
out.append("-- головна цінність імпорту: сотні годин чужої роботи з реальним")
out.append("-- залізом.")
out.append("--")
out.append("-- Промпти, вимкнення пейджера й шаблони маскування NOC у переданому")
out.append("-- дампі НЕ містив: усі __init__.py, де він їх тримає, порожні. Тому")
out.append("-- вони проставлені за родиною вендора з типових значень і")
out.append("-- потребують перевірки на живому залізі перед промисловим ужитком.")
out.append("-- =====================================================================")
out.append("")
out.append(f"-- Профілів: {len(rows)}; вендорів: {len({r['vendor'] for r in rows})}")
out.append("")
for r in rows:
meta = {"noc_source": r["source"], "family": r["family"]}
if r["alts"]:
meta["alternatives"] = r["alts"]
out.append(f"-- {r['source']}")
out.append("INSERT INTO ncm.profiles")
out.append(" (tenant_id, key, name, vendor, transport, commands, prompt_regex,")
out.append(" enable_required, scrub_patterns, redact_patterns, is_builtin)")
out.append("VALUES (NULL, {}, {}, {}, 'ssh', {}, {},".format(
sql_str(r["key"]), sql_str(r["name"]), sql_str(r["vendor"]),
sql_json(r["commands"]), sql_str(r["prompt"])))
out.append(" {}, {}, {}, true)".format(
"true" if r["enable"] else "false",
sql_json(r["scrub"]), sql_json(r["redact"])))
out.append("ON CONFLICT DO NOTHING;")
out.append("")
DST.write_text("\n".join(out), encoding="utf-8")
print(f"згенеровано профілів: {len(rows)}", file=sys.stderr)
print(f"вендорів: {len({r['vendor'] for r in rows})}", file=sys.stderr)
fams = {}
for r in rows:
fams[r["family"]] = fams.get(r["family"], 0) + 1
print("за родинами CLI:", file=sys.stderr)
for k, v in sorted(fams.items(), key=lambda x: -x[1]):
print(f" {k}: {v}", file=sys.stderr)
if skipped:
print(f"пропущено (без команди збору): {len(skipped)}", file=sys.stderr)
print(" " + ", ".join(skipped[:10]), file=sys.stderr)
if __name__ == "__main__":
main()