/** * Простейший планировщик автоматических бэкапов 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, };