fix(api): показывать ошибки строгого OSPF-каталога
This commit is contained in:
1 parent
72a63b4a4f
commit
a89cefcbf2
6 files changed
+92
-4
No files matched your search
@@ -22,7 +22,7 @@
|
||||
"test:config-sync": "tsx src/services/config-apply-plan.test.ts && tsx src/services/entity-snapshots.test.ts",
|
||||
"test:ros-file": "tsx src/services/ros-file-contents.test.ts",
|
||||
"test:backups": "tsx src/services/s3-backup-client.test.ts",
|
||||
"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",
|
||||
"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",
|
||||
"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:config-sync && npm run test:ros-file",
|
||||
"test:geoip": "tsx src/services/traffic-flow-geoip.test.ts"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
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, pollOspfCatalog, readOspfCatalogRaw, type OspfCatalogCollections } from "../services/ospf-catalog-poll.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
const devices = [{ id: 1, name: "Первый" }, { id: 2, name: "Второй" }]
|
||||
const row = { id: "*1", serverId: 1 }
|
||||
const collections = { ...emptyOspfCatalog(), neighbors: [row] } as OspfCatalogCollections
|
||||
const read = async (device: { id: number }) => { if (device.id === 2) throw new Error("Ошибка BFD тест"); return collections }
|
||||
const mixed = await pollOspfCatalog(devices, read)
|
||||
assert.deepEqual(mixed, { ...collections, failures: [{ serverId: 2, serverName: "Второй", error: "Ошибка BFD тест" }] })
|
||||
assert.deepEqual(await pollOspfCatalog(devices, async () => emptyOspfCatalog()), { ...emptyOspfCatalog(), failures: [] })
|
||||
assert.equal((await pollOspfCatalog(devices, async () => { throw new Error("Отказ") })).failures.length, 2)
|
||||
for (const rows of [[{ ...row, serverId: 2 }], [row, row], [{ ...row, id: " " }]]) {
|
||||
const result = await pollOspfCatalog([devices[0]], async () => ({ ...emptyOspfCatalog(), routes: rows } as OspfCatalogCollections))
|
||||
assert.equal(result.routes.length, 0)
|
||||
assert.equal(result.failures.length, 1)
|
||||
}
|
||||
const client = { getOspfNeighbors: async () => [], getOspfAreas: async () => [], getOspfInterfaceTemplates: async () => [], getOspfInstances: async () => [], getBfdSessions: async () => [], getIpRoutes: async () => [] }
|
||||
assert.equal((await readOspfCatalogRaw(client)).bfdSessions.length, 0)
|
||||
for (const section of ["getBfdSessions", "getIpRoutes"] as const) await assert.rejects(() => readOspfCatalogRaw({ ...client, [section]: async () => { throw new Error("Отказ " + section) } }), /Отказ/)
|
||||
const app = Fastify()
|
||||
app.setValidatorCompiler(validatorCompiler)
|
||||
app.setSerializerCompiler(serializerCompiler)
|
||||
await app.register(createOspfRoutes({ listEnabledServers: async () => devices as typeof servers.$inferSelect[], readCatalog: read }), { prefix: "/api" })
|
||||
const response = await app.inject({ method: "GET", url: "/api/ospf/catalog" })
|
||||
assert.equal(response.statusCode, 200)
|
||||
assert.deepEqual(response.json(), mixed)
|
||||
await app.close()
|
||||
console.log("OSPF catalog: partial/empty/failed/foreign/duplicate and strict BFD/routes, HTTP passed")
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from "../types/server.js"
|
||||
import { parseOspfGateway, parseOspfRouteType } from "../services/ospf-route-parse.js"
|
||||
import { z } from "zod"
|
||||
import { pollOspfCatalog, readOspfCatalogRaw, type OspfCatalogCollections } from "../services/ospf-catalog-poll.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
type TrafficSampleRow = typeof trafficSamples.$inferSelect
|
||||
@@ -544,7 +545,21 @@ async function buildOspfOptimizationPlan(server: ServerRow, pingWeight: number):
|
||||
|
||||
// ── route plugin ──────────────────────────────────────────────────────────────
|
||||
|
||||
const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
interface OspfCatalogDependencies {
|
||||
listEnabledServers: () => Promise<ServerRow[]>
|
||||
readCatalog: (server: ServerRow) => Promise<OspfCatalogCollections>
|
||||
}
|
||||
const catalogDefaults: OspfCatalogDependencies = {
|
||||
listEnabledServers: () => db.select().from(servers).where(eq(servers.enabled, true)),
|
||||
readCatalog: async server => {
|
||||
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await readOspfCatalogRaw(MikrotikClient.fromServer(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) }
|
||||
},
|
||||
}
|
||||
export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalogDefaults): FastifyPluginAsyncZod {
|
||||
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
|
||||
app.get("/ospf/neighbors", async (_req, reply) => {
|
||||
@@ -748,4 +763,6 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default ospfRoutes
|
||||
}
|
||||
|
||||
export default createOspfRoutes()
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { MikrotikClient } from "./mikrotik.js"
|
||||
import type { OspfNeighborRead, OspfInterfaceRead, OspfInstanceRead, BfdSessionRead, OspfRouteRead } from "../types/server.js"
|
||||
export interface OspfCatalogCollections {
|
||||
neighbors: OspfNeighborRead[]; interfaces: OspfInterfaceRead[]; instances: OspfInstanceRead[];
|
||||
bfdSessions: BfdSessionRead[]; routes: OspfRouteRead[];
|
||||
}
|
||||
export const emptyOspfCatalog = (): OspfCatalogCollections => ({ neighbors: [], interfaces: [], instances: [], bfdSessions: [], routes: [] })
|
||||
export async function readOspfCatalogRaw(client: Pick<MikrotikClient, "getOspfNeighbors" | "getOspfAreas" | "getOspfInterfaceTemplates" | "getOspfInstances" | "getBfdSessions" | "getIpRoutes">) {
|
||||
const [neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes] = await Promise.all([
|
||||
client.getOspfNeighbors(), client.getOspfAreas(), client.getOspfInterfaceTemplates(), client.getOspfInstances(), client.getBfdSessions(), client.getIpRoutes(),
|
||||
])
|
||||
if ([neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes].some(rows => !Array.isArray(rows))) throw new Error("Некорректный ответ RouterOS OSPF")
|
||||
return { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes }
|
||||
}
|
||||
export async function pollOspfCatalog<S extends { id: number; name: string }>(devices: readonly S[], read: (device: S) => Promise<OspfCatalogCollections>) {
|
||||
const results = await Promise.all(devices.map(async device => {
|
||||
try {
|
||||
const collections = await read(device)
|
||||
for (const rows of Object.values(collections)) {
|
||||
if (!Array.isArray(rows) || rows.some(row => row.serverId !== device.id || typeof row.id !== "string" || !row.id.trim())) throw new Error("Некорректный контекст OSPF-каталога")
|
||||
if (new Set(rows.map(row => row.id)).size !== rows.length) throw new Error("Повторяющиеся ID OSPF-каталога")
|
||||
}
|
||||
return { ...collections, failures: [] }
|
||||
} catch (error) {
|
||||
return { ...emptyOspfCatalog(), failures: [{ serverId: device.id, serverName: device.name, error: error instanceof Error ? error.message : "Ошибка опроса OSPF" }] }
|
||||
}
|
||||
}))
|
||||
return { neighbors: results.flatMap(r => r.neighbors), interfaces: results.flatMap(r => r.interfaces), instances: results.flatMap(r => r.instances), bfdSessions: results.flatMap(r => r.bfdSessions), routes: results.flatMap(r => r.routes), failures: results.flatMap(r => r.failures) }
|
||||
}
|
||||
@@ -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.8, локальная приёмка BGP; далее OSPF |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.9, строгий каталог OSPF; далее экран |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
## Блок 10.9 — строгий каталог OSPF (2026-09-30)
|
||||
|
||||
- Добавлен /ospf/catalog с пятью списками и failures по устройствам. Строгий reader не скрывает отказ BFD или /ip/route за []; успешный пустой ответ остаётся пустым. Чужой serverId, пустые/повторяющиеся ID в разделе отклоняются.
|
||||
- Снимок устройства консервативно атомарен: отказ любой зависимости делает весь его набор неподтверждённым, исправные устройства возвращаются. Это не обещает независимую доступность BFD/core при частичном сбое одного роутера. Legacy /ospf/all и старый reader сохранены до миграции consumers; правила optimize/apply не менялись.
|
||||
- Unit проверил partial/empty/all-failed/context/duplicates и реальные ветки строгого reader при отказе BFD/routes; Fastify inject с production Zod проверил HTTP каталог. Backend build прошёл. Lint без ошибок; два исходных warning в ospf.ts (and, measurePingByInterface) сохранены.
|
||||
- Далее — потребитель /ospf/catalog: валидация/inventory, кэш, неизвестные статусы/сводка, readiness оптимизации и source. Квота: 80% окна использовано; продолжаем по прямому поручению пользователя. Локальный коммит без push.
|
||||
|
||||
## Блок 10.8 — визуальная и профильная приёмка BGP (2026-09-30)
|
||||
|
||||
- Два viewport 390×844/1440×1000, клавиатура и отсутствие общего переполнения проверены. Раскрытие сессии имеет явную кнопку с доступным именем/aria-expanded, Enter обновляет session URL. Выбранная сессия, скрытая фильтрами, имеет отдельное сообщение.
|
||||
|
||||
@@ -72,3 +72,7 @@ server/session/tab и возврат/reload сохраняют контекст;
|
||||
## Блок 10.8 — BGP приёмка
|
||||
|
||||
27 E2E, unit/TypeScript/lint, frontend/backend build прошли. Шесть PNG просмотрены, мобильные подписи/детали исправлены. Далее — строгий каталог OSPF (BFD/routes сейчас скрывают свои ошибки). Внешние границы — журнал.
|
||||
|
||||
## Блок 10.9 — OSPF каталог
|
||||
|
||||
Строгий /ospf/catalog возвращает failures устройства; BFD/routes ошибки не скрываются. Снимок устройства атомарен, legacy /all и правила оптимизации сохранены. Unit/HTTP/backend build прошли; два исходных lint warning. Далее — экран OSPF.
|
||||
Reference in new issue
Block a user