fix(api): сохранять неизвестные показатели BFD и OSPF
This commit is contained in:
1 parent
47dcd89b3c
commit
e3791f3496
13 files changed
+136
-44
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import assert from "node:assert/strict"
|
||||
import Fastify from "fastify"
|
||||
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
|
||||
import { createOspfRoutes, parseBfdSessions } from "./ospf.js"
|
||||
import { createOspfRoutes, parseBfdSessions, parseBfdCatalogSessions, parseCatalogNeighbors } 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: "Второй" }]
|
||||
@@ -37,3 +37,15 @@ assert.equal(parseBfdSessions(bfdServer, rawBfd("down"), true)[0].state, "Down")
|
||||
assert.equal(parseBfdSessions(bfdServer, rawBfd("up"), true)[0].state, "Up")
|
||||
assert.equal(parseBfdSessions(bfdServer, rawBfd(), false)[0].state, "Down")
|
||||
console.log("OSPF strict BFD missing/blank/known states and legacy compatibility passed")
|
||||
|
||||
const missingBfd = parseBfdCatalogSessions(bfdServer, rawBfd("up"))[0]
|
||||
for (const field of ["multiplier", "txInterval", "rxInterval", "holdTime", "packetsRx", "packetsTx", "stateChanges"] as const) assert.equal(missingBfd[field], null)
|
||||
const zeroBfd = parseBfdCatalogSessions(bfdServer, [{ ...rawBfd("up")[0], multiplier: "0", "actual-tx-interval": "0ms", "required-min-rx": "1us", "hold-time": "1s200ms", "packets-rx": "0", "packets-tx": "9", "state-changes": "0" }])[0]
|
||||
assert.equal(zeroBfd.multiplier, 0); assert.equal(zeroBfd.txInterval, 0); assert.equal(zeroBfd.rxInterval, 0.001); assert.equal(zeroBfd.holdTime, 1200); assert.equal(zeroBfd.packetsRx, 0); assert.equal(zeroBfd.packetsTx, 9); assert.equal(zeroBfd.stateChanges, 0)
|
||||
assert.equal(parseBfdCatalogSessions(bfdServer, [{ ...rawBfd("up")[0], "desired-tx-interval": "200ms" }])[0].txInterval, null)
|
||||
const neighborSource = { ".id": "*1", address: "192.0.2.2", "router-id": "192.0.2.2", instance: "default", area: "backbone", interface: "ether1", state: "full" }
|
||||
assert.equal(parseCatalogNeighbors(bfdServer, [neighborSource], new Map())[0].priority, null)
|
||||
const neighborZero = parseCatalogNeighbors(bfdServer, [{ ...neighborSource, priority: "0", "state-changes": "0" }], new Map())[0]
|
||||
assert.equal(neighborZero.priority, 0); assert.equal(neighborZero.stateChanges, 0)
|
||||
for (const priority of ["junk", "1junk", "256", "-1"]) assert.equal(parseCatalogNeighbors(bfdServer, [{ ...neighborSource, priority }], new Map())[0].priority, null)
|
||||
console.log("OSPF strict values: missing/zero/microseconds/compound, no desired-as-actual and priority bounds 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 { observedInteger, observedDurationMs } from "../services/ospf-observed-values.js"
|
||||
import { pollOspfCatalog, readOspfCatalogRaw, type OspfCatalogCollections } from "../services/ospf-catalog-poll.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
@@ -119,6 +120,16 @@ export function parseBfdSessions(server: ServerRow, raw: RosBfdSession[], strict
|
||||
})
|
||||
}
|
||||
|
||||
export function parseBfdCatalogSessions(server: ServerRow, raw: RosBfdSession[]) {
|
||||
return parseBfdSessions(server, raw, true).map((row, index) => {
|
||||
const source = raw[index]
|
||||
return { ...row, multiplier: observedInteger(source.multiplier, 0, 255), txInterval: observedDurationMs(source["actual-tx-interval"]), rxInterval: observedDurationMs(source["required-min-rx"]), holdTime: observedDurationMs(source["hold-time"]), packetsRx: observedInteger(source["packets-rx"]), packetsTx: observedInteger(source["packets-tx"]), stateChanges: observedInteger(source["state-changes"]) }
|
||||
})
|
||||
}
|
||||
export function parseCatalogNeighbors(server: ServerRow, raw: RosOspfNeighbor[], areaMap: Map<string, string>) {
|
||||
return parseNeighbors(server, raw, areaMap).map((row, index) => ({ ...row, priority: observedInteger(raw[index].priority, 0, 255), stateChanges: observedInteger(raw[index]["state-changes"]) }))
|
||||
}
|
||||
|
||||
// ── parsers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function parseNeighbors(
|
||||
@@ -554,7 +565,7 @@ const catalogDefaults: OspfCatalogDependencies = {
|
||||
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, true), routes: parseOspfRoutes(server, ipRoutes) }
|
||||
return { neighbors: parseCatalogNeighbors(server, neighbors, areaMap), interfaces: parseInterfaces(server, ifaceTemplates, areas, instances, areaMap), instances: parseInstances(server, instances), bfdSessions: parseBfdCatalogSessions(server, bfdSessions), routes: parseOspfRoutes(server, ipRoutes) }
|
||||
},
|
||||
}
|
||||
export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalogDefaults): FastifyPluginAsyncZod {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { MikrotikClient } from "./mikrotik.js"
|
||||
import type { OspfNeighborRead, OspfInterfaceRead, OspfInstanceRead, BfdSessionRead, OspfRouteRead } from "../types/server.js"
|
||||
export type NullableFields<T, K extends keyof T> = Omit<T, K> & { [P in K]: T[P] | null }
|
||||
export type CatalogNeighbor = NullableFields<OspfNeighborRead, "priority" | "stateChanges">
|
||||
export type CatalogBfdSession = NullableFields<BfdSessionRead, "multiplier" | "txInterval" | "rxInterval" | "holdTime" | "packetsRx" | "packetsTx" | "stateChanges">
|
||||
export interface OspfCatalogCollections {
|
||||
neighbors: OspfNeighborRead[]; interfaces: OspfInterfaceRead[]; instances: OspfInstanceRead[];
|
||||
bfdSessions: BfdSessionRead[]; routes: OspfRouteRead[];
|
||||
neighbors: CatalogNeighbor[]; interfaces: OspfInterfaceRead[]; instances: OspfInstanceRead[];
|
||||
bfdSessions: CatalogBfdSession[]; routes: OspfRouteRead[];
|
||||
}
|
||||
export const emptyOspfCatalog = (): OspfCatalogCollections => ({ neighbors: [], interfaces: [], instances: [], bfdSessions: [], routes: [] })
|
||||
export async function readOspfCatalogRaw(client: Pick<MikrotikClient, "getOspfNeighbors" | "getOspfAreas" | "getOspfInterfaceTemplates" | "getOspfInstances" | "getBfdSessions" | "getIpRoutes">) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { observedInteger, observedDurationMs } from "./ospf-observed-values.js"
|
||||
for (const raw of [undefined, "", " ", "1x", "-1", "1.5", "9007199254740992"]) assert.equal(observedInteger(raw), null)
|
||||
assert.equal(observedInteger("0"), 0); assert.equal(observedInteger("255", 0, 255), 255); assert.equal(observedInteger("256", 0, 255), null)
|
||||
for (const raw of [undefined, "", "-1s", "12junk", "200", "1s2s", "60:00", "00:99:00", "1s junk"]) assert.equal(observedDurationMs(raw), null)
|
||||
for (const [raw, ms] of [["0ms", 0], ["0", 0], ["1us", 0.001], ["1s200ms", 1200], ["1h2m3s", 3723000], ["01:02:03.5", 3723500], ["02:03", 123000]] as const) assert.equal(observedDurationMs(raw), ms)
|
||||
console.log("OSPF observed values: absent/zero/invalid/unsafe/units/clock passed")
|
||||
@@ -0,0 +1,28 @@
|
||||
/** Values read from RouterOS; absence is not a measured zero. */
|
||||
export function observedInteger(value: string | undefined, min = 0, max = Number.MAX_SAFE_INTEGER): number | null {
|
||||
if (value === undefined || !/^\d+$/.test(value.trim())) return null
|
||||
const number = Number(value.trim())
|
||||
return Number.isSafeInteger(number) && number >= min && number <= max ? number : null
|
||||
}
|
||||
/** RouterOS time in milliseconds. Bare numbers are ambiguous, except explicit zero. */
|
||||
export function observedDurationMs(value: string | undefined): number | null {
|
||||
const text = value?.trim()
|
||||
if (!text) return null
|
||||
if (text === "0") return 0
|
||||
const clock = /^(?:(\d+):)?(\d{1,2}):(\d{2})(\.\d+)?$/.exec(text)
|
||||
if (clock) {
|
||||
const [, hours = "0", minutes, seconds, fraction = ""] = clock
|
||||
if (Number(minutes) >= 60 || Number(seconds) >= 60) return null
|
||||
const ms = (Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds + fraction)) * 1000
|
||||
return Number.isFinite(ms) && ms <= Number.MAX_SAFE_INTEGER ? ms : null
|
||||
}
|
||||
const units: Record<string, number> = { w: 604800000, d: 86400000, h: 3600000, m: 60000, s: 1000, ms: 1, us: 0.001, ns: 0.000001 }
|
||||
const pattern = /(\d+(?:\.\d+)?)(ms|us|ns|w|d|h|m|s)/g
|
||||
let cursor = 0, total = 0
|
||||
const seen = new Set<string>()
|
||||
for (const part of text.matchAll(pattern)) {
|
||||
if (part.index !== cursor || seen.has(part[2])) return null
|
||||
seen.add(part[2]); cursor += part[0].length; total += Number(part[1]) * units[part[2]]
|
||||
}
|
||||
return cursor === text.length && Number.isFinite(total) && total <= Number.MAX_SAFE_INTEGER ? total : null
|
||||
}
|
||||
Reference in new issue
Block a user