feat: 新增设备巡检模块实现完整的巡检告警功能

新增设备巡检模块核心功能,包括:
- 巡检数据存储与调度系统
- 华为/H3C交换机及Linux服务器采集器
- 设备扩展配置与告警规则管理
- 主机详情页巡检配置入口
- 产品名称自定义设置功能
- 网络设备SSH exec通道兼容处理
This commit is contained in:
2026-10-09 10:37:48 +08:00
parent 9202506c46
commit c64e00d835
75 changed files with 12087 additions and 446 deletions

View File

@@ -0,0 +1,502 @@
"use strict";
/**
* 巡检模块统一入口
*
* 提供 IPC handler 注册函数,供主进程注册。
* 渲染进程通过 IPC 调用巡检相关功能。
*/
const deviceStore = require("./deviceStore.cjs");
const alertRuleStore = require("./alertRuleStore.cjs");
const inspectionResultStore = require("./inspectionResultStore.cjs");
const alertRecordStore = require("./alertRecordStore.cjs");
const adapterRegistry = require("./adapterRegistry.cjs");
const InspectionScheduler = require("./inspectionScheduler.cjs");
const { execSshCommand } = require("./inspectionExec.cjs");
const { executeBoundedSshCommand } = require("../boundedSshExec.cjs");
const {
ALERT_LEVEL_LABELS,
DEVICE_TIER_LABELS,
INSPECTION_STATUS_LABELS,
ALERT_RECORD_STATUS_LABELS,
OPERATOR_LABELS,
DEVICE_CATALOG_LABELS,
METRIC_LABELS,
INSPECTION_STATUS,
} = require("./types.cjs");
let _scheduler = null;
/**
* 获取标签映射(前端展示用)
*/
function getLabels() {
return {
alert_levels: ALERT_LEVEL_LABELS,
device_tiers: DEVICE_TIER_LABELS,
inspection_status: INSPECTION_STATUS_LABELS,
alert_record_status: ALERT_RECORD_STATUS_LABELS,
operators: OPERATOR_LABELS,
device_catalog: DEVICE_CATALOG_LABELS,
metrics: METRIC_LABELS,
};
}
/**
* 注册巡检模块的 IPC handlers
*
* @param {object} ipcMain - Electron ipcMain
* @param {object} options
* @param {function} options.readStorage - 读取本地存储 (key) => string
* @param {function} options.writeStorage - 写入本地存储 (key, value) => void
*/
function registerInspectionHandlers(ipcMain, options) {
const { readStorage, writeStorage, sessions } = options;
const readFn = (key) => readStorage(key);
const writeFn = (key, val) => writeStorage(key, val);
// 根据主机地址查找会话(用于复用终端 SSH 连接)
function findSessionByHost(hostname) {
const DEBUG = process.env.INSPECTION_DEBUG === "1";
if (DEBUG) console.log("[Inspection] 查找会话,总会话数:", sessions?.size || 0);
if (!sessions) {
if (DEBUG) console.log("[Inspection] sessions 未初始化");
return null;
}
for (const session of sessions.values()) {
if (!session) continue;
// SSH 会话才有 conn
if (!session.conn) continue;
// 匹配主机地址
const sessionHost = session.hostname || session.host || "";
if (DEBUG) {
console.log("[Inspection] SSH会话:", sessionHost, "| 目标:", hostname);
}
if (sessionHost === hostname) {
if (DEBUG) console.log("[Inspection] 匹配到会话:", sessionHost);
return session;
}
}
return null;
}
// ---------- 标签映射 ----------
ipcMain.handle("inspection:getLabels", () => {
return getLabels();
});
// ---------- 系统配置 ----------
ipcMain.handle("inspection:getSysConfig", () => {
return deviceStore.getSysConfig(readFn);
});
ipcMain.handle("inspection:updateSysConfig", (_e, updates) => {
return deviceStore.updateSysConfig(readFn, writeFn, updates);
});
// ---------- 设备类型 ----------
ipcMain.handle("inspection:getCatalog", () => {
return deviceStore.getCatalog();
});
ipcMain.handle("inspection:getTiers", () => {
return deviceStore.getTiers();
});
ipcMain.handle("inspection:getAdapters", () => {
return adapterRegistry.getRegisteredAdapters();
});
// ---------- 设备扩展配置 ----------
ipcMain.handle("inspection:getAllExtensions", () => {
return deviceStore.getAllExtensions(readFn);
});
ipcMain.handle("inspection:getExtension", (_e, hostId) => {
return deviceStore.getExtension(readFn, hostId);
});
ipcMain.handle("inspection:upsertExtension", (_e, hostId, data) => {
return deviceStore.upsertExtension(readFn, writeFn, hostId, data);
});
ipcMain.handle("inspection:deleteExtension", (_e, hostId) => {
return deviceStore.deleteExtension(readFn, writeFn, hostId);
});
ipcMain.handle("inspection:getEffectiveInterval", (_e, hostId) => {
return deviceStore.getEffectiveInterval(readFn, hostId);
});
ipcMain.handle("inspection:getEnabledDevices", () => {
return deviceStore.getEnabledDevices(readFn);
});
// ---------- 告警规则 ----------
ipcMain.handle("inspection:listRules", () => {
return alertRuleStore.listRules(readFn);
});
ipcMain.handle("inspection:getRule", (_e, ruleId) => {
return alertRuleStore.getRule(readFn, ruleId);
});
ipcMain.handle("inspection:createRule", (_e, data) => {
return alertRuleStore.createRule(readFn, writeFn, data);
});
ipcMain.handle("inspection:updateRule", (_e, ruleId, updates) => {
return alertRuleStore.updateRule(readFn, writeFn, ruleId, updates);
});
ipcMain.handle("inspection:deleteRule", (_e, ruleId) => {
return alertRuleStore.deleteRule(readFn, writeFn, ruleId);
});
ipcMain.handle("inspection:toggleRule", (_e, ruleId, enabled) => {
return alertRuleStore.toggleRule(readFn, writeFn, ruleId, enabled);
});
// ---------- 巡检结果 ----------
ipcMain.handle("inspection:getLatestAll", () => {
return inspectionResultStore.getLatestAll(readFn);
});
ipcMain.handle("inspection:getLatestByDevice", (_e, hostId, limit) => {
return inspectionResultStore.getLatestByDevice(readFn, hostId, limit);
});
ipcMain.handle("inspection:getRecentResults", (_e, limit) => {
return inspectionResultStore.getRecent(readFn, limit);
});
// ---------- 单次巡检执行(前端传入完整主机配置) ----------
ipcMain.handle("inspection:runInspection", async (_e, payload) => {
const { hostId, hostConfig, catalogId } = payload || {};
if (!hostId || !hostConfig) {
return { success: false, error: "缺少主机配置" };
}
const DEBUG = process.env.INSPECTION_DEBUG === "1";
if (DEBUG) {
console.log("[Inspection] 巡检配置:", JSON.stringify({
host: hostConfig.hostname || hostConfig.address,
port: hostConfig.port || 22,
username: hostConfig.username,
hasPassword: !!hostConfig.password,
hasPrivateKey: !!hostConfig.privateKeyContent,
hasKeyId: !!hostConfig.identityFileId,
}));
}
const sshConfig = {
host: hostConfig.hostname || hostConfig.address,
port: hostConfig.port || 22,
username: hostConfig.username,
password: hostConfig.password,
privateKey: hostConfig.privateKeyContent,
passphrase: hostConfig.passphrase,
};
if (!sshConfig.host || !sshConfig.username) {
return { success: false, error: "主机地址或用户名缺失" };
}
const startTime = Date.now();
const result = {
id: `ins_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
host_id: hostId,
catalog_id: catalogId || "linux_server",
start_time: startTime,
end_time: 0,
status: INSPECTION_STATUS.SUCCESS,
metrics: {},
raw: {},
error: null,
alerts: [],
};
try {
// 构造 execFn — 优先复用终端会话的 SSH 连接
const execFn = async (command, options = {}) => {
const timeoutMs = options.timeoutMs || 60000;
const DEBUG = process.env.INSPECTION_DEBUG === "1";
// 1. 先尝试复用终端会话的连接
const session = findSessionByHost(sshConfig.host);
if (DEBUG) {
console.log("[Inspection] 查找会话:", sshConfig.host,
"| 找到会话:", session ? "是" : "否",
"| 有连接:", session?.conn ? "是" : "否");
}
if (session && session.conn) {
try {
const result = await executeBoundedSshCommand(session.conn, command, {
openingTimeoutMs: timeoutMs,
runTimeoutMs: timeoutMs,
maxOutputBytes: 100 * 1024,
});
if (DEBUG) console.log("[Inspection] 复用会话成功, stdout:", result.stdout?.slice(0, 100));
return result.stdout + (result.stderr ? "\n" + result.stderr : "");
} catch (e) {
// 终端连接失败,回退到新建连接
if (DEBUG) console.log("[Inspection] 复用会话失败,回退:", e.message);
}
}
// 2. 回退:新建 SSH 连接
return await execSshCommand(sshConfig, command, {
connectTimeoutMs: 10000,
execTimeoutMs: timeoutMs,
});
};
// 构造批量执行函数(一次连接执行多条命令)
const execCommandsFn = async (commands, options = {}) => {
const { execSshCommands } = require("./inspectionExec.cjs");
return await execSshCommands(sshConfig, commands, options);
};
// 获取设备类型对应的采集器类名
const catalogs = deviceStore.getCatalog();
const catalog = catalogs.find((c) => c.id === catalogId);
const adapterClassName = catalog?.adapter_class || "LinuxServerAdapter";
// 创建采集器实例(注入 execFn 和 execCommandsFn)
const adapter = adapterRegistry.createAdapter(adapterClassName, sshConfig, {
execFn,
execCommands: execCommandsFn,
timeoutMs: 20000,
});
if (!adapter) {
throw new Error("采集器加载失败: " + adapterClassName);
}
const collectResult = await adapter.collect();
result.metrics = collectResult.metrics || {};
result.raw = collectResult.raw || {};
if (!collectResult.online) {
result.status = collectResult.status || INSPECTION_STATUS.FAILED;
result.error = collectResult.error || "设备离线或连接失败";
}
} catch (err) {
result.status = INSPECTION_STATUS.FAILED;
result.error = err.message;
console.error("[Inspection] runInspection error:", err.message);
}
result.end_time = Date.now();
const duration = result.end_time - result.start_time;
// 构造设备信息(用于告警记录)
const deviceInfo = {
device_id: hostId,
device_name: hostConfig.label || hostConfig.hostname || hostId,
catalog_id: catalogId || "linux_server",
};
// 保存巡检结果
try {
inspectionResultStore.addResult(readFn, writeFn, {
...result,
device_id: hostId,
online: result.status === INSPECTION_STATUS.SUCCESS,
duration_ms: duration,
error_message: result.error,
});
} catch (e) {
console.warn("[Inspection] 保存巡检结果失败:", e.message);
}
// 告警判断(成功/失败都要判断,特别是 online 指标)
const triggeredAlerts = [];
try {
const deviceExt = deviceStore.getExtension(readFn, hostId);
const rules = alertRuleStore.getRulesForDevice(readFn, deviceExt || { host_id: hostId, catalog_id: catalogId });
const isOnline = result.status === INSPECTION_STATUS.SUCCESS;
for (const rule of rules) {
// online 指标 — 成功/失败都要判断(设备离线告警)
if (rule.metric === "online") {
const shouldAlert = rule.threshold === "false" ? !isOnline : isOnline;
console.log(`[Inspection] 离线规则: name=${rule.name} enabled=${rule.enabled} isOnline=${isOnline} shouldAlert=${shouldAlert}`);
const alertResult = alertRecordStore.processAlert(
readFn,
writeFn,
deviceInfo,
rule,
isOnline ? "在线" : "离线",
shouldAlert
);
if (alertResult) {
console.log(`[Inspection] 告警触发:`, JSON.stringify(alertResult));
triggeredAlerts.push(alertResult);
}
continue;
}
// 普通指标 — 只在成功时判断(失败时没有指标数据)
if (isOnline) {
const metricValue = result.metrics[rule.metric];
if (metricValue === undefined || metricValue === null) continue;
const isTriggered = alertRuleStore.evaluateRule(metricValue, rule.operator, rule.threshold);
const alertResult = alertRecordStore.processAlert(
readFn,
writeFn,
deviceInfo,
rule,
metricValue,
isTriggered
);
if (alertResult) {
triggeredAlerts.push(alertResult);
}
}
}
} catch (e) {
console.warn("[Inspection] 告警判断失败:", e.message);
}
return {
success: result.status === INSPECTION_STATUS.SUCCESS,
status: result.status,
metrics: result.metrics,
error: result.error,
duration,
alerts: triggeredAlerts,
};
});
// ---------- 告警记录 ----------
ipcMain.handle("inspection:listAlerts", (_e, options) => {
return alertRecordStore.listAlerts(readFn, options || {});
});
ipcMain.handle("inspection:getActiveAlerts", () => {
return alertRecordStore.getActiveAlerts(readFn);
});
ipcMain.handle("inspection:getAlertStats", () => {
return alertRecordStore.getAlertStats(readFn);
});
ipcMain.handle("inspection:acknowledgeAlert", (_e, alertId, operator) => {
return alertRecordStore.acknowledgeAlert(readFn, writeFn, alertId, operator);
});
ipcMain.handle("inspection:resolveAlert", (_e, alertId, reason, operator) => {
return alertRecordStore.resolveAlert(readFn, writeFn, alertId, reason, operator);
});
ipcMain.handle("inspection:closeAlert", (_e, alertId, note) => {
return alertRecordStore.closeAlert(readFn, writeFn, alertId, note);
});
ipcMain.handle("inspection:updateAlertNote", (_e, alertId, note) => {
return alertRecordStore.updateNote(readFn, writeFn, alertId, note);
});
// 存储前端传入的设备 SSH 配置(hostId → sshConfig)
let _hostConfigs = null;
// ---------- 调度器控制 ----------
ipcMain.handle("inspection:scheduler:start", (_e, options = {}) => {
// 前端可传入 { hostConfigs: { [hostId]: sshConfig } } 用于调度器执行 SSH 命令
if (options.hostConfigs) {
_hostConfigs = options.hostConfigs;
}
// 从 hostConfigs 提取 hostInfo(hostname/label)用于告警显示
const hostInfo = {};
if (_hostConfigs) {
for (const [hostId, config] of Object.entries(_hostConfigs)) {
hostInfo[hostId] = {
hostname: config.host || config.hostname || "",
label: config.label || "",
};
}
}
if (!_scheduler) {
_scheduler = new InspectionScheduler({
readFn,
writeFn,
execFn: async (hostId, command) => {
// 优先用前端传入的 hostConfigs
if (_hostConfigs && _hostConfigs[hostId]) {
const config = _hostConfigs[hostId];
const { execSshCommand: execCmd } = require("./inspectionExec.cjs");
return await execCmd(config, command, {
connectTimeoutMs: 10000,
execTimeoutMs: 30000,
});
}
// 兜底:尝试复用已有终端会话
const session = findSessionByHost(hostId);
if (session?.conn) {
const { executeBoundedSshCommand } = require("../boundedSshExec.cjs");
return await executeBoundedSshCommand(session.conn, command, {
timeoutMs: 30000,
});
}
throw new Error(`无法执行巡检:未找到主机 ${hostId} 的 SSH 配置`);
},
pollIntervalMs: 30 * 1000,
concurrency: 5,
timeoutMs: 30 * 1000,
hostInfo,
});
}
_scheduler.start();
return _scheduler.getStatus();
});
ipcMain.handle("inspection:scheduler:stop", () => {
if (_scheduler) {
_scheduler.stop();
return _scheduler.getStatus();
}
return { running: false };
});
ipcMain.handle("inspection:scheduler:status", () => {
return _scheduler ? _scheduler.getStatus() : { running: false };
});
ipcMain.handle("inspection:scheduler:scanNow", (_e, hostId) => {
if (!_scheduler) return { error: "调度器未启动" };
return _scheduler.scanDeviceNow(hostId);
});
ipcMain.handle("inspection:scheduler:scanAllNow", () => {
if (!_scheduler) return { error: "调度器未启动" };
return _scheduler.scanAllNow();
});
console.log("[Inspection] IPC handlers 已注册");
return {
getScheduler: () => _scheduler,
createScheduler: (opts) => {
_scheduler = new InspectionScheduler({
readFn,
writeFn,
execFn: execCommand,
...opts,
});
return _scheduler;
},
};
}
module.exports = {
registerInspectionHandlers,
getLabels,
};