新增设备巡检模块核心功能,包括: - 巡检数据存储与调度系统 - 华为/H3C交换机及Linux服务器采集器 - 设备扩展配置与告警规则管理 - 主机详情页巡检配置入口 - 产品名称自定义设置功能 - 网络设备SSH exec通道兼容处理
503 lines
16 KiB
JavaScript
503 lines
16 KiB
JavaScript
"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,
|
||
};
|