fix(routing): исключить опрос выключенных устройств BGP и OSPF

This commit is contained in:
Denozordec committed 2026-10-09 17:22:41 +07:00
1 parent 89de7e134b
commit 8795969871
10 files changed
+299 -13

No files matched your search

+12
View File
@@ -50,3 +50,15 @@ assert.equal((await app.inject({ method: "GET", url: "/api/servers/0/bgp/session
assert.equal(reads, before)
await app.close()
console.log("BGP catalog: partial/empty/failed/context/duplicates and HTTP compatibility/503 passed")
for (const type of ["home-router", "exit-node"]) {
let enabled = false, polled = 0
const testApp = Fastify(); testApp.setValidatorCompiler(validatorCompiler); testApp.setSerializerCompiler(serializerCompiler)
await testApp.register(createBgpRoutes({listEnabledServers:async()=>[],findServer:async id=>id===1?({id:1,name:"test",host:"192.0.2.1",enabled,type} as typeof servers.$inferSelect):undefined,readSessions:async()=>{polled++;return []},readRawSessions:async()=>{polled++;return []}}),{prefix:"/api"})
for (const url of ["/api/bgp/sessions/raw/1","/api/servers/1/bgp/sessions"]) {const r=await testApp.inject({method:"GET",url});assert.equal(r.statusCode,409);assert.match(r.json().error,/выключен/)}
assert.equal(polled,0)
for(const url of ["/api/bgp/sessions/raw/99","/api/servers/99/bgp/sessions"])assert.equal((await testApp.inject({method:"GET",url})).statusCode,404)
assert.equal((await testApp.inject({method:"GET",url:"/api/bgp/sessions/raw/1oops"})).statusCode,400);assert.equal(polled,0)
enabled=true;assert.equal((await testApp.inject({method:"GET",url:"/api/bgp/sessions/raw/1"})).statusCode,200);assert.equal(polled,1)
await testApp.close()
}
console.log("BGP selected/raw: disabled home-router/exit-node do not poll; unknown/invalid IDs and enabled read preserved")
+7 -4
View File
@@ -15,6 +15,7 @@ interface BgpDependencies {
findServer: (id: number) => Promise<ServerRow | undefined>
readCatalogSessions?: (server: ServerRow) => Promise<BgpCatalogSessionRead[]>
readSessions: (server: ServerRow) => Promise<BgpSessionRead[]>
readRawSessions?: (server: ServerRow) => Promise<unknown[]>
}
const defaults: BgpDependencies = {
listEnabledServers: () => db.select().from(servers).where(eq(servers.enabled, true)),
@@ -33,12 +34,13 @@ export function createBgpRoutes(dependencies: BgpDependencies = defaults): Fasti
// GET /api/bgp/sessions/raw/:id — raw RouterOS response for a single server (debug)
app.get("/bgp/sessions/raw/:id", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const server = (await db.select().from(servers).where(eq(servers.id, params.id)).limit(1))[0]
const server = await dependencies.findServer(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
if (server.enabled === false) return reply.status(409).send({ error: "Сервер выключен. Опрос BGP не выполнялся" })
try {
const client = MikrotikClient.fromServer(server)
const raw = await client.getBgpSessionsRaw()
app.log.info({ serverId: server.id, host: server.host, raw }, "BGP raw response")
const raw = await (dependencies.readRawSessions ?? (s => MikrotikClient.fromServer(s).getBgpSessionsRaw()))(server)
if (!Array.isArray(raw)) throw new Error("Некорректный список BGP sessions")
app.log.info({ serverId: server.id, sessionCount: raw.length }, "BGP raw response")
return reply.send({ server: { id: server.id, host: server.host, name: server.name }, raw })
} catch (err) {
return reply.status(500).send({ error: String(err) })
@@ -58,6 +60,7 @@ export function createBgpRoutes(dependencies: BgpDependencies = defaults): Fasti
const server = await dependencies.findServer(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
if (server.enabled === false) return reply.status(409).send({ error: "Сервер выключен. Опрос BGP не выполнялся" })
try {
const catalog = await pollBgpCatalog([server], dependencies.readSessions)
+14
View File
@@ -71,3 +71,17 @@ assert.equal(parseBfdCatalogSessions(bfdServer, rawBfd("up"))[0].multihop, null)
assert.equal(parseBfdCatalogSessions(bfdServer, [{ ...rawBfd("up")[0], multihop: "false" }])[0].multihop, false)
assert.equal(parseCatalogRoutes(bfdServer, [rawRoute])[0].type, "Unknown")
assert.equal(parseCatalogRoutes(bfdServer, [{ ...rawRoute, "ospf-type": "intra-area" }])[0].type, "O")
for (const type of ["home-router", "exit-node"]) {
const {MikrotikClient}=await import("../services/mikrotik.js");const original=MikrotikClient.fromServer;let reads=0
MikrotikClient.fromServer=()=>{reads++;throw Error("must not poll")}
const testApp=Fastify();testApp.setValidatorCompiler(validatorCompiler);testApp.setSerializerCompiler(serializerCompiler)
try {await testApp.register(createOspfRoutes({listEnabledServers:async()=>[],readCatalog:async()=>{throw Error("must not poll")},findServer:async id=>id===1?({id:1,name:"test",enabled:false,type} as typeof servers.$inferSelect):undefined}),{prefix:"/api"});const r=await testApp.inject({method:"GET",url:"/api/servers/1/ospf"});assert.equal(r.statusCode,409);assert.match(r.json().error,/выключен/);assert.equal((await testApp.inject({method:"GET",url:"/api/servers/99/ospf"})).statusCode,404);assert.equal((await testApp.inject({method:"GET",url:"/api/servers/1oops/ospf"})).statusCode,400);assert.equal(reads,0)}finally{await testApp.close();MikrotikClient.fromServer=original}
}
console.log("OSPF selected: disabled home-router/exit-node do not poll; unknown/invalid IDs fail before RouterOS")
{
const {MikrotikClient}=await import("../services/mikrotik.js"),original=MikrotikClient.fromServer
MikrotikClient.fromServer=()=>({getOspfNeighbors:async()=>[],getOspfAreas:async()=>[],getOspfInterfaceTemplates:async()=>[],getOspfInstances:async()=>[],getBfdSessions:async()=>{throw Error("BFD unavailable")},getIpRoutes:async()=>[]} as unknown as MikrotikClient)
const testApp=Fastify();testApp.setValidatorCompiler(validatorCompiler);testApp.setSerializerCompiler(serializerCompiler)
try{await testApp.register(createOspfRoutes({listEnabledServers:async()=>[],readCatalog:async()=>emptyOspfCatalog(),findServer:async()=>({id:1,name:"test",enabled:true} as typeof servers.$inferSelect)}),{prefix:"/api"});const r=await testApp.inject({method:"GET",url:"/api/servers/1/ospf"});assert.equal(r.statusCode,503);assert.match(r.json().error,/BFD unavailable/)}finally{await testApp.close();MikrotikClient.fromServer=original}
}
console.log("OSPF selected: failed BFD does not become successful empty data")
+5 -3
View File
@@ -553,6 +553,7 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
interface OspfCatalogDependencies {
listEnabledServers: () => Promise<ServerRow[]>
readCatalog: (server: ServerRow) => Promise<OspfCatalogCollections>
findServer?: (id: number) => Promise<ServerRow | undefined>
getOptimizationServer?: (id:number)=>Promise<ServerRow | undefined>
buildOptimizationPlan?: (server:ServerRow,weight:number)=>Promise<OspfRankedInterface[]>
setTemplateCost?: (server:ServerRow,id:string,cost:number)=>Promise<void>
@@ -676,15 +677,16 @@ export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalog
// GET /api/servers/:id/ospf — single server OSPF + BFD data
app.get("/servers/:id/ospf", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
const params = req.params as ServerIdParams
const server = (await db
const server = dependencies.findServer ? await dependencies.findServer(params.id) : (await db
.select().from(servers)
.where(eq(servers.id, params.id))
.limit(1))[0]
if (!server) return reply.status(404).send({ error: "Server not found" })
if (server.enabled === false) return reply.status(409).send({ error: "Сервер выключен. Опрос OSPF не выполнялся" })
try {
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await fetchServerOspf(server)
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await readOspfCatalogRaw(MikrotikClient.fromServer(server))
const areaMap = buildAreaMap(areas)
return reply.send({
neighbors: parseNeighbors(server, neighbors, areaMap),
@@ -695,7 +697,7 @@ export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalog
areas: areas.map(a => ({ name: a.name, areaId: a["area-id"] ?? "0.0.0.0", type: a.type, disabled: a.disabled === "true", inactive: a.inactive === "true", instance: a.instance })),
})
} catch (err) {
return reply.status(500).send({ error: String(err) })
return reply.status(503).send({ error: String(err) })
}
})