Files
MikrotikManager/backend/src/services/bgp-parse-sessions.ts
T

172 lines
6.4 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
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]
for (const [k, v] of Object.entries(map)) {
if (key.startsWith(k)) return v
}
return key.charAt(0).toUpperCase() + key.slice(1)
}
if (s["uptime"]) return "Active"
return "Idle"
}
/** Парсинг BGP-сессий с роутера — общая логика для HTTP-маршрута и alert-engine. */
export function parseBgpSessions(server: ServerRow, raw: RosBgpSession[]): BgpSessionRead[] {
return raw.map((s, idx) => {
const remoteAs = parseInt(s["remote.as"] ?? "0") || 0
const localAs = parseInt(s["local.as"] ?? "0") || 0
const type: "eBGP" | "iBGP" = localAs > 0 && 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 = parseDuration(s["hold-time"]) || parseInt(s["active-holdtime"] ?? "0") || 90
const keepalive = parseDuration(s["keepalive-time"]) || 30
const inputMessages =
parseInt(s["remote.messages"] ?? s["total-messages-received"] ?? "0") || 0
const outputMessages = parseInt(s["local.messages"] ?? s["total-messages-sent"] ?? "0") || 0
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: parseInt(s["prefix-count"] ?? s["total-updates-received"] ?? "0") || 0,
prefixesTx: parseInt(s["total-updates-sent"] ?? "0") || 0,
inputMessages,
outputMessages,
capabilities: caps,
lastError: s["last-notification"] || null,
}
})
}
/** Каталог не заменяет отсутствующие показатели значениями по умолчанию старого API. */
export type BgpCatalogSessionRead = Omit<BgpSessionRead, "holdTime" | "keepalive" | "prefixesRx" | "prefixesTx" | "inputMessages" | "outputMessages" | "remoteAs" | "localAs" | "type"> & {
holdTime: number | null; keepalive: number | null; prefixesRx: number | null; prefixesTx: number | null;
inputMessages: number | null; outputMessages: number | null; afi: string | null;
remoteAs: number | null; localAs: number | null; type: "eBGP" | "iBGP" | "Unknown";
}
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
return parseDuration(value)
}
export function parseBgpCatalogSessions(server: ServerRow, raw: RosBgpSession[]): BgpCatalogSessionRead[] {
return parseBgpSessions(server, raw).map((session, index) => {
const s = raw[index]
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"])
return { ...session,
remoteAs, localAs, type: remoteAs === null || localAs === null ? "Unknown" : remoteAs === localAs ? "iBGP" : "eBGP",
state: s.established === "true" ? "Established" : s.state?.trim() ? detectState(s) : "Unknown",
holdTime: readTimer(s["hold-time"]) ?? readCounter(s["active-holdtime"]),
keepalive: readTimer(s["keepalive-time"]),
prefixesRx: readCounter(s["prefix-count"]), prefixesTx: null,
inputMessages: readCounter(s["remote.messages"] ?? s["total-messages-received"]),
outputMessages: readCounter(s["local.messages"] ?? s["total-messages-sent"]),
afi: s["remote.afi"]?.trim() || null,
}
})
}