feat(bgp, vxlan, ospf): enhance server data handling and introduce new routes
Docker images / prepare-release (push) Successful in 7s
Docker images / backend-test (push) Successful in 1m40s
Docker images / frontend-image (push) Successful in 2m50s
Docker images / updater-image (push) Successful in 42s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 10s
Docker images / prepare-release (push) Successful in 7s
Docker images / backend-test (push) Successful in 1m40s
Docker images / frontend-image (push) Successful in 2m50s
Docker images / updater-image (push) Successful in 42s
Docker images / backend-image (push) Successful in 2m40s
Docker images / notify-webhook (push) Skipped
Docker images / publish-release (push) Successful in 10s
- Added support for fetching and displaying server data in BGP, VXLAN, and OSPF pages, improving the overall user experience. - Introduced new backend routes for OSPF and VXLAN, allowing for better data management and retrieval. - Implemented mapping functions for backend server data to frontend types, ensuring consistency across components. - Enhanced the sidebar to display counts for BGP sessions, VXLAN tunnels, and containers, providing users with quick insights into their network status. - Updated tests to cover new functionalities and ensure reliability. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
5750590b68
commit
9c0ee7940e
23 files changed
+1286
-177
No files matched your search
@@ -0,0 +1,58 @@
|
||||
import { z } from "zod"
|
||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import {
|
||||
getEnabledServerById,
|
||||
listContainers,
|
||||
listContainersForServer,
|
||||
removeContainer,
|
||||
restartContainer,
|
||||
startContainer,
|
||||
stopContainer,
|
||||
} from "../services/containers-live.js"
|
||||
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
|
||||
|
||||
const RosIdBodySchema = z.object({
|
||||
rosId: z.string().min(1),
|
||||
})
|
||||
|
||||
type RosIdBody = z.infer<typeof RosIdBodySchema>
|
||||
type MutateFn = typeof startContainer
|
||||
|
||||
const containersRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.get("/containers", async (_req, reply) => {
|
||||
const containers = await listContainers()
|
||||
return reply.send({ containers })
|
||||
})
|
||||
|
||||
app.get("/servers/:id/containers", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = await getEnabledServerById(params.id)
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
const containers = await listContainersForServer(server)
|
||||
return reply.send({ containers })
|
||||
})
|
||||
|
||||
function registerMutate(path: string, fn: MutateFn) {
|
||||
app.post(path, { schema: { params: ServerIdParamSchema, body: RosIdBodySchema } }, async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const body = req.body as RosIdBody
|
||||
const server = await getEnabledServerById(params.id)
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
try {
|
||||
await fn(server, body.rosId)
|
||||
return reply.send({ ok: true })
|
||||
} catch (err) {
|
||||
return reply.status(502).send({
|
||||
error: err instanceof Error ? err.message : "Ошибка RouterOS",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
registerMutate("/servers/:id/containers/start", startContainer)
|
||||
registerMutate("/servers/:id/containers/stop", stopContainer)
|
||||
registerMutate("/servers/:id/containers/restart", restartContainer)
|
||||
registerMutate("/servers/:id/containers/remove", removeContainer)
|
||||
}
|
||||
|
||||
export default containersRoutes
|
||||
@@ -6,9 +6,10 @@ import { MikrotikClient } from "../services/mikrotik.js"
|
||||
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
|
||||
import type {
|
||||
RosOspfNeighbor, RosOspfArea, RosOspfInterfaceTemplate, RosOspfInstance,
|
||||
RosBfdSession,
|
||||
OspfNeighborRead, OspfInterfaceRead, OspfInstanceRead, BfdSessionRead,
|
||||
RosBfdSession, RosIpRoute,
|
||||
OspfNeighborRead, OspfInterfaceRead, OspfInstanceRead, OspfRouteRead, BfdSessionRead,
|
||||
} from "../types/server.js"
|
||||
import { parseOspfGateway, parseOspfRouteType } from "../services/ospf-route-parse.js"
|
||||
import { z } from "zod"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
@@ -74,14 +75,15 @@ function parseAddrIface(addr: string): { ip: string; iface: string } {
|
||||
/** Fetch all OSPF + BFD data for one server */
|
||||
async function fetchServerOspf(server: ServerRow) {
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
const [neighbors, areas, ifaceTemplates, instances, bfdSessions] = await Promise.all([
|
||||
const [neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes] = await Promise.all([
|
||||
client.getOspfNeighbors(),
|
||||
client.getOspfAreas(),
|
||||
client.getOspfInterfaceTemplates(),
|
||||
client.getOspfInstances(),
|
||||
client.getBfdSessions().catch(() => [] as RosBfdSession[]), // BFD is optional
|
||||
client.getIpRoutes().catch(() => [] as RosIpRoute[]),
|
||||
])
|
||||
return { neighbors, areas, ifaceTemplates, instances, bfdSessions }
|
||||
return { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes }
|
||||
}
|
||||
|
||||
// ── BFD parser ────────────────────────────────────────────────────────────────
|
||||
@@ -200,6 +202,29 @@ function parseInstances(
|
||||
}))
|
||||
}
|
||||
|
||||
function parseOspfRoutes(server: ServerRow, routes: RosIpRoute[]): OspfRouteRead[] {
|
||||
const out: OspfRouteRead[] = []
|
||||
for (const [idx, r] of routes.entries()) {
|
||||
const type = parseOspfRouteType(r)
|
||||
if (!type) continue
|
||||
const { nextHop, via } = parseOspfGateway(r)
|
||||
const metric = parseInt(r["ospf-metric"] ?? r.distance ?? "0") || 0
|
||||
out.push({
|
||||
id: r[".id"] ?? String(idx),
|
||||
serverId: server.id,
|
||||
serverName: server.name || server.host,
|
||||
serverSite: server.site,
|
||||
destination: r["dst-address"] ?? "",
|
||||
type,
|
||||
cost: metric,
|
||||
nextHop,
|
||||
via,
|
||||
area: r["ospf-area"] ?? "",
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function calcRouteScore(pingMs: number, dlMbps: number, ulMbps: number, pingWeight: number) {
|
||||
const pingScore = Math.max(0, 100 - pingMs * 0.6)
|
||||
const speedScore = Math.min(100, (dlMbps + ulMbps) / 18)
|
||||
@@ -584,16 +609,17 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
const perServer = await Promise.all(
|
||||
allServers.map(async (server) => {
|
||||
try {
|
||||
const { neighbors, areas, ifaceTemplates, instances, bfdSessions } = await fetchServerOspf(server)
|
||||
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await fetchServerOspf(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),
|
||||
}
|
||||
} catch {
|
||||
return { neighbors: [], interfaces: [], instances: [], bfdSessions: [] }
|
||||
return { neighbors: [], interfaces: [], instances: [], bfdSessions: [], routes: [] }
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -603,6 +629,7 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
interfaces: perServer.flatMap(r => r.interfaces),
|
||||
instances: perServer.flatMap(r => r.instances),
|
||||
bfdSessions: perServer.flatMap(r => r.bfdSessions),
|
||||
routes: perServer.flatMap(r => r.routes),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -634,13 +661,14 @@ const ospfRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
|
||||
try {
|
||||
const { neighbors, areas, ifaceTemplates, instances, bfdSessions } = await fetchServerOspf(server)
|
||||
const { neighbors, areas, ifaceTemplates, instances, bfdSessions, ipRoutes } = await fetchServerOspf(server)
|
||||
const areaMap = buildAreaMap(areas)
|
||||
return reply.send({
|
||||
neighbors: parseNeighbors(server, neighbors, areaMap),
|
||||
interfaces: parseInterfaces(server, ifaceTemplates, areas, instances, areaMap),
|
||||
instances: parseInstances(server, instances),
|
||||
bfdSessions: parseBfdSessions(server, bfdSessions),
|
||||
routes: parseOspfRoutes(server, ipRoutes),
|
||||
areas: areas.map(a => ({ name: a.name, areaId: a["area-id"] ?? "0.0.0.0", type: a.type, disabled: a.disabled === "true", inactive: a.inactive === "true", instance: a.instance })),
|
||||
})
|
||||
} catch (err) {
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import { count } from "drizzle-orm"
|
||||
import { listCertificatesFromServers } from "../services/certificates-service.js"
|
||||
import { countWireGuardInterfaces } from "../services/wireguard-live.js"
|
||||
import { countVxlanTunnels } from "../services/vxlan-live.js"
|
||||
import { countContainers } from "../services/containers-live.js"
|
||||
import { countBgpSessions } from "../services/bgp-peers-live.js"
|
||||
import { db } from "../db/index.js"
|
||||
import {
|
||||
filterRules,
|
||||
@@ -24,9 +27,12 @@ const sidebarCountsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
const uptimeProbesTotal = await tableCount(uptimeProbes)
|
||||
const uptimeSpeedProbesTotal = await tableCount(uptimeSpeedProbes)
|
||||
const recursiveRoutesTotal = await tableCount(recursiveRoutes)
|
||||
const [certRes, wireguardTotal] = await Promise.all([
|
||||
const [certRes, wireguardTotal, bgpTotal, vxlanTotal, containersTotal] = await Promise.all([
|
||||
listCertificatesFromServers(),
|
||||
countWireGuardInterfaces().catch(() => 0),
|
||||
countBgpSessions().catch(() => 0),
|
||||
countVxlanTunnels().catch(() => 0),
|
||||
countContainers().catch(() => 0),
|
||||
])
|
||||
const certificatesTotal = certRes.certificates.length
|
||||
const usersTotal = (await listUsers()).length
|
||||
@@ -41,6 +47,9 @@ const sidebarCountsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
certificates: certificatesTotal,
|
||||
wireguard: wireguardTotal,
|
||||
users: usersTotal,
|
||||
bgpSessions: bgpTotal,
|
||||
vxlan: vxlanTotal,
|
||||
containers: containersTotal,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { eq } from "drizzle-orm"
|
||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import { listVxlanTunnels, listVxlanTunnelsForServer } from "../services/vxlan-live.js"
|
||||
import { ServerIdParamSchema, type ServerIdParams } from "../types/server.js"
|
||||
|
||||
const vxlanRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.get("/vxlan", async (_req, reply) => {
|
||||
const tunnels = await listVxlanTunnels()
|
||||
return reply.send({ tunnels })
|
||||
})
|
||||
|
||||
app.get("/servers/:id/vxlan", { schema: { params: ServerIdParamSchema } }, async (req, reply) => {
|
||||
const params = req.params as ServerIdParams
|
||||
const server = (await db.select().from(servers).where(eq(servers.id, params.id)).limit(1))[0]
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
const tunnels = await listVxlanTunnelsForServer(server)
|
||||
return reply.send({ tunnels })
|
||||
})
|
||||
}
|
||||
|
||||
export default vxlanRoutes
|
||||
Reference in new issue
Block a user