Імпорт профілів збору конфігу з 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>
This commit is contained in:
parent
48848bd3a5
commit
f162dcab19
7 changed files with 1845 additions and 0 deletions
79
HISTORY.md
79
HISTORY.md
|
|
@ -1084,3 +1084,82 @@ SQL.
|
||||||
через API, а у формі хоста лише прив'язуються. Перевірок на рівні групи
|
через API, а у формі хоста лише прив'язуються. Перевірок на рівні групи
|
||||||
хостів (у Zabbix це шаблони — Етап 6). Історії й графіків за зібраними
|
хостів (у Zabbix це шаблони — Етап 6). Історії й графіків за зібраними
|
||||||
метриками.
|
метриками.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-08-15 — Імпорт профілів збору конфігу з NOC Project
|
||||||
|
|
||||||
|
Замовник передав вивантаження профілів NOC і попросив узяти з них
|
||||||
|
**лише збір конфігу**.
|
||||||
|
|
||||||
|
### Створено
|
||||||
|
|
||||||
|
- `db/import/extract_noc.py` — розбір `get_config.py` через `ast`.
|
||||||
|
- `db/import/gen_profiles.py` — генерація міграції.
|
||||||
|
- `db/import/why_skipped.py` — пояснення, чому профіль не дав команди.
|
||||||
|
- `db/migrations/0014_ncm_profiles.sql` — 147 профілів, 67 вендорів.
|
||||||
|
|
||||||
|
### Прийняті рішення
|
||||||
|
|
||||||
|
**Розбір через `ast`, а не імпорт.** NOC-скрипт тягне половину свого
|
||||||
|
фреймворку; імпортувати його означало б або принести весь NOC, або
|
||||||
|
підсунути заглушки, які мовчки змінюють поведінку. AST бачить рівно те,
|
||||||
|
що написано в тексті, і нічого не виконує.
|
||||||
|
|
||||||
|
**Беремо ПЕРШУ команду, а не всі.** У профілях із варіантами під моделі
|
||||||
|
(D-Link DxS — п'ять гілок) решта команд є альтернативами, і виконати їх
|
||||||
|
підряд означало б зняти той самий конфіг чотири рази. Альтернативи
|
||||||
|
лишаються в коментарі до кожного INSERT.
|
||||||
|
|
||||||
|
**Шум відсіюється явним списком.** `exit`, `sh`, `changeto system`,
|
||||||
|
`terminal width 200` — це керування сесією, а не збір конфігу.
|
||||||
|
`Cisco.ASA` після фільтра дає рівно `more system:running-config`,
|
||||||
|
`Juniper.JUNOSe` — рівно `show running-configuration` замість чотирьох
|
||||||
|
випадкових команд із сусідніх гілок методу.
|
||||||
|
|
||||||
|
**Пейджер додається лише мережевому CLI.** Частина «мережевих»
|
||||||
|
пристроїв знімає конфіг шелом (`cat /etc/config/cfg.yaml` в Eltex SMG і
|
||||||
|
TAU, `tail` в Eltex RG). Додати їм `terminal datadump` означало б
|
||||||
|
відправити в bash команду мережевої консолі.
|
||||||
|
|
||||||
|
### Знайдено при роботі
|
||||||
|
|
||||||
|
**У дампі порожні всі `__init__.py`** — 359 із 364. Саме там NOC тримає
|
||||||
|
`pattern_prompt` і `command_disable_pager`, тобто найважче для
|
||||||
|
відтворення. Тому промпти й вимкнення пейджера проставлені за родиною
|
||||||
|
вендора з типових значень і позначені в міграції як такі, що потребують
|
||||||
|
перевірки на залізі. Команди збору — справжні, з NOC.
|
||||||
|
|
||||||
|
**Нормалізація вендора губила цифри**: `re.sub(r"[^a-z]", …)`
|
||||||
|
перетворював `3Com` на `com`, а `H3C` на `hc`, і жоден із них не
|
||||||
|
знаходив своєї родини CLI. Після виправлення H3C правильно успадкував
|
||||||
|
консоль Huawei (13 профілів у родині замість 10).
|
||||||
|
|
||||||
|
**`Ericsson/MINI_LINK` викликає `cli_clean`, а не `cli`** — той самий
|
||||||
|
виклик під іншою назвою. Єдиний такий на 170 файлів; після додавання
|
||||||
|
витяг зріс зі 147 до 148, а після відсіву чужого скрипта
|
||||||
|
(`python /app/zabbix/config_fetcher.py` у `Cisco.DCM` — слід чиєїсь
|
||||||
|
інсталяції, а не команда пристрою) лишилось 147.
|
||||||
|
|
||||||
|
### Чому не всі 170
|
||||||
|
|
||||||
|
Не «загубились» — у 23 просто немає CLI-команди:
|
||||||
|
|
||||||
|
- **21 не використовують CLI**: конфіг знімається по HTTP
|
||||||
|
(`self.http.get`) або збирається з шматків — камери Axis, Beward,
|
||||||
|
Dahua, Hikvision, відеокодери Harmonic, MikroTik SwOS, TFortis,
|
||||||
|
HP iLO2, OS/Linux. Потрібен окремий збирач по HTTP.
|
||||||
|
- **2 — порожні заглушки в самому NOC**: `Alcatel/7324RU` і
|
||||||
|
`Ruckus/SmartZone` мають клас `Script` без жодного методу.
|
||||||
|
|
||||||
|
### Перевірено
|
||||||
|
|
||||||
|
Міграція накотилась на `netpulse_it`: 148 профілів, 67 вендорів,
|
||||||
|
дублікатів немає (унікальний індекс `ncm_profiles_key_uniq` спрацював,
|
||||||
|
раніші засіяні профілі збереглись). Вибірково звірено складні випадки —
|
||||||
|
Cisco ASA, NXOS, FWSM, Huawei VRP3, H3C, Juniper JUNOSe, Eltex SMG/TAU.
|
||||||
|
|
||||||
|
### Чого це ще не дає
|
||||||
|
|
||||||
|
**Виконувати профілі нікому.** Агентського модуля `ncm` (SSH/Telnet)
|
||||||
|
немає — це Етап 7. Зараз це готові дані, які чекають на виконавця.
|
||||||
|
|
|
||||||
53
db/import/README.md
Normal file
53
db/import/README.md
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Імпорт профілів збору конфігу з NOC Project
|
||||||
|
|
||||||
|
`db/migrations/0014_ncm_profiles.sql` згенеровано цими скриптами з
|
||||||
|
профілів [NOC Project](https://getnoc.com). Тримаємо їх у репозиторії,
|
||||||
|
щоб імпорт можна було повторити на новішому дампі, а не розбирати
|
||||||
|
тисячу рядків INSERT-ів вручну.
|
||||||
|
|
||||||
|
## Як повторити
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python extract_noc.py /шлях/до/profiles > noc_config.json
|
||||||
|
python gen_profiles.py noc_config.json ../migrations/0014_ncm_profiles.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
`why_skipped.py` пояснює, чому конкретний профіль не дав команди.
|
||||||
|
|
||||||
|
## Як це працює
|
||||||
|
|
||||||
|
Файли розбираються модулем `ast` — **без виконання**. Це принципово:
|
||||||
|
NOC-скрипт імпортує половину свого фреймворку, і спроба його імпортувати
|
||||||
|
означала б або тягнути весь NOC, або підсовувати заглушки, які мовчки
|
||||||
|
змінюють поведінку. AST бачить рівно те, що написано в тексті.
|
||||||
|
|
||||||
|
З кожного `get_config.py` витягується:
|
||||||
|
|
||||||
|
* виклики `self.cli("…")` — команди збору;
|
||||||
|
* гілка `policy == "s"` — команда startup-конфігу;
|
||||||
|
* `strip_first_lines(config, N)` — скільки службових рядків відкинути;
|
||||||
|
* `@BaseScript.match(platform__regex=…)` — варіанти під конкретні моделі.
|
||||||
|
|
||||||
|
## Що звідки взято — це важливо розрізняти
|
||||||
|
|
||||||
|
**Команди збору — з NOC.** Головна цінність імпорту: сотні годин чужої
|
||||||
|
роботи з реальним залізом.
|
||||||
|
|
||||||
|
**Промпти, вимкнення пейджера й шаблони маскування — НЕ з NOC.**
|
||||||
|
У переданому дампі всі `__init__.py` порожні (359 із 364), а саме там
|
||||||
|
NOC тримає `pattern_prompt` і `command_disable_pager`. Тому вони
|
||||||
|
проставлені за родиною вендора з типових значень і **потребують
|
||||||
|
перевірки на живому залізі** перед промисловим ужитком.
|
||||||
|
|
||||||
|
## Що не витягується
|
||||||
|
|
||||||
|
З 170 файлів команду дають 147. Решта:
|
||||||
|
|
||||||
|
* **21 — не використовують CLI взагалі.** Конфіг знімається через HTTP
|
||||||
|
(`self.http.get(...)`) або збирається з шматків: камери Axis, Beward,
|
||||||
|
Dahua, Hikvision, відеокодери Harmonic, MikroTik SwOS, TFortis, HP
|
||||||
|
iLO2, OS/Linux. Для них потрібен окремий збирач по HTTP, а не CLI.
|
||||||
|
* **2 — порожні заглушки в самому NOC**: `Alcatel/7324RU` і
|
||||||
|
`Ruckus/SmartZone` мають клас `Script` без жодного методу.
|
||||||
|
|
||||||
|
Ці 23 не «загубились» — у них просто немає CLI-команди, яку можна взяти.
|
||||||
160
db/import/extract_noc.py
Normal file
160
db/import/extract_noc.py
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
#!/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()
|
||||||
271
db/import/gen_profiles.py
Normal file
271
db/import/gen_profiles.py
Normal file
|
|
@ -0,0 +1,271 @@
|
||||||
|
#!/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()
|
||||||
69
db/import/why_skipped.py
Normal file
69
db/import/why_skipped.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Чому конкретний get_config.py не дав команди — з цитатою з джерела."""
|
||||||
|
import ast
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(sys.argv[1])
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).parent))
|
||||||
|
from extract_noc import parse_file # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def classify(path, res):
|
||||||
|
src = path.read_text(encoding="utf-8", errors="replace")
|
||||||
|
|
||||||
|
if "error" in res:
|
||||||
|
return "не розібрався", res["error"]
|
||||||
|
|
||||||
|
variants = res["variants"]
|
||||||
|
if not variants:
|
||||||
|
# Метод execute* є, але без cli/snmp — або його немає взагалі.
|
||||||
|
if "def execute" not in src:
|
||||||
|
return "немає методу execute", ""
|
||||||
|
return "execute без cli()", first_body(src)
|
||||||
|
|
||||||
|
if not any(v["running"] or v["startup"] for v in variants):
|
||||||
|
if any(v["uses_snmp"] for v in variants):
|
||||||
|
return "лише SNMP", ""
|
||||||
|
if any(v["dynamic_cli"] for v in variants):
|
||||||
|
return "команда збирається в рантаймі", dynamic_line(src)
|
||||||
|
return "невідомо", ""
|
||||||
|
return None, ""
|
||||||
|
|
||||||
|
|
||||||
|
def first_body(src):
|
||||||
|
m = re.search(r"def execute\w*\([^)]*\):\s*\n((?:\s{8}.*\n){1,6})", src)
|
||||||
|
return (m.group(1).strip().splitlines()[0] if m else "")[:100]
|
||||||
|
|
||||||
|
|
||||||
|
def dynamic_line(src):
|
||||||
|
for line in src.splitlines():
|
||||||
|
if ".cli(" in line and not re.search(r'\.cli\(\s*["\']', line):
|
||||||
|
return line.strip()[:100]
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
groups = {}
|
||||||
|
for f in sorted(ROOT.rglob("get_config.py")):
|
||||||
|
res = parse_file(f)
|
||||||
|
reason, detail = classify(f, res)
|
||||||
|
if reason is None:
|
||||||
|
continue
|
||||||
|
rel = str(f.relative_to(ROOT)).replace("\\", "/")
|
||||||
|
groups.setdefault(reason, []).append((rel, detail))
|
||||||
|
|
||||||
|
total = sum(len(v) for v in groups.values())
|
||||||
|
print(f"не витягнулось: {total}\n")
|
||||||
|
for reason, items in sorted(groups.items(), key=lambda x: -len(x[1])):
|
||||||
|
print(f"=== {reason}: {len(items)} ===")
|
||||||
|
for rel, detail in items:
|
||||||
|
print(f" {rel}")
|
||||||
|
if detail:
|
||||||
|
print(f" {detail}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1191
db/migrations/0014_ncm_profiles.sql
Normal file
1191
db/migrations/0014_ncm_profiles.sql
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -497,6 +497,28 @@ SNMP-community, паролі SSH і Telnet живуть в `inv.credentials`,
|
||||||
зашифровані тим самим кільцем, що й секрети каналів. Прив'язка до хоста —
|
зашифровані тим самим кільцем, що й секрети каналів. Прив'язка до хоста —
|
||||||
`credential_ids` у тілі хоста. Без доступу працює лише `icmp.ping`.
|
`credential_ids` у тілі хоста. Без доступу працює лише `icmp.ping`.
|
||||||
|
|
||||||
|
## Профілі збору конфігу
|
||||||
|
|
||||||
|
`ncm.profiles` описує, як зняти конфіг із конкретної платформи: які
|
||||||
|
команди виконати, за яким промптом ловити кінець виводу, що вирізати з
|
||||||
|
diff (`scrub_patterns`) і що замаскувати перед записом у Git
|
||||||
|
(`redact_patterns`).
|
||||||
|
|
||||||
|
Вбудовано **148 профілів на 67 вендорів**, імпортованих із
|
||||||
|
[NOC Project](https://getnoc.com) — Cisco, Huawei, Juniper, MikroTik,
|
||||||
|
Eltex, D-Link, HP, Brocade, Extreme, Alcatel, Allied Telesis, Qtech,
|
||||||
|
ZTE, BDCOM та інші. Інструмент імпорту й межі його застосовності —
|
||||||
|
`db/import/README.md`.
|
||||||
|
|
||||||
|
Команди взято з NOC. Промпти й вимкнення пейджера — ні: у дампі
|
||||||
|
профілів NOC вони не містились, тому проставлені за родиною вендора з
|
||||||
|
типових значень і потребують перевірки на живому залізі.
|
||||||
|
|
||||||
|
**Виконувати ці профілі поки нікому.** Агентського модуля `ncm`
|
||||||
|
(SSH/Telnet) ще немає — це Етап 7. Профілі лежать готовими даними, і
|
||||||
|
щойно модуль з'явиться, збір конфігу запрацює на всьому парку без
|
||||||
|
дописування коду під кожен вендор.
|
||||||
|
|
||||||
## Групи й доступ до хостів
|
## Групи й доступ до хостів
|
||||||
|
|
||||||
Два незалежні виміри, які не можна змішувати в одному списку прав:
|
Два незалежні виміри, які не можна змішувати в одному списку прав:
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue