fix(api): показывать подтверждённые метрики OSPF
This commit is contained in:
1 parent
e3791f3496
commit
af05ad5cda
13 files changed
+120
-55
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, parseBfdCatalogSessions, parseCatalogNeighbors } from "./ospf.js"
|
||||
import { createOspfRoutes, parseBfdSessions, parseBfdCatalogSessions, parseCatalogNeighbors, parseCatalogInterfaces, parseCatalogInstances, parseCatalogRoutes } 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: "Второй" }]
|
||||
@@ -49,3 +49,20 @@ const neighborZero = parseCatalogNeighbors(bfdServer, [{ ...neighborSource, prio
|
||||
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")
|
||||
|
||||
const rawIface = { ".id": "*1", area: "backbone", interfaces: "ether1", disabled: "false", inactive: "false" }
|
||||
const missingIface = parseCatalogInterfaces(bfdServer, [rawIface], [], [], new Map())[0]
|
||||
for (const field of ["cost", "priority", "type", "helloInterval", "deadInterval", "useBfd"] as const) assert.equal(missingIface[field], null)
|
||||
const zeroIface = parseCatalogInterfaces(bfdServer, [{ ...rawIface, cost: "0", priority: "0", type: "ptp", "hello-interval": "0s", "dead-interval": "1s500ms", "use-bfd": "no" }], [], [], new Map())[0]
|
||||
assert.equal(zeroIface.cost, 0); assert.equal(zeroIface.priority, 0); assert.equal(zeroIface.helloInterval, 0); assert.equal(zeroIface.deadInterval, 1.5); assert.equal(zeroIface.useBfd, false)
|
||||
assert.equal(parseCatalogInterfaces(bfdServer, [{ ...rawIface, disabled: "junk", cost: "10x" }], [], [], new Map())[0].disabled, null)
|
||||
const rawInstance = { ".id": "*1", name: "default", disabled: "false", inactive: "false" }
|
||||
const missingInstance = parseCatalogInstances(bfdServer, [rawInstance])[0]
|
||||
assert.equal(missingInstance.version, null); assert.equal(missingInstance.routerId, null)
|
||||
assert.equal(parseCatalogInstances(bfdServer, [{ ...rawInstance, version: "3", "router-id": "main" }])[0].version, 3)
|
||||
const rawRoute = { ".id": "*1", "dst-address": "192.0.2.0/24", ospf: "true", distance: "110" }
|
||||
assert.equal(parseCatalogRoutes(bfdServer, [rawRoute])[0].cost, null)
|
||||
assert.equal(parseCatalogRoutes(bfdServer, [{ ...rawRoute, "ospf-metric": "0" }])[0].cost, 0)
|
||||
assert.equal(parseCatalogRoutes(bfdServer, [{ ...rawRoute, "ospf-metric": "42" }])[0].cost, 42)
|
||||
assert.equal(parseCatalogRoutes(bfdServer, [{ ...rawRoute, "ospf-metric": "42x" }])[0].cost, null)
|
||||
console.log("OSPF strict interfaces/instances/routes: absent/zero/fractional/flags, no distance-as-cost passed")
|
||||
@@ -11,7 +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 { observedInteger, observedDurationMs, observedBoolean } from "../services/ospf-observed-values.js"
|
||||
import { pollOspfCatalog, readOspfCatalogRaw, type OspfCatalogCollections } from "../services/ospf-catalog-poll.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
@@ -195,6 +195,23 @@ function parseInterfaces(
|
||||
})
|
||||
}
|
||||
|
||||
export function parseCatalogInterfaces(server: ServerRow, raw: RosOspfInterfaceTemplate[], areas: RosOspfArea[], instances: RosOspfInstance[], areaMap: Map<string, string>) {
|
||||
return parseInterfaces(server, raw, areas, instances, areaMap).map((row, index) => {
|
||||
const source = raw[index]
|
||||
const helloMs = observedDurationMs(source["hello-interval"]), deadMs = observedDurationMs(source["dead-interval"])
|
||||
return { ...row, cost: observedInteger(source.cost, 0, 65535), priority: observedInteger(source.priority, 0, 255), type: source.type?.trim() || null, disabled: observedBoolean(source.disabled), inactive: observedBoolean(source.inactive), useBfd: observedBoolean(source["use-bfd"]), helloInterval: helloMs === null ? null : helloMs / 1000, deadInterval: deadMs === null ? null : deadMs / 1000 }
|
||||
})
|
||||
}
|
||||
export function parseCatalogInstances(server: ServerRow, raw: RosOspfInstance[]) {
|
||||
return parseInstances(server, raw).map((row, index) => ({ ...row, version: ["2", "3"].includes(raw[index].version) ? Number(raw[index].version) : null, routerId: raw[index]["router-id"]?.trim() || null, disabled: observedBoolean(raw[index].disabled), inactive: observedBoolean(raw[index].inactive) }))
|
||||
}
|
||||
export function parseCatalogRoutes(server: ServerRow, raw: RosIpRoute[]) {
|
||||
return raw.flatMap((source, index) => {
|
||||
const rows = parseOspfRoutes(server, [{ ...source, ".id": source[".id"] ?? String(index) }])
|
||||
return rows.map(row => ({ ...row, cost: observedInteger(source["ospf-metric"], 0, 4294967295) }))
|
||||
})
|
||||
}
|
||||
|
||||
function parseInstances(
|
||||
server: ServerRow,
|
||||
instances: RosOspfInstance[],
|
||||
@@ -565,7 +582,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: parseCatalogNeighbors(server, neighbors, areaMap), interfaces: parseInterfaces(server, ifaceTemplates, areas, instances, areaMap), instances: parseInstances(server, instances), bfdSessions: parseBfdCatalogSessions(server, bfdSessions), routes: parseOspfRoutes(server, ipRoutes) }
|
||||
return { neighbors: parseCatalogNeighbors(server, neighbors, areaMap), interfaces: parseCatalogInterfaces(server, ifaceTemplates, areas, instances, areaMap), instances: parseCatalogInstances(server, instances), bfdSessions: parseBfdCatalogSessions(server, bfdSessions), routes: parseCatalogRoutes(server, ipRoutes) }
|
||||
},
|
||||
}
|
||||
export function createOspfRoutes(dependencies: OspfCatalogDependencies = catalogDefaults): FastifyPluginAsyncZod {
|
||||
|
||||
@@ -3,9 +3,12 @@ import type { OspfNeighborRead, OspfInterfaceRead, OspfInstanceRead, BfdSessionR
|
||||
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 type CatalogInterface = NullableFields<OspfInterfaceRead, "cost" | "type" | "priority" | "helloInterval" | "deadInterval" | "disabled" | "inactive" | "useBfd">
|
||||
export type CatalogInstance = NullableFields<OspfInstanceRead, "version" | "routerId" | "disabled" | "inactive">
|
||||
export type CatalogRoute = NullableFields<OspfRouteRead, "cost">
|
||||
export interface OspfCatalogCollections {
|
||||
neighbors: CatalogNeighbor[]; interfaces: OspfInterfaceRead[]; instances: OspfInstanceRead[];
|
||||
bfdSessions: CatalogBfdSession[]; routes: OspfRouteRead[];
|
||||
neighbors: CatalogNeighbor[]; interfaces: CatalogInterface[]; instances: CatalogInstance[];
|
||||
bfdSessions: CatalogBfdSession[]; routes: CatalogRoute[];
|
||||
}
|
||||
export const emptyOspfCatalog = (): OspfCatalogCollections => ({ neighbors: [], interfaces: [], instances: [], bfdSessions: [], routes: [] })
|
||||
export async function readOspfCatalogRaw(client: Pick<MikrotikClient, "getOspfNeighbors" | "getOspfAreas" | "getOspfInterfaceTemplates" | "getOspfInstances" | "getBfdSessions" | "getIpRoutes">) {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { observedInteger, observedDurationMs } from "./ospf-observed-values.js"
|
||||
import { observedInteger, observedDurationMs, observedBoolean } 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")
|
||||
|
||||
for (const raw of [undefined, "", "junk"]) assert.equal(observedBoolean(raw), null)
|
||||
for (const raw of ["true", "yes"]) assert.equal(observedBoolean(raw), true)
|
||||
for (const raw of ["false", "no"]) assert.equal(observedBoolean(raw), false)
|
||||
@@ -26,3 +26,9 @@ export function observedDurationMs(value: string | undefined): number | null {
|
||||
}
|
||||
return cursor === text.length && Number.isFinite(total) && total <= Number.MAX_SAFE_INTEGER ? total : null
|
||||
}
|
||||
|
||||
export function observedBoolean(value: string | undefined): boolean | null {
|
||||
if (value === "true" || value === "yes") return true
|
||||
if (value === "false" || value === "no") return false
|
||||
return null
|
||||
}
|
||||
Reference in new issue
Block a user