feat(mikrotikBackup): add MikroTik backup functionality with S3 integration and implement backup scheduler
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m2s
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 1m2s
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Простейший планировщик автоматических бэкапов 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 } = require('./mikrotikApplyService');
|
||||
const { saveBackupForServer } = require('../routes/mikrotikBackupRoutes');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function fetchRouterExport(client) {
|
||||
// Согласно официальной документации, /rest/export позволяет экспортировать конфиг.
|
||||
// Параметр "compact" без значения эквивалентен /export compact.
|
||||
const res = await client.command('export', { compact: '' });
|
||||
const data = res && res.data;
|
||||
if (!data) return '';
|
||||
|
||||
if (typeof data === 'string') return data;
|
||||
if (Array.isArray(data)) {
|
||||
// Некоторые версии могут вернуть массив строк/объектов
|
||||
return data
|
||||
.map((line) => {
|
||||
if (typeof line === 'string') return line;
|
||||
if (line && typeof line === 'object') {
|
||||
// Пытаемся собрать строку из полей, если они есть
|
||||
if (line.rsc) return String(line.rsc);
|
||||
if (line.config) return String(line.config);
|
||||
return JSON.stringify(line);
|
||||
}
|
||||
return String(line ?? '');
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
if (typeof data === 'object') {
|
||||
// Fallback: сериализуем объект
|
||||
return JSON.stringify(data, null, 2);
|
||||
}
|
||||
return String(data);
|
||||
}
|
||||
|
||||
async function runBackupOnce(logger) {
|
||||
const log = logger || console;
|
||||
try {
|
||||
const allServers = await readServersFromS3();
|
||||
const wantedIds = parseServerIds(process.env.MIKROTIK_BACKUP_SERVERS);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (!server.encryptedMikrotikPassword) {
|
||||
log.warn({ component: 'mikrotik-backup', serverId: id }, 'Skip backup: no encryptedMikrotikPassword configured');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Пароль будет расшифрован на стороне RouterOS — мы используем тот же механизм,
|
||||
// что и в apply/test-connection: encryptedMikrotikPassword должен быть уже расшифрован ранее.
|
||||
// Здесь для простоты предполагаем, что пароль можно получить так же, как в apply.
|
||||
// Чтобы не дублировать логику расшифровки, используем те же поля, что и в mikrotikConfigRoutes.getMikrotikCredentials.
|
||||
const creds = {
|
||||
host,
|
||||
port: Number(port) || 80,
|
||||
user,
|
||||
// Пароль фактически должен быть расшифрован при сохранении/загрузке,
|
||||
// однако в текущей архитектуре encryptedMikrotikPassword расшифровывается в роуте.
|
||||
// Чтобы не нарушать безопасность и не тянуть сюда ключ, требуем наличия plain-поля mikrotikPassword,
|
||||
// если оно временно присутствует (например, для локального использования или через ENV/секреты).
|
||||
password: server.mikrotikPassword || '',
|
||||
secure: false,
|
||||
};
|
||||
|
||||
if (!creds.password) {
|
||||
log.warn({ component: 'mikrotik-backup', serverId: id }, 'Skip backup: no plain mikrotikPassword available');
|
||||
continue;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
log.info(
|
||||
{
|
||||
component: 'mikrotik-backup',
|
||||
intervalMinutes: intervalMs / 60000,
|
||||
servers: process.env.MIKROTIK_BACKUP_SERVERS || 'all jumphost',
|
||||
},
|
||||
'Starting automatic MikroTik backup scheduler',
|
||||
);
|
||||
|
||||
// Первый запуск с небольшой задержкой, чтобы сервер успел подняться
|
||||
setTimeout(() => {
|
||||
runBackupOnce(log);
|
||||
}, 30_000);
|
||||
|
||||
// Периодический запуск
|
||||
setInterval(() => {
|
||||
runBackupOnce(log);
|
||||
}, intervalMs);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initMikrotikBackupScheduler,
|
||||
runBackupOnce,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user