Files
router-lists-ui/backend/services/mikrotikBackupScheduler.js
T
2026-02-11 13:20:24 +07:00

186 lines
6.6 KiB
JavaScript

/**
* Простейший планировщик автоматических бэкапов MikroTik.
*
* Работает внутри Node-процесса backend:
* - периодически обходит jumphost-сервера из servers.json
* - вызывает RouterOS REST API /rest/export (compact)
* - сохраняет конфиг в S3 через saveBackupForServer
*
* Управляется через переменные окружения:
* - MIKROTIK_BACKUP_ENABLED=true|false (по умолчанию true)
* - MIKROTIK_BACKUP_INTERVAL_MINUTES=60 (интервал между запусками)
* - MIKROTIK_BACKUP_SERVERS="id1,id2" (если не указано — все jumphost)
*/
const { readServersFromS3 } = require('../routes/serversRoutes');
const { createRosClient, fetchExportViaFile } = require('./mikrotikApplyService');
const { saveBackupForServer } = require('../routes/mikrotikBackupRoutes');
const { decrypt } = require('../utils/encryption');
const { readS3TextObject } = require('../services/s3Service');
const DEFAULT_INTERVAL_MIN = 60;
function parseServerIds(envValue) {
if (!envValue) return null;
const parts = String(envValue)
.split(',')
.map((s) => s.trim())
.filter(Boolean);
return parts.length > 0 ? parts : null;
}
/**
* Прочитать список серверов для автобэкапов из UI-настроек (rt_ui_settings.json).
* Если поле mikrotikBackupServers отсутствует или пустое — вернуть null.
*/
async function getServerIdsFromUiSettings(logger) {
try {
const data = await readS3TextObject('bgp_data/rt_ui_settings.json').catch(() => null);
if (!data || !data.body) return null;
const parsed = JSON.parse(data.body || '{}');
const list = parsed && Array.isArray(parsed.mikrotikBackupServers)
? parsed.mikrotikBackupServers
: null;
if (!list || list.length === 0) return null;
return list.map((v) => String(v)).filter(Boolean);
} catch (e) {
(logger || console).warn(
{ component: 'mikrotik-backup', err: e && e.message },
'Failed to read mikrotikBackupServers from ui-settings',
);
return null;
}
}
async function fetchRouterExport(client) {
// API не возвращает текст /export напрямую — экспортируем в файл, читаем, удаляем
return fetchExportViaFile(client);
}
async function runBackupOnce(logger) {
const log = logger || console;
try {
const allServers = await readServersFromS3();
const uiIds = await getServerIdsFromUiSettings(log);
const envIds = parseServerIds(process.env.MIKROTIK_BACKUP_SERVERS);
const wantedIds = (uiIds && uiIds.length > 0) ? uiIds : envIds;
const jumphosts = (allServers || []).filter((s) => {
if (!s || String(s.type || '').toLowerCase() !== 'jumphost') return false;
if (!wantedIds) return true;
const id = s.id || s.dns || s.ip;
return id && wantedIds.includes(String(id));
});
if (jumphosts.length === 0) {
log.info({ component: 'mikrotik-backup' }, 'No jumphost servers found for backup');
return;
}
for (const server of jumphosts) {
const id = server.id || server.dns || server.ip || 'unknown';
try {
const host = server.mikrotikHost || server.ip || server.dns;
const port = server.mikrotikPort || 80;
const user = server.mikrotikUser || 'admin';
if (!host) {
log.warn({ component: 'mikrotik-backup', serverId: id }, 'Skip backup: no MikroTik host');
continue;
}
let password = '';
if (server.encryptedMikrotikPassword) {
try {
password = decrypt(server.encryptedMikrotikPassword);
} catch (e) {
log.error(
{ component: 'mikrotik-backup', serverId: id, err: e && e.message },
'Failed to decrypt MikroTik password, skipping backup',
);
continue;
}
} else {
log.warn(
{ component: 'mikrotik-backup', serverId: id },
'Skip backup: no encryptedMikrotikPassword configured',
);
continue;
}
const creds = {
host,
port: Number(port) || 80,
user,
password,
secure: false,
};
const client = createRosClient(creds);
log.info({ component: 'mikrotik-backup', serverId: id, host: creds.host }, 'Starting automatic backup via REST /export');
const configText = await fetchRouterExport(client);
if (!configText || !configText.trim()) {
log.warn({ component: 'mikrotik-backup', serverId: id }, 'Skip backup: empty export result');
continue;
}
const result = await saveBackupForServer(id, configText, {
source: 'scheduler',
comment: 'Automatic MikroTik backup (REST /export)',
});
log.info(
{ component: 'mikrotik-backup', serverId: id, key: result.key },
'Automatic backup saved to S3',
);
} catch (err) {
log.error(
{ component: 'mikrotik-backup', serverId: id, err: err && err.message },
'Failed to create automatic backup',
);
}
}
} catch (err) {
const msg = err && err.message ? err.message : String(err);
(logger || console).error({ component: 'mikrotik-backup', err: msg }, 'Backup run failed');
}
}
function initMikrotikBackupScheduler(logger) {
const enabledEnv = String(process.env.MIKROTIK_BACKUP_ENABLED || 'true').toLowerCase();
const enabled = enabledEnv !== 'false' && enabledEnv !== '0' && enabledEnv !== 'off';
const log = logger || console;
if (!enabled) {
log.info({ component: 'mikrotik-backup' }, 'Automatic MikroTik backup scheduler is disabled');
return;
}
const intervalMin =
Number(process.env.MIKROTIK_BACKUP_INTERVAL_MINUTES || DEFAULT_INTERVAL_MIN) || DEFAULT_INTERVAL_MIN;
const intervalMs = Math.max(5, intervalMin) * 60 * 1000;
const intervalMinutes = intervalMs / 60000;
log.info(
{
component: 'mikrotik-backup',
intervalMinutes,
nextRunInMinutes: intervalMinutes,
servers: process.env.MIKROTIK_BACKUP_SERVERS || 'all jumphost',
},
'Automatic MikroTik backup scheduler started (no backup on startup, first run in %d min)',
intervalMinutes,
);
// Только по расписанию — без бэкапа при перезапуске контейнера
setInterval(() => {
runBackupOnce(log);
}, intervalMs);
}
module.exports = {
initMikrotikBackupScheduler,
runBackupOnce,
};