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>
271 lines
11 KiB
Python
271 lines
11 KiB
Python
#!/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()
|