fix(api): проверять однозначность Router ID оптимизатора
This commit is contained in:
1 parent
7077844655
commit
338335ddc0
6 files changed
+63
-23
No files matched your search
@@ -26,7 +26,7 @@
|
||||
"test:live-maps": "tsx src/services/ospf-route-parse.test.ts && tsx src/services/vxlan-live.test.ts && tsx src/services/containers-live.test.ts && tsx src/services/firewall-poll.test.ts && tsx src/services/gre-catalog-poll.test.ts && tsx src/routes/filters-gre-catalog.test.ts && tsx src/routes/bgp-catalog.test.ts && tsx src/services/bgp-catalog-parse.test.ts && tsx src/routes/ospf-catalog.test.ts && tsx src/services/ospf-observed-values.test.ts",
|
||||
"test": "npm run test:alert-engine && npm run test:auth && npm run test:wireguard && npm run test:traffic-rate && npm run test:traffic-flow && npm run test:users && npm run test:pg && npm run test:backups && npm run test:live-maps && npm run test:ospf-optimizer && npm run test:recursive-routes && npm run test:config-sync && npm run test:ros-file",
|
||||
"test:geoip": "tsx src/services/traffic-flow-geoip.test.ts",
|
||||
"test:ospf-optimizer": "tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/routes/ospf-optimizer.test.ts"
|
||||
"test:ospf-optimizer": "tsx src/services/ospf-optimizer-identity.test.ts && tsx src/services/ospf-optimizer-metrics.test.ts && tsx src/services/ospf-optimizer-plan.test.ts && tsx src/routes/ospf-optimizer.test.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.888.0",
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from "../types/server.js"
|
||||
import { parseOspfGateway, parseOspfRouteType, parseOspfCatalogRouteType } from "../services/ospf-route-parse.js"
|
||||
import { z } from "zod"
|
||||
import {resolveOptimizerRouterOwners} from "../services/ospf-optimizer-identity.js"
|
||||
import {readOspfOptimizerObserved} from "../services/ospf-optimizer-metrics.js"
|
||||
import type {OspfOptimizerObserved} from "@mmapp/contracts"
|
||||
import {readEditableOspfTemplates,validateOspfOptimizationPlan} from "../services/ospf-optimizer-plan.js"
|
||||
@@ -362,27 +363,10 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
|
||||
|
||||
async function buildRouterIdToServerMap(needed: Set<string>): Promise<Map<string, ServerRow["id"]>> {
|
||||
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
const out = new Map<string, ServerRow["id"]>()
|
||||
const neededLeft = new Set(needed)
|
||||
|
||||
for (const s of enabledServers) {
|
||||
if (neededLeft.size === 0) break
|
||||
try {
|
||||
const c = MikrotikClient.fromServer(s)
|
||||
const inst = await c.getOspfInstances()
|
||||
for (const i of inst) {
|
||||
const rid = String(i["router-id"] ?? "").trim()
|
||||
if (!rid) continue
|
||||
if (neededLeft.has(rid)) {
|
||||
out.set(rid, s.id)
|
||||
neededLeft.delete(rid)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore per-server errors, preview should still work
|
||||
}
|
||||
}
|
||||
return out
|
||||
return resolveOptimizerRouterOwners(enabledServers,needed,async id=>{
|
||||
const target=enabledServers.find(server=>server.id===id)!
|
||||
return MikrotikClient.fromServer(target).getOspfInstances()
|
||||
})
|
||||
}
|
||||
|
||||
const routerIdToServerId = await buildRouterIdToServerMap(neededRouterIds)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import assert from "node:assert/strict"
|
||||
import {resolveOptimizerRouterOwners} from "./ospf-optimizer-identity.js"
|
||||
const devices=[{id:1},{id:2}],needed=new Set(["192.0.2.1"])
|
||||
const instance={"router-id":"192.0.2.1",disabled:"false"}
|
||||
let calls:number[]=[]
|
||||
const resolved=await resolveOptimizerRouterOwners(devices,needed,async id=>{calls.push(id);return id===1?[instance,instance]:[{"router-id":"192.0.2.2",disabled:"false"}]})
|
||||
assert.equal(resolved.get("192.0.2.1"),1);assert.deepEqual(calls,[1,2])
|
||||
await assert.rejects(()=>resolveOptimizerRouterOwners(devices,needed,async()=>[instance]),/нескольким/)
|
||||
await assert.rejects(()=>resolveOptimizerRouterOwners(devices,needed,async id=>{if(id===2)throw new Error("offline");return [instance]}),/проверить/)
|
||||
for(const raw of [null,{},[{"router-id":"main",disabled:"false"}],[{"router-id":"0.0.0.0",disabled:"false"}],[{"router-id":"192.0.2.1"}],[{"router-id":"192.00.2.1",disabled:"false"}]])await assert.rejects(()=>resolveOptimizerRouterOwners(devices,needed,async()=>raw))
|
||||
assert.equal((await resolveOptimizerRouterOwners(devices,needed,async()=>[{disabled:"true"}])).size,0)
|
||||
assert.equal((await resolveOptimizerRouterOwners(devices,needed,async()=>[])).size,0)
|
||||
calls=[];await resolveOptimizerRouterOwners(devices,new Set(),async id=>{calls.push(id);return []});assert.equal(calls.length,0)
|
||||
console.log("OSPF optimizer identity: complete inventory, literal IDs, duplicate ownership/unknown/failed refusal; no early first-match")
|
||||
@@ -0,0 +1,31 @@
|
||||
import {z} from "zod"
|
||||
import {observedBoolean} from "./ospf-observed-values.js"
|
||||
const schema=z.array(z.object({"router-id":z.string().optional(),disabled:z.string().optional()}))
|
||||
function literal(value:string|undefined){
|
||||
const id=value?.trim(),parts=id?.split(".")
|
||||
return id && id!=="0.0.0.0" && parts?.length===4 && parts.every(part=>/^(0|[1-9]\d{0,2})$/.test(part)&&Number(part)<=255)?id:null
|
||||
}
|
||||
export async function resolveOptimizerRouterOwners(devices:readonly {id:number}[],needed:Set<string>,read:(id:number)=>Promise<unknown>):Promise<Map<string,number>>{
|
||||
if(needed.size===0)return new Map()
|
||||
const owners=new Map<string,Set<number>>()
|
||||
for(const device of devices){
|
||||
let raw:unknown
|
||||
try {raw=await read(device.id)} catch {throw new Error("Не удалось проверить Router ID устройства "+device.id)}
|
||||
const parsed=schema.safeParse(raw)
|
||||
if(!parsed.success)throw new Error("Недостоверный каталог OSPF устройства "+device.id)
|
||||
for(const instance of parsed.data){
|
||||
const disabled=observedBoolean(instance.disabled)
|
||||
if(disabled===true)continue
|
||||
const id=literal(instance["router-id"])
|
||||
if(disabled===null||id===null)throw new Error("Неизвестен Router ID или disabled устройства "+device.id)
|
||||
const set=owners.get(id)??new Set<number>();set.add(device.id);owners.set(id,set)
|
||||
}
|
||||
}
|
||||
const result=new Map<string,number>()
|
||||
for(const id of needed){
|
||||
const set=owners.get(id)
|
||||
if(set && set.size>1)throw new Error("Router ID "+id+" принадлежит нескольким устройствам")
|
||||
if(set?.size===1)result.set(id,[...set][0])
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -26,11 +26,18 @@
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.40; OSPF provenance добавлен, далее идентичность Router ID и визуальная приёмка |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.41; Router ID проверяется без first-match, далее визуальная приёмка |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
## Блок 10.41 — однозначное устройство OSPF (2026-10-01)
|
||||
|
||||
- Сопоставление Router ID больше не останавливается на первом совпадении. Проверяются все enabled устройства: нужен literal ID активного экземпляра, повтор на другом устройстве отклоняется; неизвестный disabled/ID либо отказ опроса сообщает ошибку до write. Повторы одного ID внутри одного устройства допустимы; known disabled исключены. Без соседских Router ID дополнительный сетевой опрос не нужен.
|
||||
- Изменена достоверность контекста, формулы score/cost и порядок apply прежние. Для RouterOS с динамическим/symbolic Router ID или отсутствующими flags потребуется отдельная проверка фактического каталога; их не угадываем по host.
|
||||
- Backend optimizer identity/metrics/parser/HTTP tests и production backend прошли. Lint без ошибок, два прежних unused warning в ospf.ts. Реальных RouterOS запросов не выполняли.
|
||||
- Далее обновить 1440/390/320 screenshots и выполнить локальную приёмку оптимизатора; затем карта сети.
|
||||
|
||||
## Блок 10.40 — происхождение метрик OSPF API (2026-10-01)
|
||||
|
||||
- Общий контракт @mmapp/contracts: value/source/sourceId/measuredAt для Ping/DL/UL. Источники speed-probe, interface-traffic, unknown; неизвестность не становится нулём. Задержка трафиком не измеряется. Backend добавляет observed данные до валидации ranked plan; некорректные данные отклоняются до write. Ranking/model defaults прежние.
|
||||
|
||||
@@ -212,3 +212,7 @@ server сохраняет выбор/reload/history без потери друг
|
||||
## 10.40. OSPF provenance API
|
||||
|
||||
Shared контракт и backend observed исходные значения/даты, UI модель отдельно от снимка. Legacy без observed даёт неизвестность; rank/write прежние. Backend optimizer/live-maps, 30 E2E, TS/lint и обе production сборки прошли. Далее Router ID ambiguity, затем visual acceptance.
|
||||
|
||||
## 10.41. Однозначность Router ID
|
||||
|
||||
Полный опрос enabled inventory без first-match; duplicate ownership/unknown/failed каталог отклоняет план до write. Known disabled/empty и одинаковый ID одного устройства допустимы. Optimizer suite/backend build прошли, lint два прежних warning. Реальный RouterOS каталог ещё внешняя приёмка; далее screenshots.
|
||||
Reference in new issue
Block a user