Один коміт, а не десяток тематичних, свідомо: теми переплетені в
спільних файлах (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 серпня.
231 lines
11 KiB
Go
231 lines
11 KiB
Go
package store
|
||
|
||
import "strings"
|
||
|
||
// Словник трапів: чесна відповідь на питання про MIB.
|
||
//
|
||
// 0058 відмовився від джерела `trap` з таким аргументом: «без словника
|
||
// MIB умова звелася б до порівняння сирих OID, тобто до другої мовчазної
|
||
// обіцянки». Аргумент правильний, а от висновок із нього був занадто
|
||
// широкий. Він припускав, що словник буває або повний, або ніякий.
|
||
//
|
||
// Повного не буде, і це рішення, а не відкладена робота. Повний словник
|
||
// MIB — це компілятор ASN.1, сховище вендорських файлів під кожен
|
||
// кабінет і підтримка діалектів, у яких один і той самий виробник
|
||
// суперечить сам собі між прошивками. Продукт, який обіцяє «завантажте
|
||
// свої MIB», обіцяє все це, і обіцяє надовго.
|
||
//
|
||
// Але між «усі MIB світу» і «нічого» є те, що працює вже: шість трапів,
|
||
// однакових у КОЖНОГО виробника, бо їх визначає сам протокол (RFC 1215,
|
||
// вони ж snmpTraps з RFC 3418). Порт упав, порт піднявся, коробка
|
||
// перезавантажилась, хтось підбирає community — це і є те, заради чого
|
||
// трапи вмикають у переважній більшості випадків.
|
||
//
|
||
// Решта лишається сирим OID із позначкою «невідомий трап». Саме
|
||
// позначкою: підставити назву, вгадану за схожістю префікса, означало б
|
||
// збрехати рівно там, де написаному довіряють найбільше. І поруч —
|
||
// власний словник кабінету (inv.trap_oids), у який вписують ті кілька
|
||
// вендорських трапів, які насправді потрібні. Кілька, а не тисячі: у
|
||
// реальному кабінеті трапів, на які хтось справді дивиться, менше
|
||
// десятка.
|
||
//
|
||
// Словник живе на СЕРВЕРІ, а не на зонді, і це принципово. Зонд віддає
|
||
// сирий OID; назва приліплюється при читанні. Тому назва, дописана
|
||
// сьогодні, підписує й учорашній журнал — а якби перекладав зонд, увесь
|
||
// архів лишився б із тим словником, який був на момент прийому.
|
||
|
||
// TrapMeaning — те, що ми знаємо про трап.
|
||
type TrapMeaning struct {
|
||
// Коротка назва з MIB, як її пишуть у документації вендора.
|
||
Name string `json:"name"`
|
||
// Що це означає людською. Порожньо для власних записів кабінету —
|
||
// їх пише сам клієнт і не зобов'язаний пояснювати.
|
||
Description string `json:"description,omitempty"`
|
||
// Звідки взялась назва: `builtin` — з цього переліку, `custom` —
|
||
// зі словника кабінету. Показується поруч із назвою, щоб «так
|
||
// вирішили ми» не плуталось із «так вирішив стандарт».
|
||
Origin string `json:"origin"`
|
||
}
|
||
|
||
// builtinTraps — шість трапів, визначених самим протоколом.
|
||
//
|
||
// Перелік свідомо не росте «за потребою». Кожен доданий сюди рядок —
|
||
// це твердження «так у ВСІХ вендорів», і воно має бути правдою, а не
|
||
// спостереженням на трьох пристроях. Усе, що вужче за це, — робота
|
||
// словника кабінету.
|
||
var builtinTraps = map[string]TrapMeaning{
|
||
"1.3.6.1.6.3.1.1.5.1": {
|
||
Name: "coldStart",
|
||
Description: "пристрій перезавантажився з повним скиданням стану — живлення або reload",
|
||
},
|
||
"1.3.6.1.6.3.1.1.5.2": {
|
||
Name: "warmStart",
|
||
Description: "агент SNMP перезапустився, сам пристрій не перезавантажувався",
|
||
},
|
||
"1.3.6.1.6.3.1.1.5.3": {
|
||
Name: "linkDown",
|
||
Description: "порт перейшов у стан down",
|
||
},
|
||
"1.3.6.1.6.3.1.1.5.4": {
|
||
Name: "linkUp",
|
||
Description: "порт перейшов у стан up",
|
||
},
|
||
"1.3.6.1.6.3.1.1.5.5": {
|
||
Name: "authenticationFailure",
|
||
Description: "звернення по SNMP з невірним community або обліковим записом",
|
||
},
|
||
"1.3.6.1.6.3.1.1.5.6": {
|
||
Name: "egpNeighborLoss",
|
||
Description: "втрачено сусіда EGP; на сучасному обладнанні майже не трапляється",
|
||
},
|
||
}
|
||
|
||
// builtinVarbinds — назви полів, які приходять у самих трапах.
|
||
//
|
||
// Це не MIB пристрою, а обов'язкова частина будь-якого трапа (RFC 1213
|
||
// і RFC 3418) плюс чотири колонки ifTable, без яких linkDown не
|
||
// відповідає на єдине питання, яке до нього ставлять: «який саме порт».
|
||
var builtinVarbinds = map[string]string{
|
||
"1.3.6.1.2.1.1.3.0": "sysUpTime",
|
||
"1.3.6.1.2.1.1.5.0": "sysName",
|
||
"1.3.6.1.6.3.1.1.4.1.0": "snmpTrapOID",
|
||
"1.3.6.1.6.3.1.1.4.3.0": "snmpTrapEnterprise",
|
||
"1.3.6.1.6.3.18.1.3.0": "snmpTrapAddress",
|
||
"1.3.6.1.2.1.2.2.1.1": "ifIndex",
|
||
"1.3.6.1.2.1.2.2.1.2": "ifDescr",
|
||
"1.3.6.1.2.1.2.2.1.7": "ifAdminStatus",
|
||
"1.3.6.1.2.1.2.2.1.8": "ifOperStatus",
|
||
"1.3.6.1.2.1.31.1.1.1.1": "ifName",
|
||
}
|
||
|
||
// ifStatusNames розшифровує ifAdminStatus/ifOperStatus (RFC 2863).
|
||
//
|
||
// Без цього linkDown показує «2», і людина мусить пам'ятати, що двійка
|
||
// — це down. Розшифровка вузька навмисно: значення тут визначає той
|
||
// самий стандарт, що й самі стовпці, тож це не вгадування.
|
||
var ifStatusNames = map[string]string{
|
||
"1": "up", "2": "down", "3": "testing",
|
||
"4": "unknown", "5": "dormant", "6": "notPresent", "7": "lowerLayerDown",
|
||
}
|
||
|
||
// BuiltinTrapNames віддає вбудований словник для форми правила.
|
||
//
|
||
// Формі він потрібен рівно для того, щоб людина могла обрати «linkDown»
|
||
// зі списку, а не набирати `1.3.6.1.6.3.1.1.5.3` з голови. Саме
|
||
// неможливість це набрати й була причиною, з якої 0058 відклав джерело
|
||
// `trap`.
|
||
func BuiltinTrapNames() map[string]TrapMeaning {
|
||
out := make(map[string]TrapMeaning, len(builtinTraps))
|
||
for oid, m := range builtinTraps {
|
||
m.Origin = "builtin"
|
||
out[oid] = m
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ResolveTrapOID перекладає OID трапа в назву.
|
||
//
|
||
// custom — словник кабінету; він має пріоритет над вбудованим. Це
|
||
// свідомо: якщо клієнт вирішив, що `linkDown` у його мережі варто
|
||
// називати «порт впав», сперечатися з ним немає підстав — його мережа,
|
||
// його чергові, його слова.
|
||
//
|
||
// Порожня назва — не помилка, а відповідь: «ми не знаємо, що це».
|
||
// Викликач має показати сам OID і сказати це вголос.
|
||
func ResolveTrapOID(custom map[string]TrapMeaning, oid string) TrapMeaning {
|
||
oid = NormalizeOID(oid)
|
||
if oid == "" {
|
||
return TrapMeaning{}
|
||
}
|
||
if m, ok := custom[oid]; ok {
|
||
m.Origin = "custom"
|
||
return m
|
||
}
|
||
if m, ok := builtinTraps[oid]; ok {
|
||
m.Origin = "builtin"
|
||
return m
|
||
}
|
||
return TrapMeaning{}
|
||
}
|
||
|
||
// ResolveVarbindOID перекладає OID varbind-а в назву стовпця.
|
||
//
|
||
// Індекс рядка відрізається: ifOperStatus.7 і ifOperStatus.19 — це той
|
||
// самий стовпець на різних портах, і словник, у якому треба заводити
|
||
// запис на кожен порт, словником не є. Відрізаних індексів може бути
|
||
// кілька (ifIndex у ifTable один, а от у деяких таблицях складений
|
||
// ключ), тому пробуємо кілька рівнів — але не більше трьох: далі
|
||
// «скорочення» вже перетворюється на вгадування.
|
||
func ResolveVarbindOID(custom map[string]TrapMeaning, oid string) string {
|
||
oid = NormalizeOID(oid)
|
||
if oid == "" {
|
||
return ""
|
||
}
|
||
for i := 0; i < 4; i++ {
|
||
if m, ok := custom[oid]; ok {
|
||
return m.Name
|
||
}
|
||
if n, ok := builtinVarbinds[oid]; ok {
|
||
return n
|
||
}
|
||
cut := strings.LastIndexByte(oid, '.')
|
||
if cut <= 0 {
|
||
break
|
||
}
|
||
oid = oid[:cut]
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// DescribeVarbindValue додає значенню сенс там, де він визначений
|
||
// стандартом. Порожньо означає «показуйте як є».
|
||
func DescribeVarbindValue(oid, value string) string {
|
||
switch ResolveVarbindOID(nil, oid) {
|
||
case "ifAdminStatus", "ifOperStatus":
|
||
return ifStatusNames[value]
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// NormalizeOID зводить OID до однієї форми: без провідної крапки,
|
||
// без пробілів навколо.
|
||
//
|
||
// Одна форма потрібна не для краси. OID приїжджає з трьох місць — від
|
||
// зонда, з форми правила й зі словника кабінету, — і кожне з них має
|
||
// свою звичку щодо провідної крапки. Правило, яке не спрацювало через
|
||
// один символ, виглядало б точно як правило, під яке не було подій.
|
||
func NormalizeOID(s string) string {
|
||
s = strings.TrimSpace(s)
|
||
for len(s) > 0 && s[0] == '.' {
|
||
s = s[1:]
|
||
}
|
||
return strings.TrimRight(s, ".")
|
||
}
|
||
|
||
// ValidOID — чи схоже це на числовий ідентифікатор об'єкта.
|
||
//
|
||
// Перевірка навмисно поверхнева: ми не звіряємось із деревом
|
||
// зареєстрованих гілок і не вимагаємо, щоб перший вузол був 0, 1 або 2.
|
||
// Мета одна — відсіяти явну описку у формі («1.3.6.1.6.3.1.1.5,3» або
|
||
// назву, набрану замість OID), поки людина ще дивиться на форму.
|
||
func ValidOID(s string) bool {
|
||
s = NormalizeOID(s)
|
||
if s == "" {
|
||
return false
|
||
}
|
||
var digits int
|
||
for i := 0; i < len(s); i++ {
|
||
switch c := s[i]; {
|
||
case c >= '0' && c <= '9':
|
||
digits++
|
||
case c == '.':
|
||
if digits == 0 {
|
||
return false
|
||
}
|
||
digits = 0
|
||
default:
|
||
return false
|
||
}
|
||
}
|
||
return digits > 0
|
||
}
|