fix(api): проверять однозначность Router ID оптимизатора

This commit is contained in:
Denozordec committed 2026-10-01 00:53:40 +07:00
1 parent 7077844655
commit 338335ddc0
6 files changed
+63 -23

No files matched your search

+5 -21
View File
@@ -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
}