diff --git a/README.md b/README.md index f574347..ec02715 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ BGP_BACKGROUND_URL=http://77.232.38.173:8080/api/update_bgp/background?api_key=d ### Фильтры и конфигурации - GET `/api/filters` / POST `/api/filters` — фильтры для всех серверов. -- GET `/api/filters/generate-config` — сгенерировать конфиг MikroTik из `filters.json`. +- GET `/api/filters/generate-config?format=text|json` — сгенерировать конфиг MikroTik из `filters.json`. - POST `/api/filters/export-config` — сохранить сгенерированный конфиг в S3 (`mikrotik-frouting-config.txt`). - GET `/api/server-configs` / POST `/api/server-configs` — список серверов (id, name, ...). @@ -93,7 +93,10 @@ BGP_BACKGROUND_URL=http://77.232.38.173:8080/api/update_bgp/background?api_key=d - GET `/api/server-filters/:serverId` — фильтры сервера. - POST `/api/server-filters/:serverId` — сохранить фильтры сервера. -- POST `/api/server-filters/generate-config` — сгенерировать конфиг MikroTik на лету из переданных `{ filters }`. +- POST `/api/server-filters/generate-config` — сгенерировать конфиг MikroTik на лету из переданных `{ filters, format?: 'text'|'json' }`. +- POST `/api/mikrotik/generate` — сгенерировать конфиг интерфейсов и маршрутов. Body: `{ format?: 'text'|'json', type?: 'interfaces'|'recursive'|'all', serverId?: string, config?, servers? }`. Возвращает `{ blocks }` — массив блоков с полем `code` (text) или `operations` (json). +- GET `/api/mikrotik/generate-interfaces?format=text|json&serverId=` — только интерфейсы. +- GET `/api/mikrotik/generate-recursive-routes?format=text|json&serverId=` — только рекурсивные маршруты. ### Прочее - GET `/api/servers` / POST `/api/servers` — список серверов. diff --git a/backend/routes/filtersRoutes.js b/backend/routes/filtersRoutes.js index 5fa74a0..cbb4913 100644 --- a/backend/routes/filtersRoutes.js +++ b/backend/routes/filtersRoutes.js @@ -9,13 +9,17 @@ const { streamToString } = require('../services/s3Service'); const { generateMikrotikConfig } = require('../utils/mikrotikConfig'); const { getS3JsonWithHeaders } = require('../utils/s3Helpers'); -// GET /api/filters/generate-config +// GET /api/filters/generate-config?format=text|json async function generateConfig(req, res) { try { + const format = (req.query.format || 'text').toLowerCase(); + if (!['text', 'json'].includes(format)) { + return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST'); + } await getS3JsonWithHeaders('filters.json', req, res, { transform: (filters) => { - const config = generateMikrotikConfig(filters); - return { config }; + const result = generateMikrotikConfig(filters, { format }); + return format === 'json' ? result : { config: result }; }, defaultValue: [], }); @@ -54,10 +58,18 @@ async function exportConfig(req, res) { } // POST /api/server-filters/generate-config +// Body: { filters, format?: 'text'|'json' } async function generateServerFilterConfig(req, res) { - const { filters } = req.body; - const config = generateMikrotikConfig(filters); - res.json({ config }); + const { filters, format = 'text' } = req.body || {}; + const fmt = String(format || 'text').toLowerCase(); + if (!['text', 'json'].includes(fmt)) { + return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST'); + } + const result = generateMikrotikConfig(Array.isArray(filters) ? filters : [], { format: fmt }); + if (fmt === 'json') { + return res.json(result); + } + res.json({ config: result }); } module.exports = { diff --git a/backend/routes/mikrotikConfigRoutes.js b/backend/routes/mikrotikConfigRoutes.js new file mode 100644 index 0000000..45bd5cb --- /dev/null +++ b/backend/routes/mikrotikConfigRoutes.js @@ -0,0 +1,169 @@ +/** + * Роуты для генерации конфигурации MikroTik (интерфейсы, маршруты) + * Поддерживает форматы: text (как раньше) и json (структурированные операции для API) + */ + +const { GetObjectCommand } = require('@aws-sdk/client-s3'); +const { s3, BUCKET_NAME, streamToString } = require('../services/s3Service'); +const { sendError } = require('../middleware/errorHandler'); +const { decrypt } = require('../utils/encryption'); +const { + buildMikrotikInterfaceBlocks, + buildMikrotikRecursiveRoutes, + buildMikrotikConfig, +} = require('../utils/mikrotikInterfaceGenerator'); +const { readS3TextObject } = require('../services/s3Service'); + +async function fetchJsonFromS3(key, defaultValue = null) { + try { + const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: key })); + const body = await streamToString(data.Body); + const parsed = JSON.parse(body || 'null'); + return parsed ?? defaultValue; + } catch (e) { + if (e?.name === 'NoSuchKey' || e?.$metadata?.httpStatusCode === 404) { + return defaultValue; + } + throw e; + } +} + +async function fetchIpsecPasswordMap(passwordIds) { + const ids = [...new Set((passwordIds || []).filter(Boolean))]; + if (ids.length === 0) return {}; + + const data = await readS3TextObject('network-config/ipsec-passwords.json').catch(() => ({ body: '[]' })); + let passwords = []; + try { + passwords = JSON.parse(data.body || '[]'); + if (!Array.isArray(passwords)) passwords = []; + } catch { + passwords = []; + } + + const map = {}; + for (const p of passwords) { + if (ids.includes(p.id) && p.encryptedPassword) { + try { + map[p.id] = decrypt(p.encryptedPassword) || ''; + } catch { + map[p.id] = ''; + } + } + } + return map; +} + +/** + * POST /api/mikrotik/generate + * Body: { format?: 'text'|'json', serverId?: string, type?: 'interfaces'|'recursive'|'all', config?: object, servers?: array } + * Если config/servers не переданы — загружаются из S3 + */ +async function generateMikrotikConfig(req, res) { + try { + const { format = 'text', serverId, type = 'all', config: bodyConfig, servers: bodyServers } = req.body || {}; + + if (!['text', 'json'].includes(format)) { + return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST'); + } + + let config = bodyConfig; + let servers = bodyServers; + + if (!config) { + config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [], ipPools: [] }); + if (!config || typeof config !== 'object') config = { gateways: [], tunnelInterfaces: [], ipPools: [] }; + } + + if (!Array.isArray(servers)) { + servers = await fetchJsonFromS3('servers.json', []); + if (!Array.isArray(servers)) servers = []; + } + + const passwordIds = (config.tunnelInterfaces || []) + .filter(i => i.ipsecPasswordId && String(i.ipsecPasswordId).trim() !== '') + .map(i => i.ipsecPasswordId.trim()); + const passwordMap = await fetchIpsecPasswordMap(passwordIds); + + const options = { format, serverId }; + const includeInterfaces = type === 'interfaces' || type === 'all'; + const includeRecursive = type === 'recursive' || type === 'all'; + + const blocks = await buildMikrotikConfig(config, servers, passwordMap, { + ...options, + includeInterfaces, + includeRecursive, + }); + + res.json({ blocks }); + } catch (error) { + console.error('Error generating MikroTik config:', error); + return sendError(res, 500, error.message || 'Error generating configuration', 'E_SERVER'); + } +} + +/** + * 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 || null; + + if (!['text', 'json'].includes(format)) { + return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST'); + } + + const config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [] }); + const servers = await fetchJsonFromS3('servers.json', []); + const passwordIds = (config.tunnelInterfaces || []) + .filter(i => i.ipsecPasswordId && String(i.ipsecPasswordId).trim() !== '') + .map(i => i.ipsecPasswordId.trim()); + const passwordMap = await fetchIpsecPasswordMap(passwordIds); + + const blocks = await buildMikrotikInterfaceBlocks(config, servers, passwordMap, { + format, + serverId: serverId || undefined, + }); + + res.json({ blocks }); + } catch (error) { + console.error('Error generating interfaces:', error); + return sendError(res, 500, error.message || 'Error generating interfaces', 'E_SERVER'); + } +} + +/** + * 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 || null; + + if (!['text', 'json'].includes(format)) { + return sendError(res, 400, 'format must be "text" or "json"', 'E_BAD_REQUEST'); + } + + const config = await fetchJsonFromS3('network-config.json', { gateways: [], tunnelInterfaces: [] }); + const servers = await fetchJsonFromS3('servers.json', []); + + const blocks = await buildMikrotikRecursiveRoutes(config, servers, { + format, + serverId: serverId || undefined, + }); + + res.json({ blocks }); + } catch (error) { + console.error('Error generating recursive routes:', error); + return sendError(res, 500, error.message || 'Error generating routes', 'E_SERVER'); + } +} + +module.exports = { + generateMikrotikConfig, + generateInterfaces, + generateRecursiveRoutes, +}; diff --git a/backend/server.js b/backend/server.js index c8da7dd..da035ba 100644 --- a/backend/server.js +++ b/backend/server.js @@ -27,6 +27,7 @@ const filtersRoutes = require('./routes/filtersRoutes'); const serverConfigsRoutes = require('./routes/serverConfigsRoutes'); const miscRoutes = require('./routes/miscRoutes'); const ipsecPasswordsRoutes = require('./routes/ipsecPasswordsRoutes'); +const mikrotikConfigRoutes = require('./routes/mikrotikConfigRoutes'); const app = express(); const port = Number(process.env.PORT) || 3001; @@ -485,6 +486,11 @@ app.post('/api/ipsec-passwords', writeLimiter, ipsecPasswordsRoutes.createIpsecP app.put('/api/ipsec-passwords/:id', writeLimiter, ipsecPasswordsRoutes.updateIpsecPassword); app.delete('/api/ipsec-passwords/:id', writeLimiter, ipsecPasswordsRoutes.deleteIpsecPassword); +// === MIKROTIK CONFIG GENERATION (text + json) === +app.post('/api/mikrotik/generate', mikrotikConfigRoutes.generateMikrotikConfig); +app.get('/api/mikrotik/generate-interfaces', mikrotikConfigRoutes.generateInterfaces); +app.get('/api/mikrotik/generate-recursive-routes', mikrotikConfigRoutes.generateRecursiveRoutes); + // === MIKROTIK VALIDATION === app.post('/api/mikrotik/validate', async (req, res) => { const { config } = req.body; diff --git a/backend/utils/mikrotikConfig.js b/backend/utils/mikrotikConfig.js index 38d1001..e127f4f 100644 --- a/backend/utils/mikrotikConfig.js +++ b/backend/utils/mikrotikConfig.js @@ -1,6 +1,7 @@ /** * Утилиты для генерации конфигурации MikroTik * Устраняют дублирование кода генерации конфигурации + * Поддерживает форматы: 'text' (как раньше) и 'json' (структурированные данные для API) */ const { buildNestedGatewayBlocks } = require('./helpers'); @@ -8,10 +9,14 @@ const { buildNestedGatewayBlocks } = require('./helpers'); /** * Генерация конфигурации MikroTik из массива фильтров * @param {Array} filters - Массив фильтров с полями {community, gateway} - * @returns {string} Сгенерированная конфигурация + * @param {object} options - { format: 'text'|'json' } + * @returns {string|object} Сгенерированная конфигурация (строка или объект) */ -function generateMikrotikConfig(filters) { +function generateMikrotikConfig(filters, options = {}) { + const { format = 'text' } = options; + if (!Array.isArray(filters) || filters.length === 0) { + if (format === 'json') return { gatewayGroups: {}, config: null }; return '// No filters to generate configuration'; } @@ -24,6 +29,14 @@ function generateMikrotikConfig(filters) { gatewayGroups[filter.gateway].push(filter.community); }); + if (format === 'json') { + return { + gatewayGroups, + filterName: 'bgp-in-tmp', + generatedAt: new Date().toISOString(), + }; + } + let config = '// Frouting filter configuration for MikroTik 7.14+\n'; config += '// Generated automatically\n'; config += `// Date: ${new Date().toISOString()}\n\n`; diff --git a/backend/utils/mikrotikInterfaceGenerator.js b/backend/utils/mikrotikInterfaceGenerator.js new file mode 100644 index 0000000..575e9da --- /dev/null +++ b/backend/utils/mikrotikInterfaceGenerator.js @@ -0,0 +1,404 @@ +/** + * Генератор конфигурации MikroTik для интерфейсов и маршрутов + * Поддерживает два формата вывода: 'text' (как раньше) и 'json' (структурированные операции для API) + */ + +/** + * @param {string} serverId + * @param {Array} servers + * @returns {object|null} + */ +function getServerInfo(serverId, servers) { + if (!serverId || !Array.isArray(servers)) return null; + return servers.find(s => s.id === serverId || s.ip === serverId || s.dns === serverId) || null; +} + +/** + * @param {string} parentId + * @param {object} config + * @param {Array} templateGateways + * @returns {object|null} + */ +function getParentGateway(parentId, config, templateGateways = null) { + if (!parentId || !config) return null; + + if (Array.isArray(templateGateways)) { + const g = templateGateways.find(x => x.id === parentId); + if (g) return { parentType: 'gateway', ...g, ip: g.ip }; + } + + const gateway = config.gateways?.find(g => g.id === parentId); + if (gateway) return { parentType: 'gateway', ...gateway, ip: gateway.ip }; + + const iface = config.tunnelInterfaces?.find(i => i.id === parentId); + if (iface) { + return { + parentType: 'interface', + ...iface, + remoteIp: iface.remoteIp, + localIp: iface.localIp, + name: iface.name, + interfaceType: iface.type, + }; + } + return null; +} + +/** + * Сериализация одной операции в строку RouterOS + * @param {object} op - { path, action, params } + * @returns {string} + */ +function operationToRouterOSLine(op) { + const { path, action, params } = op; + if (!path || !action) return ''; + const pathStr = path.replace(/^\//, '').replace(/\//g, ' '); + const fullPath = pathStr ? `/${pathStr}` : ''; + const parts = (params && typeof params === 'object') + ? Object.entries(params) + .filter(([, v]) => v != null && v !== '') + .map(([k, v]) => { + const val = String(v); + if (val.includes(' ') || val.includes('"')) return `${k}="${val}"`; + return `${k}=${val}`; + }) + : []; + return `${fullPath} ${action} ${parts.join(' ')}`.trim(); +} + +/** + * Сериализация массива операций в текст + * @param {Array} operations + * @returns {string} + */ +function serializeOperationsToText(operations) { + if (!Array.isArray(operations)) return ''; + return operations.map(op => operationToRouterOSLine(op)).filter(Boolean).join('\n'); +} + +/** + * Построение блоков MikroTik для интерфейсов + * @param {object} config - { tunnelInterfaces, gateways } + * @param {Array} servers + * @param {object} passwordMap - { [ipsecPasswordId]: decryptedPassword } + * @param {object} options - { format: 'text'|'json', serverId?: string } + * @returns {Promise} + */ +async function buildMikrotikInterfaceBlocks(config, servers, passwordMap, options = {}) { + const { format = 'text', serverId: filterServerId } = options; + const interfaces = (config.tunnelInterfaces || []).filter( + i => i.serverId && i.serverId2 && i.localIp && i.remoteIp + ); + + if (interfaces.length === 0) return []; + + const interfacesByServer = {}; + for (const iface of interfaces) { + const server1 = getServerInfo(iface.serverId, servers); + const server2 = getServerInfo(iface.serverId2, servers); + if (!server1 || !server2) continue; + + const server1Name = server1.dns || server1.ip || iface.serverId; + const server2Name = server2.dns || server2.ip || iface.serverId2; + const interfaceName1 = iface.name || `${iface.type}-tunnel`; + const interfaceName2 = iface.name2 || iface.name || `${iface.type}-tunnel`; + + const ipsecPassword = (iface.ipsecPasswordId && passwordMap[iface.ipsecPasswordId]) || null; + + if (!interfacesByServer[server1Name]) { + interfacesByServer[server1Name] = { serverInfo: server1, interfaces: [] }; + } + interfacesByServer[server1Name].interfaces.push({ + interfaceName: interfaceName1, + localIp: iface.localIp, + remoteIp: iface.remoteIp, + type: iface.type, + server2Name, + server2Info: server2, + ipsecPassword, + mtu: iface.mtu || null, + }); + + if (!interfacesByServer[server2Name]) { + interfacesByServer[server2Name] = { serverInfo: server2, interfaces: [] }; + } + interfacesByServer[server2Name].interfaces.push({ + interfaceName: interfaceName2, + localIp: iface.remoteIp, + remoteIp: iface.localIp, + type: iface.type, + server2Name: server1Name, + server2Info: server1, + ipsecPassword, + mtu: iface.mtu || null, + }); + } + + const sortedServers = Object.entries(interfacesByServer).sort(([a], [b]) => a.localeCompare(b)); + const blocks = []; + + for (const [serverName, serverData] of sortedServers) { + if (filterServerId) { + const s = getServerInfo(filterServerId, servers); + const matchName = s ? (s.dns || s.ip || filterServerId) : filterServerId; + if (serverName !== matchName && serverData.serverInfo?.id !== filterServerId) continue; + } + + const server = serverData.serverInfo; + const isHomeRouter = server.type === 'home'; + const hasGreInterfaces = serverData.interfaces.some(i => i.type === 'GRE'); + + const operations = []; + let code = ''; + + if (format === 'text') { + code += `# Настройка IP адресов интерфейсов для сервера: ${serverName}\n`; + if (server.provider) code += `# Провайдер: ${server.provider}\n`; + if (server.country) code += `# Страна: ${server.country}\n`; + if (server.ip && server.ip !== serverName) code += `# IP: ${server.ip}\n`; + code += '\n'; + } + + if (isHomeRouter && hasGreInterfaces) { + if (format === 'text') { + code += `# Создание interface list для GRE туннелей (если не существует)\n`; + code += `:if ([/interface list find name=GREs] = "") do={ /interface list add name=GREs comment="GRE tunnels list" }\n\n`; + } else { + operations.push({ + path: '/interface/list', + action: 'add', + params: { name: 'GREs', comment: 'GRE tunnels list' }, + meta: { ensureExists: true }, + }); + } + } + + for (let i = 0; i < serverData.interfaces.length; i++) { + const iface = serverData.interfaces[i]; + const remoteAddress = iface.server2Info?.dns || iface.server2Info?.ip || iface.server2Name; + const hasIpsecPassword = iface.ipsecPassword && String(iface.ipsecPassword).trim() !== ''; + const localAddressParam = isHomeRouter && server.ip ? server.ip : null; + + if (format === 'text') { + code += `# Интерфейс ${i + 1}: ${iface.interfaceName} (${iface.type})\n`; + code += `# Связь с сервером: ${iface.server2Name}\n`; + code += `# Local IP: ${iface.localIp}\n`; + code += `# Remote IP: ${iface.remoteIp}\n`; + } + + if (iface.type === 'GRE') { + const greParams = { + name: iface.interfaceName, + 'remote-address': remoteAddress, + 'keepalive': '10s', + 'allow-fast-path': 'no', + }; + if (localAddressParam) greParams['local-address'] = localAddressParam; + if (iface.mtu) greParams.mtu = String(iface.mtu); + if (hasIpsecPassword) greParams['ipsec-secret'] = iface.ipsecPassword; + + const op = { path: '/interface/gre', action: 'add', params: greParams }; + operations.push(op); + + if (format === 'text') { + if (hasIpsecPassword) code += `# Создание GRE туннеля с IPSec\n`; + else code += `# Создание GRE туннеля (без IPSec)\n`; + code += operationToRouterOSLine(op) + '\n'; + } + + if (isHomeRouter) { + const listOp = { + path: '/interface/list/member', + action: 'add', + params: { list: 'GREs', interface: iface.interfaceName, comment: `GRE tunnel to ${iface.server2Name}` }, + }; + operations.push(listOp); + if (format === 'text') code += operationToRouterOSLine(listOp) + '\n'; + } + } + + const addrOp = { + path: '/ip/address', + action: 'add', + params: { + address: `${iface.localIp}/30`, + interface: iface.interfaceName, + comment: `Interface: ${iface.interfaceName} (${iface.type}) to ${iface.server2Name}`, + }, + }; + operations.push(addrOp); + if (format === 'text') code += operationToRouterOSLine(addrOp) + '\n'; + } + + if (format === 'text') { + code += '# Проверка настроенных адресов:\n'; + code += '# /ip address print\n'; + } + + blocks.push({ + type: 'interface-addresses', + serverName, + server, + ...(format === 'text' ? { code } : { operations }), + }); + } + + return blocks; +} + +/** + * Построение блоков для рекурсивных маршрутов + * @param {object} config + * @param {Array} servers + * @param {object} options - { format: 'text'|'json', serverId?: string } + * @returns {Promise} + */ +async function buildMikrotikRecursiveRoutes(config, servers, options = {}) { + const { format = 'text', serverId: filterServerId } = options; + const recursiveGateways = (config.gateways || []).filter(g => g.type === 'recursive'); + const blocks = []; + + if (recursiveGateways.length === 0) return []; + + const gatewaysByServer = {}; + for (const gw of recursiveGateways) { + const sid = gw.serverId || '__unassigned__'; + if (!gatewaysByServer[sid]) gatewaysByServer[sid] = []; + gatewaysByServer[sid].push(gw); + } + + for (const [sid, gateways] of Object.entries(gatewaysByServer)) { + const server = getServerInfo(sid, servers); + const serverName = server?.dns || server?.ip || sid; + + if (filterServerId && sid !== filterServerId) continue; + + const operations = []; + let code = ''; + + if (format === 'text') { + code += `# Рекурсивные маршруты для сервера: ${serverName}\n`; + if (server?.provider) code += `# Провайдер: ${server.provider}\n`; + if (server?.country) code += `# Страна: ${server.country}\n`; + code += '\n'; + code += '# Удаление существующих рекурсивных маршрутов (опционально)\n'; + code += '/ip route remove [find comment~"Recursive"]\n\n'; + } else { + operations.push({ + path: '/ip/route', + action: 'remove', + params: {}, + meta: { findComment: 'Recursive' }, + }); + } + + let validCount = 0; + + for (const gw of gateways) { + const parentGatewaysList = (gw.parentGateways && gw.parentGateways.length > 0) + ? gw.parentGateways + : (gw.parentGatewayId ? [{ id: gw.parentGatewayId, distance: undefined }] : []); + + if (parentGatewaysList.length === 0 || !gw.ip) continue; + + for (const parentRef of parentGatewaysList) { + const parent = getParentGateway(parentRef.id, config); + if (!parent) continue; + + const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip; + if (!parentIp) continue; + + validCount++; + const gatewayValue = parent.parentType === 'interface' + ? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}` + : parentIp; + const distance = parentRef.distance; + const comment = gw.description + ? `Recursive: ${gw.description} -> ${parentIp}` + : `Recursive: ${gw.ip} -> ${parentIp}`; + + const routeParams = { + 'dst-address': `${gw.ip}/32`, + gateway: gatewayValue, + comment, + }; + if (distance != null) routeParams.distance = String(distance); + + const op = { path: '/ip/route', action: 'add', params: routeParams }; + operations.push(op); + + if (format === 'text') { + code += `# Рекурсивный gateway: ${gw.ip}\n`; + code += operationToRouterOSLine(op) + '\n'; + } + + if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) { + const defaultParams = { + 'dst-address': '0.0.0.0/0', + gateway: gatewayValue, + comment: `Recursive: ${gw.description || gw.ip} (default)`, + }; + if (distance != null) defaultParams.distance = String(distance); + const defaultOp = { path: '/ip/route', action: 'add', params: defaultParams }; + operations.push(defaultOp); + if (format === 'text') code += operationToRouterOSLine(defaultOp) + '\n'; + } + } + } + + if (validCount > 0) { + if (format === 'text') { + code += '# Проверка: /ip route print where comment~"Recursive"\n'; + } + blocks.push({ + type: 'recursive-routes', + serverName, + server, + ...(format === 'text' ? { code } : { operations }), + }); + } + } + + return blocks; +} + +/** + * Комбинированная генерация: интерфейсы + рекурсивные маршруты + * @param {object} config + * @param {Array} servers + * @param {object} passwordMap + * @param {object} options - { format: 'text'|'json', serverId?: string, includeInterfaces?: boolean, includeRecursive?: boolean } + */ +async function buildMikrotikConfig(config, servers, passwordMap, options = {}) { + const { + format = 'text', + serverId, + includeInterfaces = true, + includeRecursive = true, + } = options; + + const opts = { format, serverId }; + const blocks = []; + + if (includeInterfaces) { + const ifBlocks = await buildMikrotikInterfaceBlocks(config, servers, passwordMap, opts); + blocks.push(...ifBlocks); + } + + if (includeRecursive) { + const recBlocks = await buildMikrotikRecursiveRoutes(config, servers, opts); + blocks.push(...recBlocks); + } + + return blocks; +} + +module.exports = { + getServerInfo, + getParentGateway, + operationToRouterOSLine, + serializeOperationsToText, + buildMikrotikInterfaceBlocks, + buildMikrotikRecursiveRoutes, + buildMikrotikConfig, +}; diff --git a/frontend/src/NetworkConfigManager.jsx b/frontend/src/NetworkConfigManager.jsx index 9a90bac..9bc9cc1 100644 --- a/frontend/src/NetworkConfigManager.jsx +++ b/frontend/src/NetworkConfigManager.jsx @@ -2146,8 +2146,14 @@ function NetworkConfigManager() { const handleGenerateMikrotikCode = async () => { setLoading(true); try { - const code = await generateMikrotikRecursiveRoutes(); - setGeneratedMikrotikCode(code); + const { data } = await api.post('/mikrotik/generate', { + format: 'text', + type: 'all', + config, + servers, + }); + const blocks = data?.blocks || []; + setGeneratedMikrotikCode(blocks); setMikrotikCodeModalOpen(true); } catch (error) { console.error('Error generating MikroTik code:', error); @@ -2167,156 +2173,35 @@ function NetworkConfigManager() { setLoading(true); try { if (type === 'interface') { - // Получаем все интерфейсы для этого сервера (где сервер участвует как первый или второй) - // Фильтруем только те, где оба сервера указаны и есть IP адреса - const serverInterfaces = (config.tunnelInterfaces || []).filter(iface => - (iface.serverId === serverId || iface.serverId2 === serverId) && - iface.serverId && iface.serverId2 && iface.localIp && iface.remoteIp - ); - - if (serverInterfaces.length === 0) { + const { data } = await api.post('/mikrotik/generate', { + format: 'text', + type: 'interfaces', + serverId, + config, + servers, + }); + const blocks = data?.blocks || []; + if (!blocks.length) { notify.error('Нет интерфейсов с полной конфигурацией для этого сервера'); setLoading(false); return; } - - const blocks = await generateMikrotikInterfaceAddressesForSelection(serverInterfaces); - if (!blocks || blocks.length === 0) { - notify.error('Нет данных для генерации кода MikroTik для этого сервера'); - setLoading(false); - return; - } - - // Фильтруем блоки, чтобы показать только те, что относятся к данному серверу - const server = getServerInfo(serverId); - const serverName = server?.dns || server?.ip || serverId; - const filteredBlocks = blocks.filter(block => - block.serverName === serverName || block.server?.id === serverId - ); - - if (filteredBlocks.length === 0) { - notify.error('Не удалось сгенерировать код для этого сервера'); - setLoading(false); - return; - } - - setGeneratedMikrotikCode(filteredBlocks); + setGeneratedMikrotikCode(blocks); setMikrotikCodeModalOpen(true); } else if (type === 'gateway') { - // Получаем все gateways для этого сервера - const serverGateways = (config.gateways || []).filter(gw => gw.serverId === serverId); - - if (serverGateways.length === 0) { - notify.error('Нет gateways для этого сервера'); - setLoading(false); - return; - } - - // Генерируем код для рекурсивных маршрутов этого сервера - const recursiveGateways = serverGateways.filter(gw => gw.type === 'recursive'); - - if (recursiveGateways.length === 0) { + const { data } = await api.post('/mikrotik/generate', { + format: 'text', + type: 'recursive', + serverId, + config, + servers, + }); + const blocks = data?.blocks || []; + if (!blocks.length) { notify.error('Нет рекурсивных gateways для генерации кода'); setLoading(false); return; } - - const blocks = []; - const server = getServerInfo(serverId); - const serverName = server?.dns || server?.ip || serverId; - - // Генерируем код для каждого рекурсивного gateway - recursiveGateways.forEach(gw => { - if (!gw.ip) return; - - // Поддержка старого формата (parentGatewayId) и нового (parentGateways) - const parentGatewaysList = gw.parentGateways && Array.isArray(gw.parentGateways) && gw.parentGateways.length > 0 - ? gw.parentGateways - : (gw.parentGatewayId ? [{ id: gw.parentGatewayId, distance: undefined }] : []); - - if (parentGatewaysList.length === 0) return; - - let code = `# Рекурсивный маршрут для gateway: ${gw.ip}\n`; - if (server?.provider) { - code += `# Провайдер: ${server.provider}\n`; - } - if (server?.country) { - code += `# Страна: ${server.country}\n`; - } - code += '\n'; - - let validRoutesCount = 0; - - parentGatewaysList.forEach((parentRef) => { - const parent = getParentGateway(parentRef.id); - - if (!parent) { - code += `# Родительский gateway/интерфейс "${parentRef.id}" не найден, пропущен\n`; - return; - } - - // Определяем IP родителя в зависимости от типа - const parentIp = parent.parentType === 'interface' ? parent.remoteIp : parent.ip; - - if (!parentIp) { - const parentType = parent.parentType === 'interface' ? 'интерфейс' : 'gateway'; - code += `# Родительский ${parentType} не имеет IP, пропущен\n`; - return; - } - - validRoutesCount++; - - // Создаём маршрут для рекурсивного gateway - const parentLabel = parent.parentType === 'interface' - ? `интерфейс ${parent.name || parent.interfaceType || 'interface'} (${parent.remoteIp})` - : `gateway ${parent.ip}`; - - const distanceSuffix = parentRef.distance !== undefined && parentRef.distance !== null ? ` (distance: ${parentRef.distance})` : ''; - code += `# Рекурсивный gateway: ${gw.ip}${distanceSuffix}\n`; - code += `# Родительский: ${parentLabel}\n`; - - // Формируем gateway: для интерфейсов добавляем имя через %, для gateway - только IP - const gatewayValue = parent.parentType === 'interface' - ? `${parentIp}%${parent.name || parent.interfaceType || 'interface'}` - : parentIp; - - // Создаём маршрут к IP рекурсивного gateway через родительский - const comment = gw.description - ? `Recursive: ${gw.description} -> ${parentIp}${distanceSuffix}` - : `Recursive: ${gw.ip} -> ${parentIp}${distanceSuffix}`; - - const distanceParam = parentRef.distance !== undefined && parentRef.distance !== null ? ` distance=${parentRef.distance}` : ''; - code += `/ip route add dst-address=${gw.ip}/32 gateway=${gatewayValue}${distanceParam} comment="${comment}"\n`; - - // Если у рекурсивного gateway есть описание с указанием на default route - if (gw.description && (gw.description.toLowerCase().includes('default') || gw.description.toLowerCase().includes('0.0.0.0'))) { - code += `# Дополнительный default route через рекурсивный gateway${distanceSuffix}\n`; - const defaultComment = gw.description ? `Recursive: ${gw.description} (default)${distanceSuffix}` : `Recursive: ${gw.ip} (default)${distanceSuffix}`; - code += `/ip route add dst-address=0.0.0.0/0 gateway=${gatewayValue}${distanceParam} comment="${defaultComment}"\n`; - } - code += '\n'; - }); - - code += '# Проверка созданных маршрутов:\n'; - code += '# /ip route print where comment~"Recursive"\n'; - code += `# Всего создано маршрутов: ${validRoutesCount}\n`; - - if (validRoutesCount > 0) { - blocks.push({ - type: 'recursive-routes', - serverName, - server, - code - }); - } - }); - - if (blocks.length === 0) { - notify.error('Не удалось сгенерировать код для gateways этого сервера'); - setLoading(false); - return; - } - setGeneratedMikrotikCode(blocks); setMikrotikCodeModalOpen(true); } else {