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
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m37s
This commit is contained in:
@@ -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` — список серверов.
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user