Files
router-lists-ui/backend/routes/mikrotikConfigRoutes.js
T
denozord 61c87e69ef
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m9s
fix(mikrotikConfigRoutes): enhance script execution logic to find script ID before running
2026-02-03 23:47:39 +07:00

322 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Роуты для генерации и применения конфигурации MikroTik
* Использует RouterOS REST API — требует RouterOS 7.1+ с www-ssl (443) или www (80)
*/
const { GetObjectCommand } = require('@aws-sdk/client-s3');
const { s3, BUCKET_NAME, streamToString, readS3TextObject } = require('../services/s3Service');
const { sendError, sendOk } = require('../middleware/errorHandler');
const { decrypt } = require('../utils/encryption');
const { readServersFromS3 } = require('./serversRoutes');
const {
buildMikrotikConfig,
buildMikrotikInterfaceBlocks,
buildMikrotikRecursiveRoutes,
} = require('../utils/mikrotikInterfaceGenerator');
const { createRosClient, applyBlock } = require('../services/mikrotikApplyService');
const IPSEC_PASSWORDS_KEY = 'network-config/ipsec-passwords.json';
const NETWORK_CONFIG_KEY = 'network-config.json';
/** Загрузить network-config из S3 */
async function loadNetworkConfig() {
try {
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: NETWORK_CONFIG_KEY }));
const body = await streamToString(data.Body);
const parsed = JSON.parse(body || '{}');
return parsed && typeof parsed === 'object' ? parsed : {};
} catch (e) {
if (e?.name === 'NoSuchKey' || e?.$metadata?.httpStatusCode === 404) return {};
throw e;
}
}
/** Загрузить и расшифровать IPSec пароли → { id: password } */
async function loadPasswordMap() {
const map = {};
try {
const data = await readS3TextObject(IPSEC_PASSWORDS_KEY).catch(() => ({ body: '[]' }));
const passwords = JSON.parse(data.body || '[]');
if (!Array.isArray(passwords)) return map;
for (const p of passwords) {
if (p.id && p.encryptedPassword) {
try {
map[p.id] = decrypt(p.encryptedPassword);
} catch (_) {
// пропускаем при ошибке расшифровки
}
}
}
} catch (_) {}
return map;
}
/** Получить MikroTik credentials из сервера (jumphost) */
function getMikrotikCredentials(server) {
if (!server || server.type !== 'jumphost') return null;
const host = server.mikrotikHost || server.ip || server.dns;
if (!host) return null;
const port = server.mikrotikPort;
// REST API: порт 80 (HTTP, www) или 443 (HTTPS, www-ssl). По умолчанию 80
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 };
}
/**
* POST /api/mikrotik/generate
* Body: { format?, type?, serverId?, config?, servers? }
*/
async function generateMikrotikConfig(req, res) {
try {
const { format = 'text', type = 'all', serverId, config: bodyConfig, servers: bodyServers } = req.body || {};
const fmt = String(format).toLowerCase();
const typ = String(type).toLowerCase();
let config = bodyConfig;
let servers = bodyServers;
let passwordMap = {};
if (!config) config = await loadNetworkConfig();
if (!Array.isArray(servers)) servers = await readServersFromS3();
if (typ !== 'recursive') passwordMap = await loadPasswordMap();
const opts = {
format: fmt === 'json' ? 'json' : 'text',
serverId: serverId || undefined,
includeInterfaces: typ === 'interfaces' || typ === 'all',
includeRecursive: typ === 'recursive' || typ === 'all',
};
const blocks = await buildMikrotikConfig(config, servers, passwordMap, opts);
return res.json({ blocks });
} catch (error) {
console.error('generateMikrotikConfig:', error);
return sendError(res, 500, error.message || 'Error generating config', 'E_GENERATE');
}
}
/**
* GET /api/mikrotik/generate-interfaces?format=text|json&serverId=
*/
async function generateInterfaces(req, res) {
try {
const format = (req.query.format || 'text').toLowerCase();
const serverId = req.query.serverId || undefined;
const config = await loadNetworkConfig();
const servers = await readServersFromS3();
const passwordMap = await loadPasswordMap();
const blocks = await buildMikrotikInterfaceBlocks(config, servers, passwordMap, {
format: format === 'json' ? 'json' : 'text',
serverId,
});
return res.json({ blocks });
} catch (error) {
console.error('generateInterfaces:', error);
return sendError(res, 500, error.message || 'Error generating interfaces', 'E_GENERATE');
}
}
/**
* GET /api/mikrotik/generate-recursive-routes?format=text|json&serverId=
*/
async function generateRecursiveRoutes(req, res) {
try {
const format = (req.query.format || 'text').toLowerCase();
const serverId = req.query.serverId || undefined;
const config = await loadNetworkConfig();
const servers = await readServersFromS3();
const blocks = await buildMikrotikRecursiveRoutes(config, servers, {
format: format === 'json' ? 'json' : 'text',
serverId,
});
return res.json({ blocks });
} catch (error) {
console.error('generateRecursiveRoutes:', error);
return sendError(res, 500, error.message || 'Error generating routes', 'E_GENERATE');
}
}
/**
* POST /api/mikrotik/test-connection
* Body: { serverId } или { host, port?, user?, password }
*/
async function testMikrotikConnection(req, res) {
try {
const { serverId, host, port, user, password } = req.body || {};
let creds;
if (serverId) {
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');
}
creds = getMikrotikCredentials(server);
if (!creds) {
return sendError(res, 400, 'MikroTik credentials not configured', 'E_CREDENTIALS');
}
} else if (host && user !== undefined) {
creds = {
host,
port: port || 80,
user,
password: password || '',
secure: false,
};
} else {
return sendError(res, 400, 'Provide serverId or (host, user, password)', 'E_BAD_REQUEST');
}
const client = createRosClient(creds);
await client.print('system/resource');
return sendOk(res, { ok: true, message: 'Connection successful' });
} catch (error) {
const msg = error.response?.data?.message || error.message || 'Connection failed';
const status = error.response?.status;
console.error('testMikrotikConnection:', error);
return sendError(res, status && status >= 400 ? status : 502, msg, 'E_CONNECTION');
}
}
/**
* POST /api/mikrotik/apply
* Body: { serverId, type?: 'interfaces'|'recursive'|'all', dryRun?: boolean }
*/
async function applyMikrotikConfig(req, res) {
try {
const { serverId, type = 'all', dryRun = true } = 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 config = await loadNetworkConfig();
const passwordMap = await loadPasswordMap();
const typ = String(type).toLowerCase();
const opts = {
format: 'json',
serverId,
includeInterfaces: typ === 'interfaces' || typ === 'all',
includeRecursive: typ === 'recursive' || typ === 'all',
};
const blocks = await buildMikrotikConfig(config, servers, passwordMap, opts);
const blocksWithOps = blocks.filter(b => Array.isArray(b.operations) && b.operations.length > 0);
if (blocksWithOps.length === 0) {
return res.json({
ok: true,
dryRun: !!dryRun,
summary: { created: 0, updated: 0, skipped: 0, errors: [] },
results: [],
message: 'No operations to apply',
});
}
const client = createRosClient(creds);
const allResults = [];
const mikrotikRequests = [];
for (const block of blocksWithOps) {
mikrotikRequests.push({
block: block.type || 'unknown',
serverName: block.serverName,
operationCount: block.operations?.length || 0,
});
const blockResults = await applyBlock(client, block, !!dryRun);
allResults.push({ block: block.type || block.serverName, results: blockResults });
}
const summary = {
created: allResults.flatMap(r => r.results).filter(r => r.status === 'created').length,
updated: allResults.flatMap(r => r.results).filter(r => r.status === 'updated').length,
skipped: allResults.flatMap(r => r.results).filter(r => r.status === 'skip' || r.status === 'skipped' || r.status === 'would_create' || r.status === 'would_update' || r.status === 'would_remove').length,
errors: allResults.flatMap(r => r.results).filter(r => r.status === 'error').map(r => r.error),
};
return res.json({
ok: summary.errors.length === 0,
dryRun: !!dryRun,
summary,
results: allResults,
mikrotikRequests,
});
} catch (error) {
console.error('applyMikrotikConfig:', error);
return sendError(res, 500, error.message || 'Error applying config', 'E_APPLY');
}
}
/**
* POST /api/mikrotik/run-script
* Body: { serverId, script?: string } — по умолчанию script=update_bgp_filter
*/
async function runScript(req, res) {
try {
const { serverId, script = 'update_bgp_filter' } = 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);
// Сначала находим скрипт по имени, затем запускаем по .id
const scripts = await client.print('system/script');
const list = Array.isArray(scripts?.data) ? scripts.data : (scripts?.data ? [scripts.data] : []);
const found = list.find(s => (s.name || s['.id']) === script);
const scriptId = found ? (found['.id'] || found.name) : script;
await client.command('system/script/run', { '.id': scriptId });
return sendOk(res, { ok: true, message: `Скрипт ${script} запущен` });
} catch (error) {
const msg = error.response?.data?.message || error.message || 'Ошибка запуска скрипта';
const status = error.response?.status;
console.error('runScript:', error);
return sendError(res, status && status >= 400 ? status : 502, msg, 'E_SCRIPT');
}
}
module.exports = {
generateMikrotikConfig,
generateInterfaces,
generateRecursiveRoutes,
testMikrotikConnection,
applyMikrotikConfig,
runScript,
};