Імпорт профілів збору конфігу з 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:
byrsapty 2026-08-24 02:34:55 +03:00
parent 48848bd3a5
commit f162dcab19
7 changed files with 1845 additions and 0 deletions

View file

@ -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
View 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
View 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
View 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
View 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()

File diff suppressed because it is too large Load diff

View file

@ -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. Профілі лежать готовими даними, і
щойно модуль з'явиться, збір конфігу запрацює на всьому парку без
дописування коду під кожен вендор.
## Групи й доступ до хостів ## Групи й доступ до хостів
Два незалежні виміри, які не можна змішувати в одному списку прав: Два незалежні виміри, які не можна змішувати в одному списку прав: