gigafibre-fsm/services/targo-hub/lib/olt-snmp.js
louispaulb 320655b0a0 refactor: major cleanup — remove dead dispatch app, commit all backend code, extract client composables
- Remove apps/dispatch/ (100% replaced by ops dispatch module, unmaintained)
- Commit services/targo-hub/lib/ (24 modules, 6290 lines — was never tracked)
- Commit services/docuseal + services/legacy-db docker-compose configs
- Extract client app composables: useOTP, useAddressSearch, catalog data, format utils
- Refactor CartPage.vue 630→175 lines, CatalogPage.vue 375→95 lines
- Clean hardcoded credentials from config.js fallback values
- Add client portal: catalog, cart, checkout, OTP verification, address search
- Add ops: NetworkPage, AgentFlowsPage, ConversationPanel, UnifiedCreateModal
- Add ops composables: useBestTech, useConversations, usePermissions, useScanner
- Add field app: scanner composable, docker/nginx configs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 17:38:38 -04:00

342 lines
11 KiB
JavaScript

'use strict'
const snmp = require('net-snmp')
const cfg = require('./config')
const { log } = require('./helpers')
const olts = new Map()
let pollTimer = null
const POLL_INTERVAL = cfg.OLT_POLL_INTERVAL || 300000
const SNMP_TIMEOUT = 4000
// --- TP-Link DeltaStream GPON OLT MIB (enterprise 11863) ---
// ONU table: .6.100.1.7.2.1.{col}.0.{port}.{onu}
const TPLINK = {
BASE: '1.3.6.1.4.1.11863.6.100.1.7.2.1',
COL_PORT_NAME: 3, COL_ONU_IDX: 4, COL_DESCRIPTION: 5, COL_SERIAL: 6,
COL_LINE_PROF: 7, COL_STATUS: 8, COL_VENDOR: 15, COL_MODEL: 16,
COL_UPTIME: 19, COL_HW_VER: 20, COL_RX_POWER: 26, COL_TX_POWER: 27,
COL_ONU_RX: 28, COL_DISTANCE: 30, COL_TEMPERATURE: 31, COL_LAST_DOWN: 32,
COL_FIRMWARE: 33, COL_OFFLINE_CAUSE: 42, COL_MAC: 43,
COL_MAP: {},
FETCH_COLS: [],
}
TPLINK.COL_MAP = {
[TPLINK.COL_PORT_NAME]: 'port', [TPLINK.COL_ONU_IDX]: 'onuIdx',
[TPLINK.COL_DESCRIPTION]: 'description', [TPLINK.COL_SERIAL]: 'serial',
[TPLINK.COL_VENDOR]: 'vendor', [TPLINK.COL_MODEL]: 'model',
[TPLINK.COL_UPTIME]: 'uptime', [TPLINK.COL_RX_POWER]: 'rxPowerOlt',
[TPLINK.COL_TX_POWER]: 'txPowerOlt', [TPLINK.COL_ONU_RX]: 'rxPowerOnu',
[TPLINK.COL_DISTANCE]: 'distance', [TPLINK.COL_TEMPERATURE]: 'temperature',
[TPLINK.COL_FIRMWARE]: 'firmware', [TPLINK.COL_OFFLINE_CAUSE]: 'lastOfflineCause',
[TPLINK.COL_MAC]: 'mac', [TPLINK.COL_LINE_PROF]: 'lineProfile',
}
TPLINK.FETCH_COLS = [
TPLINK.COL_PORT_NAME, TPLINK.COL_ONU_IDX, TPLINK.COL_DESCRIPTION, TPLINK.COL_SERIAL,
TPLINK.COL_STATUS, TPLINK.COL_VENDOR, TPLINK.COL_MODEL, TPLINK.COL_UPTIME,
TPLINK.COL_RX_POWER, TPLINK.COL_TX_POWER, TPLINK.COL_ONU_RX,
TPLINK.COL_DISTANCE, TPLINK.COL_TEMPERATURE, TPLINK.COL_FIRMWARE,
TPLINK.COL_OFFLINE_CAUSE, TPLINK.COL_MAC, TPLINK.COL_LINE_PROF,
]
// backward compat alias
const OID = TPLINK
// --- Raisecom ISCOM6800 GPON OLT MIB (enterprise 8886) ---
// ONU table: 1.3.6.1.4.1.8886.18.3.1.3.1.1.{col}.{onu_id}
// Signal table: 1.3.6.1.4.1.8886.18.3.1.3.3.1.1.{onu_id} (rx_power in dBm*10)
// onu_id encoding: (slot+32)*8388608 + port*65536 + ontid
const RAISECOM = {
BASE_TABLE: '1.3.6.1.4.1.8886.18.3.1.3.1.1',
BASE_SIGNAL: '1.3.6.1.4.1.8886.18.3.1.3.3.1.1',
COL_SERIAL: 2,
COL_DISTANCE: 6,
COL_FIRMWARE: 7,
COL_STATUS: 8,
COL_LINE_PROF: 9,
COL_DEREG_COUNT: 15,
COL_ONU_RX: 16,
FETCH_COLS: [2, 6, 7, 8, 9, 16],
COL_MAP: { 2: 'serial', 6: 'distance', 7: 'firmware', 9: 'lineProfile', 16: 'rxPowerOnu' },
}
function decodeRaisecomOnuId (onuId) {
const slot = Math.floor(onuId / 8388608) - 32
const port = Math.floor((onuId % 8388608) / 65536)
const ontid = onuId % 65536
return { slot, port, ontid }
}
function registerOlt ({ host, community, name, type }) {
if (!host || !community) return
if (olts.has(host)) return olts.get(host)
const vendor = (type || 'tplink').toLowerCase()
if (vendor !== 'tplink' && vendor !== 'raisecom') {
log(`OLT ${name || host}: unknown type "${type}", defaulting to tplink`)
}
const olt = {
host, community, name: name || host,
type: (vendor === 'raisecom') ? 'raisecom' : 'tplink',
lastPoll: null, lastError: null, lastDuration: null,
onuCount: 0, onlineCount: 0, onus: new Map(),
}
olts.set(host, olt)
log(`OLT registered: ${name || host} (${host}) [${olt.type}]`)
return olt
}
const createSession = (host, community) =>
snmp.createSession(host, community, { timeout: SNMP_TIMEOUT, retries: 1, version: snmp.Version2c })
function snmpWalk (session, oid) {
return new Promise((resolve) => {
const results = []
const timer = setTimeout(() => {
try { session.close() } catch {}
resolve(results)
}, SNMP_TIMEOUT + 2000)
session.subtree(oid, 50, (varbinds) => {
for (const vb of varbinds) {
if (!snmp.isVarbindError(vb)) results.push(vb)
}
}, (error) => {
clearTimeout(timer)
resolve(results)
})
})
}
function extractVal (vb) {
if (vb.type === snmp.ObjectType.OctetString) return vb.value.toString()
if (vb.type === snmp.ObjectType.Integer) return vb.value
return vb.value?.toString?.() || ''
}
function parseTplinkTable (varbinds) {
const byKey = new Map()
const baseLen = TPLINK.BASE.split('.').length
for (const vb of varbinds) {
const suffix = vb.oid.toString().split('.').slice(baseLen)
if (suffix.length < 3) continue
const col = parseInt(suffix[0])
const key = suffix.slice(1).join('.')
if (!byKey.has(key)) byKey.set(key, {})
const entry = byKey.get(key)
const val = extractVal(vb)
if (col === TPLINK.COL_STATUS) {
entry.status = (val === 1 || val === '1') ? 'online' : 'offline'
} else if (TPLINK.COL_MAP[col]) {
entry[TPLINK.COL_MAP[col]] = val
}
}
const onus = []
for (const [key, entry] of byKey) {
if (!entry.serial) continue
entry._key = key
onus.push(entry)
}
return onus
}
function parseRaisecomTable (tableVarbinds, signalVarbinds) {
const byId = new Map()
const tableBaseLen = RAISECOM.BASE_TABLE.split('.').length
for (const vb of tableVarbinds) {
const suffix = vb.oid.toString().split('.').slice(tableBaseLen)
if (suffix.length < 2) continue
const col = parseInt(suffix[0])
const onuId = parseInt(suffix[1])
if (isNaN(onuId)) continue
if (!byId.has(onuId)) byId.set(onuId, { _onuId: onuId })
const entry = byId.get(onuId)
const val = extractVal(vb)
if (col === RAISECOM.COL_STATUS) {
entry.status = (val === 1 || val === '1') ? 'online' : 'offline'
} else if (RAISECOM.COL_MAP[col]) {
entry[RAISECOM.COL_MAP[col]] = val
}
}
// Parse signal table for OLT-side rx power
const sigBaseLen = RAISECOM.BASE_SIGNAL.split('.').length
for (const vb of signalVarbinds) {
const suffix = vb.oid.toString().split('.').slice(sigBaseLen)
if (suffix.length < 1) continue
const onuId = parseInt(suffix[0])
if (isNaN(onuId) || !byId.has(onuId)) continue
const val = extractVal(vb)
// Raisecom reports rx_power in dBm * 10
byId.get(onuId).rxPowerOlt = typeof val === 'number' ? val / 10 : val
}
const onus = []
for (const [onuId, entry] of byId) {
if (!entry.serial) continue
const { slot, port, ontid } = decodeRaisecomOnuId(onuId)
entry.port = `0/${slot}/${port}`
entry.onuIdx = ontid
entry._key = `${slot}.${port}.${ontid}`
onus.push(entry)
}
return onus
}
// backward compat alias
const parseOnuTable = parseTplinkTable
async function pollTplink (session) {
const allVarbinds = []
for (const col of TPLINK.FETCH_COLS) {
const vbs = await snmpWalk(session, `${TPLINK.BASE}.${col}`)
allVarbinds.push(...vbs)
}
return parseTplinkTable(allVarbinds)
}
async function pollRaisecom (olt) {
// Raisecom needs a fresh session per walk (session gets closed after subtree timeout)
const tableVarbinds = []
for (const col of RAISECOM.FETCH_COLS) {
const s = createSession(olt.host, olt.community)
const vbs = await snmpWalk(s, `${RAISECOM.BASE_TABLE}.${col}`)
try { s.close() } catch {}
tableVarbinds.push(...vbs)
}
const sigSession = createSession(olt.host, olt.community)
const signalVarbinds = await snmpWalk(sigSession, RAISECOM.BASE_SIGNAL)
try { sigSession.close() } catch {}
return parseRaisecomTable(tableVarbinds, signalVarbinds)
}
async function pollOlt (olt) {
const startMs = Date.now()
let session
try {
session = createSession(olt.host, olt.community)
let onus
if (olt.type === 'raisecom') {
session.close(); session = null
onus = await pollRaisecom(olt)
} else {
onus = await pollTplink(session)
session.close(); session = null
}
const prevOnus = new Map(olt.onus)
olt.onus.clear()
let onlineCount = 0
for (const onu of onus) {
olt.onus.set(onu.serial, onu)
if (onu.status === 'online') onlineCount++
const prev = prevOnus.get(onu.serial)
if (prev && prev.status !== onu.status) {
const event = onu.status === 'offline' ? 'olt_offline' : 'olt_online'
logOnuEvent(olt, onu, event, onu.status === 'offline' ? (onu.lastOfflineCause || null) : null)
}
}
olt.onuCount = onus.length
olt.onlineCount = onlineCount
olt.lastPoll = new Date().toISOString()
olt.lastError = null
olt.lastDuration = Date.now() - startMs
log(`OLT ${olt.name}: ${onus.length} ONUs (${onlineCount} online) in ${olt.lastDuration}ms`)
} catch (e) {
olt.lastError = { message: e.message, ts: new Date().toISOString() }
olt.lastDuration = Date.now() - startMs
log(`OLT ${olt.name} SNMP FAILED (${olt.lastDuration}ms): ${e.message} — resuming next`)
} finally {
if (session) try { session.close() } catch {}
}
}
async function logOnuEvent (olt, onu, event, reason) {
log(`ONU ${onu.serial} on ${olt.name}: ${event}${reason ? ' (' + reason + ')' : ''}`)
try {
const { logDeviceEvent } = require('./devices')
if (logDeviceEvent) {
await logDeviceEvent(onu.serial, event, reason, {
olt: olt.name, port: onu.port, mac: onu.mac,
rxPowerOlt: onu.rxPowerOlt, rxPowerOnu: onu.rxPowerOnu,
distance: onu.distance, lastOfflineCause: onu.lastOfflineCause,
})
}
} catch (e) {
log(`Failed to log ONU event: ${e.message}`)
}
}
async function pollAllOlts () {
if (!olts.size) return
const startMs = Date.now()
let ok = 0, failed = 0
for (const [, olt] of olts) {
await pollOlt(olt)
olt.lastError ? failed++ : ok++
}
if (failed > 0) log(`OLT poll complete: ${ok} OK, ${failed} FAILED in ${Date.now() - startMs}ms`)
}
function getOnuBySerial (serial) {
if (!serial) return null
const normalized = serial.replace(/^(TPLG)([A-F0-9])/i, '$1-$2').toUpperCase()
for (const [, olt] of olts) {
const enrich = (o) => ({ ...o, oltName: olt.name, oltHost: olt.host })
let onu = olt.onus.get(serial) || olt.onus.get(normalized)
if (onu) return enrich(onu)
for (const [s, o] of olt.onus) {
if (s.includes(serial) || serial.includes(s.replace(/-/g, ''))) return enrich(o)
}
}
return null
}
const getOltStats = () => [...olts.entries()].map(([host, olt]) => ({
host, name: olt.name, type: olt.type, onuCount: olt.onuCount, onlineCount: olt.onlineCount,
lastPoll: olt.lastPoll, lastDuration: olt.lastDuration, lastError: olt.lastError,
}))
const getAllOnus = () => {
const all = []
for (const [, olt] of olts) {
for (const [, onu] of olt.onus) all.push({ ...onu, oltName: olt.name })
}
return all
}
function startOltPoller () {
for (const o of (cfg.OLT_LIST || [])) registerOlt(o)
if (cfg.OLT_HOST && !olts.has(cfg.OLT_HOST)) {
registerOlt({ host: cfg.OLT_HOST, community: cfg.OLT_COMMUNITY || 'public', name: cfg.OLT_NAME || cfg.OLT_HOST, type: cfg.OLT_TYPE })
}
if (!olts.size) return log('OLT SNMP poller: no OLTs configured')
log(`OLT SNMP poller: ${olts.size} OLT(s), polling every ${POLL_INTERVAL / 1000}s`)
setTimeout(() => {
pollAllOlts().catch(e => log('OLT poll error:', e.message))
pollTimer = setInterval(() => {
pollAllOlts().catch(e => log('OLT poll error:', e.message))
}, POLL_INTERVAL)
}, 10000)
}
function stopOltPoller () {
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
}
module.exports = {
registerOlt, startOltPoller, stopOltPoller,
getOnuBySerial, getOltStats, getAllOnus, pollAllOlts,
}