feat(MikrotikConfig): enhance MikroTik configuration generation with support for 'text' and 'json' formats; add new API endpoints for interface and recursive route generation
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m37s

This commit is contained in:
2026-02-03 17:46:30 +07:00
parent 5b96325184
commit 0ea0f376a6
7 changed files with 644 additions and 152 deletions
+5 -2
View File
@@ -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` — список серверов.
+18 -6
View File
@@ -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 = {
+169
View File
@@ -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,
};
+6
View File
@@ -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;
+15 -2
View File
@@ -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`;
+404
View File
@@ -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<Array>}
*/
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<Array>}
*/
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,
};
+27 -142
View File
@@ -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 {