feat(ResourceStats): add resource statistics API endpoint and integrate into frontend with routing and localization updates
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m35s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m35s
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* Роуты для сбора статистики ресурсов (RAM, CPU, HDD) с MikroTik RouterOS.
|
||||
* GET /api/resources/stats — system/resource по каждому jumphost/home с учётами MikroTik.
|
||||
*/
|
||||
|
||||
const { sendError } = require('../middleware/errorHandler');
|
||||
const { decrypt } = require('../utils/encryption');
|
||||
const { readServersFromS3 } = require('./serversRoutes');
|
||||
const { createRosClient } = require('../services/mikrotikApplyService');
|
||||
|
||||
function getMikrotikCredentials(server) {
|
||||
if (!server || (server.type !== 'jumphost' && server.type !== 'home')) return null;
|
||||
const host = server.mikrotikHost || server.ip || server.dns;
|
||||
if (!host) return null;
|
||||
const port = server.mikrotikPort;
|
||||
const restPort = (port === 80 || port === 443 || port === 8443) ? port : 80;
|
||||
const user = server.mikrotikUser || 'admin';
|
||||
let password = '';
|
||||
if (server.encryptedMikrotikPassword) {
|
||||
try {
|
||||
password = decrypt(server.encryptedMikrotikPassword);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return { host, port: restPort, user, password, secure: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсинг system/resource в нормализованный объект для UI.
|
||||
* MikroTik REST возвращает: total-memory, free-memory (bytes), cpu-load (%),
|
||||
* total-hdd-space, free-hdd-space (bytes), uptime, version, board-name и др.
|
||||
*/
|
||||
function parseResource(raw) {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const num = (v) => (v != null && v !== '' ? parseInt(String(v), 10) : null);
|
||||
const totalMemory = num(raw['total-memory']);
|
||||
const freeMemory = num(raw['free-memory']);
|
||||
const usedMemory = totalMemory != null && freeMemory != null ? totalMemory - freeMemory : null;
|
||||
const totalHdd = num(raw['total-hdd-space']);
|
||||
const freeHdd = num(raw['free-hdd-space']);
|
||||
const usedHdd = totalHdd != null && freeHdd != null ? totalHdd - freeHdd : null;
|
||||
const cpuLoad = raw['cpu-load'] != null ? parseFloat(String(raw['cpu-load'])) : null;
|
||||
|
||||
return {
|
||||
// RAM (bytes и проценты)
|
||||
totalMemory: totalMemory,
|
||||
freeMemory: freeMemory,
|
||||
usedMemory: usedMemory,
|
||||
memoryUsagePercent:
|
||||
totalMemory != null && totalMemory > 0 && usedMemory != null
|
||||
? Math.round((usedMemory / totalMemory) * 100)
|
||||
: null,
|
||||
// CPU (%)
|
||||
cpuLoad: cpuLoad != null ? Math.round(cpuLoad) : null,
|
||||
cpuCount: num(raw['cpu-count']) ?? null,
|
||||
cpuFrequency: raw['cpu-frequency'] != null ? String(raw['cpu-frequency']) : null,
|
||||
cpu: raw.cpu != null ? String(raw.cpu) : null,
|
||||
// HDD (bytes и проценты)
|
||||
totalHddSpace: totalHdd,
|
||||
freeHddSpace: freeHdd,
|
||||
usedHddSpace: usedHdd,
|
||||
hddUsagePercent:
|
||||
totalHdd != null && totalHdd > 0 && usedHdd != null
|
||||
? Math.round((usedHdd / totalHdd) * 100)
|
||||
: null,
|
||||
// прочее
|
||||
uptime: raw.uptime != null ? String(raw.uptime) : null,
|
||||
version: raw.version != null ? String(raw.version) : null,
|
||||
boardName: raw['board-name'] != null ? String(raw['board-name']) : null,
|
||||
architectureName: raw['architecture-name'] != null ? String(raw['architecture-name']) : null,
|
||||
platform: raw.platform != null ? String(raw.platform) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/resources/stats
|
||||
* По каждому jumphost/home с MikroTik — запрос system/resource, ответ с группировкой по роутерам.
|
||||
*/
|
||||
async function getResourceStats(req, res) {
|
||||
try {
|
||||
const servers = await readServersFromS3();
|
||||
const routers = (Array.isArray(servers) ? servers : []).filter(
|
||||
(s) =>
|
||||
s &&
|
||||
(String(s.type || '').toLowerCase() === 'jumphost' ||
|
||||
String(s.type || '').toLowerCase() === 'home')
|
||||
);
|
||||
|
||||
const results = await Promise.all(
|
||||
routers.map(async (server) => {
|
||||
const serverId = server.id || server.dns || server.ip;
|
||||
const name = server.name || server.dns || server.ip || serverId;
|
||||
const host = server.mikrotikHost || server.ip || server.dns;
|
||||
|
||||
const creds = getMikrotikCredentials(server);
|
||||
if (!creds) {
|
||||
return {
|
||||
serverId,
|
||||
name,
|
||||
host,
|
||||
groupKey: server.groupKey || null,
|
||||
error: 'MikroTik API не настроен или нет пароля',
|
||||
resource: null,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const client = createRosClient(creds);
|
||||
const apiRes = await client.print('system/resource');
|
||||
const raw = apiRes?.data;
|
||||
const single = Array.isArray(raw) ? raw[0] : raw;
|
||||
const resource = parseResource(single);
|
||||
return {
|
||||
serverId,
|
||||
name,
|
||||
host,
|
||||
groupKey: server.groupKey || null,
|
||||
error: null,
|
||||
resource,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err.response?.data?.message || err.message || 'Ошибка запроса к RouterOS';
|
||||
return {
|
||||
serverId,
|
||||
name,
|
||||
host,
|
||||
groupKey: server.groupKey || null,
|
||||
error: msg,
|
||||
resource: null,
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return res.json({ routers: results });
|
||||
} catch (error) {
|
||||
console.error('getResourceStats:', error);
|
||||
return sendError(
|
||||
res,
|
||||
500,
|
||||
error.message || 'Ошибка сбора статистики ресурсов',
|
||||
'E_RESOURCE_STATS'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getResourceStats,
|
||||
};
|
||||
@@ -30,6 +30,7 @@ const ipsecPasswordsRoutes = require('./routes/ipsecPasswordsRoutes');
|
||||
const mikrotikConfigRoutes = require('./routes/mikrotikConfigRoutes');
|
||||
const mikrotikBackupRoutes = require('./routes/mikrotikBackupRoutes');
|
||||
const trafficRoutes = require('./routes/trafficRoutes');
|
||||
const resourceStatsRoutes = require('./routes/resourceStatsRoutes');
|
||||
const { initMikrotikBackupScheduler } = require('./services/mikrotikBackupScheduler');
|
||||
const { initNetworkMapScheduler } = require('./services/networkMapScheduler');
|
||||
const { initPingServicesScheduler } = require('./services/pingServicesScheduler');
|
||||
@@ -479,6 +480,9 @@ app.post('/api/uptime/check', writeLimiter, mikrotikConfigRoutes.uptimeCheck);
|
||||
// === TRAFFIC STATS (MikroTik interfaces by jumphost) ===
|
||||
app.get('/api/traffic/interface-stats', trafficRoutes.getInterfaceStats);
|
||||
|
||||
// === RESOURCE STATS (RAM/CPU/HDD по роутерам) ===
|
||||
app.get('/api/resources/stats', resourceStatsRoutes.getResourceStats);
|
||||
|
||||
// === MIKROTIK BACKUPS (S3) ===
|
||||
app.post('/api/mikrotik/backups', writeLimiter, mikrotikBackupRoutes.createBackup);
|
||||
app.get('/api/mikrotik/backups', mikrotikBackupRoutes.listBackups);
|
||||
|
||||
Reference in New Issue
Block a user