fix(alerts): учитывать ошибки реального источника GRE
This commit is contained in:
1 parent
8b60ab040a
commit
9c4a0992d7
15 files changed
+370
-22
No files matched your search
@@ -8,7 +8,7 @@
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "npm run build -w @mmapp/contracts && tsc",
|
||||
"test:alerts-telegram": "tsx src/services/alerts-outbox-retry.test.ts && tsx src/services/alerts-outbox-overview.test.ts && tsx src/services/alert-engine/outbox-execution.test.ts && tsx src/services/alerts-telegram-delivery.test.ts && tsx src/services/alerts-revision.test.ts && tsx src/services/alerts-overview.test.ts && tsx ../packages/contracts/src/alerts-normalization.test.ts && tsx src/services/alerts-config-execution.test.ts && tsx src/services/alerts-telegram-settings.test.ts && tsx src/services/alerts-telegram-send.test.ts && tsx src/routes/alerts.test.ts && tsx src/plugins/auth.alerts.test.ts",
|
||||
"test:alert-engine": "tsx src/services/alert-engine/server-signal-identity.test.ts && tsx src/services/alert-engine/collector-source-signals.test.ts && tsx src/services/alert-engine/monitoring-history.test.ts && tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/bgp-peers-live.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/collector-source-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts",
|
||||
"test:alert-engine": "tsx src/services/alert-engine/server-signal-identity.test.ts && tsx src/services/alert-engine/collector-source-signals.test.ts && tsx src/services/alert-engine/monitoring-history.test.ts && tsx src/services/alert-engine/groups.test.ts && tsx src/services/alert-engine/evaluate-rule.test.ts && tsx src/services/gre-tunnels-live.test.ts && tsx src/services/bgp-peers-live.test.ts && tsx src/services/gre-bgp-snapshot-collector.test.ts && tsx src/services/gre-bgp-snapshot-history.test.ts && tsx src/services/gre-bgp-snapshot-sql.test.ts && tsx src/services/alert-engine/collector-source-sql.test.ts && tsx src/services/alert-engine/confirm-stability.test.ts",
|
||||
"start": "node dist/index.js",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import {createServer} from 'node:http'
|
||||
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
|
||||
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
|
||||
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{collectGreBgpSnapshotOnce}=await import('/app/backend/dist/services/gre-bgp-snapshot-collector.js')
|
||||
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
|
||||
const baseline=process.env.MMAPP_BASELINE==='true',cases=[];let phase='populated',nativeCalls=0,nativeWrites=0,disabledCalls=0
|
||||
const fixture=label=>createServer((req,res)=>{nativeCalls++;if(label==='disabled')disabledCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');const gre=req.url==='/rest/interface/gre',failed=(gre&&phase==='gre-failed')||(label==='second'&&(gre&&['gre-partial','both-partial'].includes(phase)||!gre&&['bgp-partial','both-partial'].includes(phase)));if(failed){res.statusCode=401;res.end(JSON.stringify({error:401,message:'own219 denied'}));return}const row=gre?{'.id':'*1',name:'own219-gre',running:'true',disabled:'false'}:{'.id':'*1',name:'own219-peer',established:'true','remote.as':'65219','local.as':'65220'};const rows=phase==='empty'?[]:gre&&label==='second'&&phase==='malformed'?[{unknown:'fixture'}]:gre&&label==='second'&&phase==='duplicate'?[row,row]:[row];res.end(JSON.stringify(rows))})
|
||||
const natives=['first','second','disabled'].map(fixture);for(const n of natives)await new Promise(r=>n.listen(0,'127.0.0.1',r))
|
||||
const report={date:'2026-10-14',block:'12.5.219',baseline,database,cases,scope:'Actual compiled GRE/BGP sources and snapshot collector, owned SQL/native HTTP fixture; no API auth claim',passed:false}
|
||||
try{
|
||||
await initDatabase();for(let i=0;i<natives.length;i++)await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password,type) VALUES($1,'127.0.0.1',$2,false,$3,'fixture','fixture',$4)",['own219-'+i,natives[i].address().port,i<2,i===2?'home-router':'exit-node'])
|
||||
for(phase of ['populated','empty','gre-partial','gre-failed','bgp-partial','both-partial','malformed','duplicate','disabled','recovery']){
|
||||
if(phase==='disabled')await pool.query('UPDATE servers SET enabled=false');if(phase==='recovery')await pool.query("UPDATE servers SET enabled=true WHERE name IN ('own219-0','own219-1')")
|
||||
const before=nativeCalls,snapshot=await collectGreBgpSnapshotOnce(),greFail=['gre-partial','gre-failed','both-partial','malformed','duplicate'].includes(phase),bgpFail=['bgp-partial','both-partial'].includes(phase),empty=['empty','disabled'].includes(phase),greCount=empty?0:greFail?(baseline?phase==='gre-failed'?0:phase==='malformed'?2:phase==='duplicate'?3:1:0):2,bgpCount=empty||bgpFail?0:2,errorCount=Number(bgpFail)+Number(greFail&&!baseline)
|
||||
assert.equal(snapshot.greWritten,greCount);assert.equal(snapshot.bgpWritten,bgpCount);assert.equal(snapshot.errors.length,errorCount);assert.equal(snapshot.fatalError,undefined)
|
||||
if(greFail&&!baseline)assert(snapshot.errors.some(e=>e.startsWith('GRE: Не опрошены устройства GRE:')))
|
||||
if(bgpFail)assert(snapshot.errors.some(e=>e.startsWith('BGP: Не опрошены устройства BGP:')))
|
||||
if(phase==='disabled')assert.equal(nativeCalls,before)
|
||||
cases.push({phase,greWritten:snapshot.greWritten,bgpWritten:snapshot.bgpWritten,sourceErrors:snapshot.errors.length,nativeCalls:nativeCalls-before})
|
||||
}
|
||||
assert.equal(nativeWrites,0);assert.equal(disabledCalls,0);report.passed=true
|
||||
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre219-private.log',String(e.stack??e),{mode:0o600})}finally{for(const n of natives){n.closeAllConnections();await new Promise(r=>n.close(r))}await closePool();report.nativeWrites=nativeWrites;report.disabledHomeRouterCalls=disabledCalls;fs.writeFileSync('/app/backend/acceptance/gre219-'+(baseline?'baseline':'fixed')+'.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}if(!report.passed)process.exitCode=1
|
||||
@@ -0,0 +1,14 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import {fetchGreTunnelLiveRows} from './gre-tunnels-live.js'
|
||||
import {collectGreBgpSnapshotOnce} from './gre-bgp-snapshot-collector.js'
|
||||
import {servers} from '../db/schema.js'
|
||||
const devices=[{id:1,name:'Первый'},{id:2,name:'Второй'}] as typeof servers.$inferSelect[]
|
||||
for(const phase of ['populated','empty','partial','failed','disabled','sync-failed'] as const){
|
||||
let calls=0
|
||||
const deps={listEnabledServers:async()=>phase==='disabled'?[]:devices,readTunnels:(server:typeof servers.$inferSelect)=>{calls++;if(phase==='sync-failed')throw Error('sync denied');return (async()=>{if(phase==='failed'||(phase==='partial'&&server.id===2))throw Error('native401');return phase==='populated'?[{targetLabel:'Own GRE / '+server.name,status:'up' as const}]:[]})()}}
|
||||
const result=await collectGreBgpSnapshotOnce({fetchGre:()=>fetchGreTunnelLiveRows(deps),fetchBgp:async()=>[{serverName:'BGP-control',name:'own-peer',state:'Established'}]})
|
||||
assert.equal(result.bgpWritten,1);assert.equal(result.greWritten,phase==='populated'?2:0);assert.equal(result.errors.length,['partial','failed','sync-failed'].includes(phase)?1:0)
|
||||
if(result.errors.length){assert.match(result.errors[0],/GRE: Не опрошены устройства GRE/);assert.match(result.errors[0],/Второй/);if(phase!=='partial')assert.match(result.errors[0],/Первый/)}
|
||||
if(phase==='disabled')assert.equal(calls,0)
|
||||
}
|
||||
console.log('GRE alert source: populated/empty/partial/all-failed/disabled/sync error; BGP control preserved, per-device errors retained')
|
||||
@@ -2,6 +2,7 @@ import { eq } from "drizzle-orm"
|
||||
import { db } from "../db/index.js"
|
||||
import { servers } from "../db/schema.js"
|
||||
import { MikrotikClient } from "./mikrotik.js"
|
||||
import { readInternetPathRows } from "./internet-path-snapshot.js"
|
||||
|
||||
type ServerRow = typeof servers.$inferSelect
|
||||
|
||||
@@ -40,21 +41,26 @@ function mapGreRow(server: ServerRow, g: RosGre, idx: number): GreTunnelLiveRow
|
||||
|
||||
async function fetchGreForServer(server: ServerRow): Promise<GreTunnelLiveRow[]> {
|
||||
const client = MikrotikClient.fromServer(server)
|
||||
const greRaw = await client.get<RosGre[]>("/interface/gre")
|
||||
const greRaw = readInternetPathRows(await client.get<unknown>("/interface/gre"), "GRE alert source", [".id", "name"])
|
||||
if (new Set(greRaw.map(row => row[".id"])).size !== greRaw.length) throw new Error("Повторяющиеся ID GRE")
|
||||
return greRaw.map((g, idx) => mapGreRow(server, g, idx))
|
||||
}
|
||||
|
||||
interface GreAlertDependencies {
|
||||
listEnabledServers: () => Promise<ServerRow[]>
|
||||
readTunnels: (server: ServerRow) => Promise<GreTunnelLiveRow[]>
|
||||
}
|
||||
const alertDefaults: GreAlertDependencies = {
|
||||
listEnabledServers: () => db.select().from(servers).where(eq(servers.enabled, true)),
|
||||
readTunnels: fetchGreForServer,
|
||||
}
|
||||
|
||||
/** GRE с включённых серверов — тот же смысл, что `/api/filters/gre-tunnels` (без правил фильтров). */
|
||||
export async function fetchGreTunnelLiveRows(): Promise<GreTunnelLiveRow[]> {
|
||||
const enabledServers = await db.select().from(servers).where(eq(servers.enabled, true))
|
||||
const results = await Promise.all(
|
||||
enabledServers.map(async (server) => {
|
||||
try {
|
||||
return await fetchGreForServer(server)
|
||||
} catch {
|
||||
return [] as GreTunnelLiveRow[]
|
||||
}
|
||||
}),
|
||||
)
|
||||
return results.flat()
|
||||
export async function fetchGreTunnelLiveRows(dependencies: GreAlertDependencies = alertDefaults): Promise<GreTunnelLiveRow[]> {
|
||||
const devices = await dependencies.listEnabledServers()
|
||||
const results = await Promise.allSettled(devices.map(server => Promise.resolve().then(() => dependencies.readTunnels(server))))
|
||||
const failures = results.flatMap((result, index) => result.status === "rejected"
|
||||
? [`${devices[index].name}: ${result.reason instanceof Error ? result.reason.message : "Ошибка опроса устройства"}`] : [])
|
||||
if (failures.length) throw new Error("Не опрошены устройства GRE: " + failures.join("; "))
|
||||
return results.flatMap(result => result.status === "fulfilled" ? result.value : [])
|
||||
}
|
||||
Reference in new issue
Block a user