/** * Роуты для генерации и применения конфигурации 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); // /rest/system/script/run — запуск скрипта по имени (number или .id) await client.command('system/script/run', { number: script }); 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, };