160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
import { servers } from "../db/schema.js"
|
|
import type { BgpSessionRead, RosBgpSession } from "../types/server.js"
|
|
|
|
type ServerRow = typeof servers.$inferSelect
|
|
|
|
/** Parse RouterOS duration like "1m30s", "1d5h13m23s580ms", "30s" → seconds */
|
|
function parseDuration(s: string | undefined): number {
|
|
if (!s) return 0
|
|
let total = 0
|
|
const matches = s.matchAll(/(\d+)(w|d|h|m(?!s)|s|ms)/g)
|
|
for (const m of matches) {
|
|
const n = parseInt(m[1])
|
|
switch (m[2]) {
|
|
case "w":
|
|
total += n * 604800
|
|
break
|
|
case "d":
|
|
total += n * 86400
|
|
break
|
|
case "h":
|
|
total += n * 3600
|
|
break
|
|
case "m":
|
|
total += n * 60
|
|
break
|
|
case "s":
|
|
total += n
|
|
break
|
|
case "ms":
|
|
total += n / 1000
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return total
|
|
}
|
|
|
|
function parseCapabilities(capStr: string | undefined): string[] {
|
|
if (!capStr) return []
|
|
const codeMap: Record<string, string> = {
|
|
mp: "MP-BGP",
|
|
rr: "Route Refresh",
|
|
gr: "Graceful Restart",
|
|
as4: "4-byte-AS",
|
|
enhe: "Extended Next-Hop",
|
|
role: "BGP Role",
|
|
err: "Extended Route Refresh",
|
|
llgr: "Long-Lived GR",
|
|
"add-path": "ADD-PATH",
|
|
}
|
|
return capStr
|
|
.split(",")
|
|
.map((c) => c.trim().toLowerCase())
|
|
.filter(Boolean)
|
|
.map((c) => codeMap[c] ?? c)
|
|
.filter((v, i, a) => a.indexOf(v) === i)
|
|
}
|
|
|
|
function detectState(s: RosBgpSession): string {
|
|
if (s["established"] === "true") return "Established"
|
|
|
|
if (s["state"]) {
|
|
const map: Record<string, string> = {
|
|
established: "Established",
|
|
active: "Active",
|
|
idle: "Idle",
|
|
connect: "Connect",
|
|
opensent: "OpenSent",
|
|
openconfirm: "OpenConfirm",
|
|
"open sent": "OpenSent",
|
|
"open confirm": "OpenConfirm",
|
|
}
|
|
const key = s["state"].trim().toLowerCase()
|
|
if (map[key]) return map[key]
|
|
return "Unknown"
|
|
}
|
|
|
|
return "Unknown"
|
|
}
|
|
|
|
/** Парсинг BGP-сессий с роутера — общая логика для HTTP-маршрута и alert-engine. */
|
|
export function parseBgpSessions(server: ServerRow, raw: RosBgpSession[]): BgpSessionRead[] {
|
|
return raw.map((s, idx) => {
|
|
const asn = (value: string | undefined) => {
|
|
const n = readCounter(value)
|
|
return n !== null && n > 0 && n <= 4294967295 ? n : null
|
|
}
|
|
const remoteAs = asn(s["remote.as"]), localAs = asn(s["local.as"])
|
|
const type = remoteAs === null || localAs === null ? "Unknown" : localAs === remoteAs ? "iBGP" : "eBGP"
|
|
|
|
const localCaps = parseCapabilities(s["local.capabilities"])
|
|
const remoteCaps = parseCapabilities(s["remote.capabilities"])
|
|
const legacyCaps: string[] = []
|
|
if (s["4-octet-as-capability"] === "true") legacyCaps.push("4-byte-AS")
|
|
if (s["as4-capability"] === "true") legacyCaps.push("4-byte-AS")
|
|
if (s["refresh-capability"] === "true") legacyCaps.push("Route Refresh")
|
|
if (s["add-path-capability"] === "true") legacyCaps.push("ADD-PATH")
|
|
if (s["graceful-restart-capability"] === "true") legacyCaps.push("Graceful Restart")
|
|
if (s["extended-message-capability"] === "true") legacyCaps.push("Extended Messages")
|
|
const caps = [...new Set([...localCaps, ...remoteCaps, ...legacyCaps])]
|
|
|
|
const holdTime = readTimer(s["hold-time"]) ?? readCounter(s["active-holdtime"])
|
|
const keepalive = readTimer(s["keepalive-time"])
|
|
|
|
const inputMessages =
|
|
readCounter(s["remote.messages"] ?? s["total-messages-received"])
|
|
const outputMessages = readCounter(s["local.messages"] ?? s["total-messages-sent"])
|
|
|
|
const peerIp = (s["remote.address"] ?? "").replace(/\/\d+$/, "")
|
|
|
|
return {
|
|
id: s[".id"] ?? String(idx),
|
|
serverId: server.id,
|
|
serverName: server.name || server.host,
|
|
serverSite: server.site,
|
|
serverCountry: server.country,
|
|
name: s["name"] ?? peerIp,
|
|
peerIp,
|
|
remoteAs,
|
|
localAs,
|
|
localId: s["local.id"] ?? "",
|
|
remoteId: s["remote.id"] ?? "",
|
|
state: detectState(s),
|
|
type,
|
|
uptime: s["uptime"] || null,
|
|
holdTime,
|
|
keepalive,
|
|
prefixesRx: readCounter(s["prefix-count"]),
|
|
prefixesTx: null,
|
|
inputMessages,
|
|
outputMessages,
|
|
capabilities: caps,
|
|
lastError: s["last-notification"] || null,
|
|
}
|
|
})
|
|
}
|
|
|
|
/** Каталог не заменяет отсутствующие показатели значениями по умолчанию старого API. */
|
|
export type BgpCatalogSessionRead = BgpSessionRead & { afi: string | null }
|
|
function readCounter(value: string | undefined): number | null {
|
|
if (value === undefined || !/^\d+$/.test(value.trim())) return null
|
|
const parsed = Number(value)
|
|
return Number.isSafeInteger(parsed) ? parsed : null
|
|
}
|
|
function readTimer(value: string | undefined): number | null {
|
|
if (value === "0" || value === "infinity") return 0
|
|
if (!value || !/^(?:\d+(?:w|d|h|ms|m|s))+$/.test(value)) return null
|
|
const seconds = parseDuration(value)
|
|
return Number.isFinite(seconds) && seconds >= 0 ? seconds : null
|
|
}
|
|
export function parseBgpCatalogSessions(server: ServerRow, raw: RosBgpSession[]): BgpCatalogSessionRead[] {
|
|
return parseBgpSessions(server, raw).map((session, index) => {
|
|
const s = raw[index]
|
|
return { ...session,
|
|
afi: s["remote.afi"]?.trim() || null,
|
|
}
|
|
})
|
|
}
|