fix(api): показывать ошибки опроса старых OSPF и BFD API
This commit is contained in:
1 parent
eaf724d6cc
commit
5ac5a25dfc
17 files changed
+688
-96
No files matched your search
@@ -0,0 +1,32 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import Fastify from 'fastify'
|
||||
import {validatorCompiler,serializerCompiler} from '@fastify/type-provider-zod'
|
||||
import {createOspfRoutes} from './ospf.js'
|
||||
import {emptyOspfCatalog} from '../services/ospf-catalog-poll.js'
|
||||
import {MikrotikClient} from '../services/mikrotik.js'
|
||||
import {servers} from '../db/schema.js'
|
||||
const devices=[{id:1,name:'Первый'},{id:2,name:'Второй'}] as typeof servers.$inferSelect[]
|
||||
const original=MikrotikClient.fromServer
|
||||
let failedIds:number[]=[],reads=0,enabled=true,populated=false
|
||||
MikrotikClient.fromServer=server=>{
|
||||
const read=async()=>{reads++;if(failedIds.includes(server.id))throw Error('Недоступен '+server.name);return []}
|
||||
return {getOspfNeighbors:read,getOspfAreas:read,getOspfInterfaceTemplates:read,getOspfInstances:async()=>{await read();return populated?[{'.id':'*1',name:'ospf-own'}]:[]},getBfdSessions:async()=>{await read();return populated?[{'.id':'*1','local-address':'192.0.2.1','remote-address':'192.0.2.2',state:'up'}]:[]},getIpRoutes:read,getIpv6Routes:read} as unknown as MikrotikClient
|
||||
}
|
||||
const app=Fastify();app.setValidatorCompiler(validatorCompiler);app.setSerializerCompiler(serializerCompiler)
|
||||
let cases=0
|
||||
try{
|
||||
await app.register(createOspfRoutes({listEnabledServers:async()=>enabled?devices:[],readCatalog:async()=>emptyOspfCatalog()}),{prefix:'/api'})
|
||||
for(const phase of ['populated','empty','partial','failed','disabled'] as const){
|
||||
populated=phase==='populated';enabled=phase!=='disabled';failedIds=phase==='partial'?[2]:phase==='failed'?[1,2]:[]
|
||||
for(const endpoint of ['ospf/neighbors','ospf/interfaces','ospf/instances','ospf/all','bfd/sessions']){
|
||||
const before=reads,r=await app.inject({method:'GET',url:'/api/'+endpoint}),body=r.json()
|
||||
assert.equal(r.statusCode,failedIds.length?503:200)
|
||||
if(failedIds.length){assert.deepEqual(body.failures.map((f:{serverId:number})=>f.serverId),failedIds);assert.match(body.error,/OSPF\/BFD/);assert.deepEqual(body.failures.map((f:{error:string})=>f.error),failedIds.map(id=>'Недоступен '+devices[id-1].name))}
|
||||
else if(endpoint==='ospf/all'){assert.deepEqual(Object.keys(body).sort(),['bfdSessions','instances','interfaces','neighbors','routes']);assert.equal(body.instances.length,populated?2:0);assert.equal(body.bfdSessions.length,populated?2:0)}
|
||||
else{assert(Array.isArray(body));assert.equal(body.length,populated&&['ospf/instances','bfd/sessions'].includes(endpoint)?2:0)}
|
||||
if(phase==='disabled')assert.equal(reads,before)
|
||||
cases++
|
||||
}
|
||||
}
|
||||
}finally{await app.close();MikrotikClient.fromServer=original}
|
||||
console.log('Legacy OSPF/BFD: '+cases+' HTTP checks passed; populated/empty/partial/failed/no enabled devices')
|
||||
+47
-87
@@ -18,6 +18,8 @@ import {readEditableOspfTemplates,validateOspfOptimizationPlan} from "../service
|
||||
import { observedInteger, observedDurationMs, observedBoolean } from "../services/ospf-observed-values.js"
|
||||
import { pollOspfCatalog, readOspfCatalogRaw, type OspfCatalogCollections } from "../services/ospf-catalog-poll.js"
|
||||
|
||||
import { pollLegacyOspf } from "../services/ospf-legacy-poll.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
type TrafficSampleRow = typeof trafficSamples.$inferSelect
|
||||
type SpeedProbeRow = typeof uptimeSpeedProbes.$inferSelect
|
||||
@@ -561,108 +563,66 @@ export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalog
|
||||
return async (app) => {
|
||||
app.get("/ospf/catalog", async (_req, reply) => reply.send(await pollOspfCatalog(await dependencies.listEnabledServers(), dependencies.readCatalog)))
|
||||
|
||||
// GET /api/ospf/neighbors — aggregate OSPF neighbors from ALL enabled servers
|
||||
// Legacy success envelopes have no place for partial failures. Fail explicitly.
|
||||
const legacyFailure = (failures: { serverId: number; serverName: string; error: string }[]) => ({
|
||||
error: "Не удалось опросить все устройства OSPF/BFD", failures,
|
||||
})
|
||||
app.get("/ospf/neighbors", async (_req, reply) => {
|
||||
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
|
||||
const results = await Promise.all(
|
||||
allServers.map(async (server) => {
|
||||
try {
|
||||
const { neighbors, areas } = await fetchServerOspf(server)
|
||||
const areaMap = buildAreaMap(areas)
|
||||
return parseNeighbors(server, neighbors, areaMap)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return reply.send(results.flat())
|
||||
const result = await pollLegacyOspf(await dependencies.listEnabledServers(), async server => {
|
||||
const { neighbors, areas } = await fetchServerOspf(server)
|
||||
return parseNeighbors(server, neighbors, buildAreaMap(areas))
|
||||
})
|
||||
if (result.failures.length) return reply.status(503).send(legacyFailure(result.failures))
|
||||
return reply.send(result.values.flat())
|
||||
})
|
||||
|
||||
// GET /api/ospf/interfaces — aggregate OSPF interface templates from ALL enabled servers
|
||||
app.get("/ospf/interfaces", async (_req, reply) => {
|
||||
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
|
||||
const results = await Promise.all(
|
||||
allServers.map(async (server) => {
|
||||
try {
|
||||
const { ifaceTemplates, areas, instances } = await fetchServerOspf(server)
|
||||
const areaMap = buildAreaMap(areas)
|
||||
return parseInterfaces(server, ifaceTemplates, areas, instances, areaMap)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return reply.send(results.flat())
|
||||
const result = await pollLegacyOspf(await dependencies.listEnabledServers(), async server => {
|
||||
const { ifaceTemplates, areas, instances } = await fetchServerOspf(server)
|
||||
return parseInterfaces(server, ifaceTemplates, areas, instances, buildAreaMap(areas))
|
||||
})
|
||||
if (result.failures.length) return reply.status(503).send(legacyFailure(result.failures))
|
||||
return reply.send(result.values.flat())
|
||||
})
|
||||
|
||||
// GET /api/ospf/instances — aggregate OSPF instances from ALL enabled servers
|
||||
app.get("/ospf/instances", async (_req, reply) => {
|
||||
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
|
||||
const results = await Promise.all(
|
||||
allServers.map(async (server) => {
|
||||
try {
|
||||
const { instances } = await fetchServerOspf(server)
|
||||
return parseInstances(server, instances)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
return reply.send(results.flat())
|
||||
const result = await pollLegacyOspf(await dependencies.listEnabledServers(), async server => {
|
||||
const { instances } = await fetchServerOspf(server)
|
||||
return parseInstances(server, instances)
|
||||
})
|
||||
if (result.failures.length) return reply.status(503).send(legacyFailure(result.failures))
|
||||
return reply.send(result.values.flat())
|
||||
})
|
||||
|
||||
// GET /api/ospf/all — single round-trip: neighbors + interfaces + instances + BFD
|
||||
app.get("/ospf/all", async (_req, reply) => {
|
||||
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
|
||||
const perServer = await Promise.all(
|
||||
allServers.map(async (server) => {
|
||||
try {
|
||||
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await fetchServerOspf(server)
|
||||
const areaMap = buildAreaMap(areas)
|
||||
return {
|
||||
neighbors: parseNeighbors(server, neighbors, areaMap),
|
||||
interfaces: parseInterfaces(server, ifaceTemplates, areas, instances, areaMap),
|
||||
instances: parseInstances(server, instances),
|
||||
bfdSessions: parseBfdSessions(server, bfdSessions),
|
||||
routes: parseOspfRoutes(server, ipRoutes),
|
||||
}
|
||||
} catch {
|
||||
return { neighbors: [], interfaces: [], instances: [], bfdSessions: [], routes: [] }
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const result = await pollLegacyOspf(await dependencies.listEnabledServers(), async server => {
|
||||
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await fetchServerOspf(server)
|
||||
const areaMap = buildAreaMap(areas)
|
||||
return {
|
||||
neighbors: parseNeighbors(server, neighbors, areaMap),
|
||||
interfaces: parseInterfaces(server, ifaceTemplates, areas, instances, areaMap),
|
||||
instances: parseInstances(server, instances),
|
||||
bfdSessions: parseBfdSessions(server, bfdSessions),
|
||||
routes: parseOspfRoutes(server, ipRoutes),
|
||||
}
|
||||
})
|
||||
if (result.failures.length) return reply.status(503).send(legacyFailure(result.failures))
|
||||
return reply.send({
|
||||
neighbors: perServer.flatMap(r => r.neighbors),
|
||||
interfaces: perServer.flatMap(r => r.interfaces),
|
||||
instances: perServer.flatMap(r => r.instances),
|
||||
bfdSessions: perServer.flatMap(r => r.bfdSessions),
|
||||
routes: perServer.flatMap(r => r.routes),
|
||||
neighbors: result.values.flatMap(r => r.neighbors),
|
||||
interfaces: result.values.flatMap(r => r.interfaces),
|
||||
instances: result.values.flatMap(r => r.instances),
|
||||
bfdSessions: result.values.flatMap(r => r.bfdSessions),
|
||||
routes: result.values.flatMap(r => r.routes),
|
||||
})
|
||||
})
|
||||
|
||||
// GET /api/bfd/sessions — BFD sessions only (for direct access)
|
||||
app.get("/bfd/sessions", async (_req, reply) => {
|
||||
const allServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
const results = await Promise.all(
|
||||
allServers.map(async (server) => {
|
||||
try {
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
const raw = await client.getBfdSessions()
|
||||
return parseBfdSessions(server, raw)
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}),
|
||||
)
|
||||
return reply.send(results.flat())
|
||||
const result = await pollLegacyOspf(await dependencies.listEnabledServers(), async server => {
|
||||
const raw = await MikrotikClient.fromServer(server).getBfdSessions()
|
||||
return parseBfdSessions(server, raw)
|
||||
})
|
||||
if (result.failures.length) return reply.status(503).send(legacyFailure(result.failures))
|
||||
return reply.send(result.values.flat())
|
||||
})
|
||||
|
||||
// GET /api/servers/:id/ospf — single server OSPF + BFD data
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/** Legacy APIs cannot represent partial data in their successful array envelopes. */
|
||||
export async function pollLegacyOspf<S extends { id: number; name: string }, T>(
|
||||
devices: readonly S[], read: (device: S) => Promise<T>,
|
||||
) {
|
||||
const results = await Promise.all(devices.map(async device => {
|
||||
try {
|
||||
return { ok: true as const, value: await read(device) }
|
||||
} catch (error) {
|
||||
return { ok: false as const, failure: {
|
||||
serverId: device.id, serverName: device.name,
|
||||
error: error instanceof Error ? error.message : "Ошибка опроса устройства",
|
||||
} }
|
||||
}
|
||||
}))
|
||||
return {
|
||||
values: results.flatMap(result => result.ok ? [result.value] : []),
|
||||
failures: results.flatMap(result => result.ok ? [] : [result.failure]),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user