Netpulse_SasS/server/internal/store/ncm_paths.go
byrsapty ed8fc831bf Дві сесії роботи: 0058–0068, розгортання однією командою, тести
Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (store.go, docker-compose.yml, deploy/README.md), і
розділити їх можна було б лише індексуванням шматків. Коміти, які не
збираються, гірші за один великий — тим паче що це рівно той стан, який
перевірявся разом.

ЩО ПРАЦЮЄ НА СТЕНДІ Й ПЕРЕВІРЕНО ТАМ

  0058  подієві алерти: syslog, ncm, compliance спрацьовують у мить
        події; правило з нереалізованим джерелом більше не зберігається
        мовчки
  0059  snmp.walk і прототипи шаблонів — таблиці з динамічним індексом
        описуються шаблоном, а не Go
  0060  відкат конфігу: план як різниця, маскування паролів із підписом
        плану, обов'язковий контрольний збір, verifying при обриві
  0061  кнопки Telegram: довге опитування, авторизація не з callback_data
  0062  аудит і архів хостів; тест на AST, що падає на ключі без назви
  0063  RLS: три ролі, окремий пул для фонових тактів
  0064  строки зберігання даних і сторінка сховища
  0065  приймач SNMP-трапів; перевірено справжніми пакетами по дроту,
        переклад v1→v2 за RFC 3584 дає правильний OID
  0066  ескалації сповіщень
  0067  алерт про вичерпання диска
  0068  поля заливки конфігу переїхали в каталог профілів

Плюс: 137 тестів вебу з нуля (їх не було взагалі), одинадцять справжніх
вад, знайдених ними й виправлених, і виправлення двох інтеграційних
тестів grpcapi, які мовчки пропускались півтора року.

ЩО ЩЕ НЕ ЗАПУСКАЛОСЬ

  netpulse            установник: одна команда замість 18 змінних і
                      593 рядків інструкції
  RLS з першого запуску  нова інсталяція під політиками одразу;
                      RLS-EXISTING-INSTALL.md лишається тільки для
                      старих інсталяцій
  .forgejo + CI       раннер не зареєстрований

Ці три перевірені компіляцією й міркуванням, але не виконанням.

ГОЛОВНИЙ ВИСНОВОК ДВОХ СЕСІЙ

Зелена перевірка доводить рівно те, що вона перевіряє. Тест ізоляції RLS
був правильний і зелений — і пропустив зламаний вхід, бо перевіряв «чи
не видно чужого», коли зламалось «чи видно своє». Інтеграційні тести
grpcapi були зелені, бо не виконувались. Схема, довідник і протокол
описували те, чого в коді не існувало, і виглядало це як готове.

Тому в кожному завданні цих сесій стояла вимога назвати НЕПОКРИТЕ, а
чотири задачі закінчились не можливістю, а відмовою: правило з
нереалізованим джерелом не зберігається, профіль без команд заливки
каже про це замість мовчазної кнопки, міграція RLS валить сама себе на
таблиці без політики, тест словника аудиту падає на ключі без назви.

Подробиці — HISTORY.md, розділи за 26 і 27 серпня.
2026-08-27 17:32:49 +03:00

130 lines
7.2 KiB
Go
Raw Permalink 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.

package store
import "strings"
// Розкладка файлів усередині репозиторію версій.
//
// Одна функція на весь продукт, і це не про охайність. Шлях складався
// у двох місцях — у StoreConfig (живий збір) і в SyncGit (переливання
// історії з бази). Поки обидва писали той самий рядок, усе працювало;
// щойно вони розійшлися б, та сама версія лягла б у дві різні гілки
// дерева, і історія одного хоста роз'їхалась би надвоє. Дзеркало
// зробило б цей розрив ще й видимим назовні.
//
// ЧОМУ ЦЕ ВАЖЛИВО САМЕ ЗАРАЗ. Сьогодні на хост припадає один конфіг:
// `running`, один файл. Наступний вид вмісту — конфіги СЕРВЕРІВ, а це
// не один файл, а набір: /etc/nginx/nginx.conf, /etc/network/interfaces,
// /etc/postfix/main.cf. Якщо шлях у дереві закріпить припущення «один
// файл на хост», додавання серверів означатиме переписування шляхів —
// тобто переписування історії в уже надісланому дзеркалі. Це
// найдорожча правка з можливих: віддалений сервер прийме її лише
// примусом, а всі, хто вже склонував дзеркало, отримають розбіжність.
//
// Тому контракт закріплюється зараз, до першого push, і він такий:
//
// config_type БЕЗ скісної риски → <хост>/<тип>.cfg
// config_type ЗІ скісною рискою → <хост>/<шлях як є>
//
// Мережеве залізо лишається в першому випадку — байт у байт як
// сьогодні, жоден наявний шлях не зсувається. Сервер піде в другий:
// config_type = "files/etc/nginx/nginx.conf" дасть
// <хост>/files/etc/nginx/nginx.conf. Гілка та сама (device/<uuid>),
// дерево те саме, дзеркало везе це без жодної правки — refspec у нього
// шаблонний, а не перелік.
//
// Чому саме config_type, а не нова колонка. Він уже text, уже входить
// в UNIQUE (device_id, config_type, commit_sha), уже партиціонує
// ретеншен і порівняння версій. Тобто «остання версія цього файлу»,
// «diff між версіями», «тримати останні N» починають працювати для
// кожного файлу сервера окремо — і це саме та поведінка, якої від них
// чекають. Нова колонка дала б другий вимір, який довелося б додати в
// кожен із цих запитів.
// ConfigPath — шлях версії конфігу всередині гілки пристрою.
func ConfigPath(deviceName, configType string) string {
if configType == "" {
configType = "running"
}
dev := sanitizePath(deviceName)
if !strings.Contains(configType, "/") {
return dev + "/" + sanitizePath(configType) + ".cfg"
}
parts := make([]string, 0, 8)
for _, seg := range strings.Split(configType, "/") {
seg = sanitizeSegment(seg)
if seg == "" {
continue
}
parts = append(parts, seg)
}
if len(parts) == 0 {
return dev + "/running.cfg"
}
return dev + "/" + strings.Join(parts, "/")
}
// sanitizeSegment чистить одну ланку шляху.
//
// Відрізняється від sanitizePath рівно в одному: провідна крапка
// зберігається. Для імені хоста вона зайва, а для файлу сервера — це
// різниця між .bashrc і bashrc, тобто між правильним іменем і
// вигаданим. "." і ".." не виживають: вихід за межі дерева тут не
// обговорюється, і gitstore відкидає їх іще раз у splitPath — дві сітки
// замість однієї, бо ціна помилки тут запис за межі репозиторію.
func sanitizeSegment(seg string) string {
if seg == "" || seg == "." || seg == ".." {
return ""
}
lead := ""
if strings.HasPrefix(seg, ".") {
lead = "."
}
out := sanitizePath(seg)
if out == "device" && strings.Trim(seg, ".-") == "" {
// sanitizePath віддає "device" на порожньому результаті — для
// ланки шляху це неправда, а не запасне ім'я.
return ""
}
return lead + out
}
// DeviceBranch — ім'я гілки пристрою в репозиторії тенанта.
//
// Раніше тут стояв самий лише UUID: гілка мусила пережити
// перейменування хоста, інакше історія розсипалась би на дві.
// Міркування правильне, наслідок — ні: у Forgejo людина бачить два
// десятки рядків `device/8d1b1df8-fa6b-4fe9-80c6-31d72978a231` і не
// може знайти серед них свій комутатор ані за іменем, ані за адресою.
// Пошук по гілках не працює взагалі, а саме він і є першим, чим
// користуються в дзеркалі.
//
// Тому гілка тепер читабельна, а стабільність забезпечується інакше —
// перейменуванням самої гілки при зміні імені (див. renameDeviceBranch).
// Це нормальна операція Git: історія лишається однією, змінюється
// тільки підпис.
//
// Адреса в імені не косметика: у мережах повно хостів на кшталт
// «Комутатор» і «ONU», і сама лише назва їх не розрізняє.
func DeviceBranch(deviceID, name, address string) string {
base := sanitizePath(name)
if base == "device" || base == "" {
// Імені немає або від нього нічого не лишилось — тоді хоч
// щось унікальне, інакше всі безіменні зіллються в одну гілку.
base = shortID(deviceID)
}
if address != "" {
return "device/" + base + "-" + sanitizePath(address)
}
// Без адреси покладаємось на ідентифікатор: два хости з однаковою
// назвою — звичайна річ, дві гілки з однаковою назвою — ні.
return "device/" + base + "-" + shortID(deviceID)
}
func shortID(id string) string {
if len(id) >= 8 {
return id[:8]
}
return id
}