feat(Mikrotik): add ping endpoint and implement prefetching of ping data for gateways in EasySwitchManager
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 3m18s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 3m18s
This commit is contained in:
@@ -272,6 +272,113 @@ async function applyMikrotikConfig(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mikrotik/ping
|
||||
* Пингует внешний ресурс через указанный MikroTik (jumphost),
|
||||
* по возможности привязываясь к интерфейсу, соответствующему gateway.
|
||||
*
|
||||
* Body:
|
||||
* - serverId: ID/имя сервера из servers.json (обязательно)
|
||||
* - gatewayIp?: IP шлюза (remote IP туннеля или прямой gateway)
|
||||
* - interfaceName?: имя интерфейса RouterOS (если знаем его заранее)
|
||||
* - srcAddress?: исходный IP (например, local IP туннеля)
|
||||
* - target?: адрес для ping (по умолчанию www.gstatic.com)
|
||||
* - count?: количество пакетов (по умолчанию 5)
|
||||
*/
|
||||
async function pingViaInterface(req, res) {
|
||||
try {
|
||||
const {
|
||||
serverId,
|
||||
gatewayIp,
|
||||
interfaceName,
|
||||
srcAddress,
|
||||
target = 'www.gstatic.com',
|
||||
count = 5,
|
||||
} = req.body || {};
|
||||
|
||||
if (!serverId) {
|
||||
return sendError(res, 400, 'serverId is required', 'E_BAD_REQUEST');
|
||||
}
|
||||
|
||||
const servers = await readServersFromS3();
|
||||
const server = servers.find((s) => (s.id || s.dns || s.ip) === serverId);
|
||||
if (!server || server.type !== 'jumphost') {
|
||||
return sendError(res, 400, 'Jumphost server not found', 'E_NOT_FOUND');
|
||||
}
|
||||
|
||||
const creds = getMikrotikCredentials(server);
|
||||
if (!creds) {
|
||||
return sendError(res, 400, 'MikroTik credentials not configured for this server', 'E_CREDENTIALS');
|
||||
}
|
||||
|
||||
const client = createRosClient(creds);
|
||||
|
||||
// Пытаемся найти интерфейс по gatewayIp и serverId, если interfaceName не передан
|
||||
let ifaceName = interfaceName || null;
|
||||
let srcAddr = srcAddress || null;
|
||||
|
||||
if (!ifaceName && !srcAddr && gatewayIp) {
|
||||
const config = await loadNetworkConfig();
|
||||
const tunnelIfaces = Array.isArray(config.tunnelInterfaces) ? config.tunnelInterfaces : [];
|
||||
|
||||
const iface = tunnelIfaces.find((i) => {
|
||||
const sameServer =
|
||||
i.serverId === serverId ||
|
||||
i.serverId === server.ip ||
|
||||
i.serverId === server.dns;
|
||||
const sameIp = i.remoteIp === gatewayIp || i.localIp === gatewayIp;
|
||||
return sameServer && sameIp;
|
||||
});
|
||||
|
||||
if (iface) {
|
||||
ifaceName = iface.name || null;
|
||||
srcAddr = iface.localIp || srcAddr;
|
||||
}
|
||||
}
|
||||
|
||||
const body = {};
|
||||
body.address = target;
|
||||
body.count = Number(count) > 0 ? Number(count) : 5;
|
||||
if (ifaceName) body.interface = ifaceName;
|
||||
if (srcAddr) body['src-address'] = srcAddr;
|
||||
|
||||
const pingRes = await client.command('tool/ping', body);
|
||||
const raw = pingRes?.data;
|
||||
const rows = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
||||
|
||||
const parseMs = (val) => {
|
||||
if (val == null) return null;
|
||||
const s = String(val).trim();
|
||||
const m = s.match(/([\d.]+)/);
|
||||
return m ? Number(m[1]) : null;
|
||||
};
|
||||
|
||||
// Ищем строку с аггрегированными полями (avg-rtt, min-rtt, max-rtt, packet-loss)
|
||||
let summary = rows.find((r) => r['avg-rtt'] || r['packet-loss']) || rows[rows.length - 1] || {};
|
||||
|
||||
const avgMs = parseMs(summary['avg-rtt'] || summary.time);
|
||||
const minMs = parseMs(summary['min-rtt']);
|
||||
const maxMs = parseMs(summary['max-rtt']);
|
||||
const loss =
|
||||
summary['packet-loss'] != null
|
||||
? Number(String(summary['packet-loss']).replace('%', ''))
|
||||
: null;
|
||||
|
||||
return sendOk(res, {
|
||||
ok: true,
|
||||
avgMs,
|
||||
minMs,
|
||||
maxMs,
|
||||
loss,
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error.response?.data?.message || error.message || 'Ping failed';
|
||||
const status = error.response?.status;
|
||||
console.error('pingViaInterface:', error);
|
||||
return sendError(res, status && status >= 400 ? status : 502, msg, 'E_PING');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mikrotik/run-script
|
||||
* Body: { serverId, script?: string } — по умолчанию script=update_bgp_filter
|
||||
@@ -318,4 +425,5 @@ module.exports = {
|
||||
testMikrotikConnection,
|
||||
applyMikrotikConfig,
|
||||
runScript,
|
||||
pingViaInterface,
|
||||
};
|
||||
|
||||
@@ -452,7 +452,10 @@ app.post('/api/mikrotik/generate', mikrotikConfigRoutes.generateMikrotikConfig);
|
||||
app.get('/api/mikrotik/generate-interfaces', mikrotikConfigRoutes.generateInterfaces);
|
||||
app.get('/api/mikrotik/generate-recursive-routes', mikrotikConfigRoutes.generateRecursiveRoutes);
|
||||
app.post('/api/mikrotik/test-connection', mikrotikConfigRoutes.testMikrotikConnection);
|
||||
// Простой health-check (используется NetworkConfigManager при открытии модалки применения)
|
||||
app.get('/api/mikrotik/ping', (req, res) => res.json({ ok: true, ts: Date.now() }));
|
||||
// Реальный ping через RouterOS по интерфейсу/шлюзу
|
||||
app.post('/api/mikrotik/ping', writeLimiter, mikrotikConfigRoutes.pingViaInterface);
|
||||
app.post('/api/mikrotik/apply', mikrotikConfigRoutes.applyMikrotikConfig);
|
||||
app.post('/api/mikrotik/run-script', mikrotikConfigRoutes.runScript);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user