feat(mikrotik-config): add OSPF interface templates management with loading and applying functionality
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m35s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m35s
This commit is contained in:
@@ -15,7 +15,7 @@ const {
|
||||
buildMikrotikRecursiveRoutes,
|
||||
getParentGateway,
|
||||
} = require('../utils/mikrotikInterfaceGenerator');
|
||||
const { createRosClient, applyBlock, rosPrint, rosAdd, rosRemove } = require('../services/mikrotikApplyService');
|
||||
const { createRosClient, applyBlock, rosPrint, rosAdd, rosSet, rosRemove } = require('../services/mikrotikApplyService');
|
||||
|
||||
const IPSEC_PASSWORDS_KEY = 'network-config/ipsec-passwords.json';
|
||||
const NETWORK_CONFIG_KEY = 'network-config.json';
|
||||
@@ -1474,6 +1474,203 @@ async function getAddressLists(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOspfInterfaceName(entry) {
|
||||
if (!entry || typeof entry !== 'object') return '';
|
||||
return String(entry.interfaces ?? entry.interface ?? '').trim();
|
||||
}
|
||||
|
||||
function parseOspfCost(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) return null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function sameOspfInterface(a, b) {
|
||||
return String(a || '').trim().toUpperCase() === String(b || '').trim().toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/mikrotik/ospf-interface-templates?serverId=
|
||||
* Возвращает текущие OSPF interface-template с HOME роутеров.
|
||||
*/
|
||||
async function getOspfInterfaceTemplates(req, res) {
|
||||
try {
|
||||
const requestedServerId = String(req.query?.serverId || '').trim();
|
||||
const servers = await readServersFromS3();
|
||||
const homeServers = servers.filter((s) => String(s?.type || '').toLowerCase() === 'home');
|
||||
|
||||
const targets = requestedServerId
|
||||
? homeServers.filter((s) => [s.id, s.ip, s.dns].map((x) => String(x || '')).includes(requestedServerId))
|
||||
: homeServers;
|
||||
|
||||
if (requestedServerId && targets.length === 0) {
|
||||
return sendError(res, 404, 'HOME server not found', 'E_NOT_FOUND');
|
||||
}
|
||||
|
||||
const results = [];
|
||||
const errors = [];
|
||||
|
||||
for (const server of targets) {
|
||||
const serverId = server.id || server.ip || server.dns;
|
||||
const creds = getMikrotikCredentials(server);
|
||||
if (!creds) {
|
||||
errors.push({
|
||||
serverId,
|
||||
serverLabel: server.dns || server.ip || serverId,
|
||||
error: 'MikroTik credentials not configured for this server',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = createRosClient(creds);
|
||||
const raw = await rosPrint(client, 'routing/ospf/interface-template');
|
||||
const templates = (Array.isArray(raw) ? raw : [])
|
||||
.map((item) => {
|
||||
const interfaceName = normalizeOspfInterfaceName(item);
|
||||
if (!interfaceName) return null;
|
||||
return {
|
||||
id: item['.id'] || null,
|
||||
interfaceName,
|
||||
cost: parseOspfCost(item.cost),
|
||||
area: item.area || '',
|
||||
networks: item.networks || '',
|
||||
networkType: item['type'] || item['network-type'] || '',
|
||||
disabled: String(item.disabled || '').toLowerCase() === 'true',
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
results.push({
|
||||
serverId,
|
||||
serverLabel: server.dns || server.ip || serverId,
|
||||
templates,
|
||||
});
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
serverId,
|
||||
serverLabel: server.dns || server.ip || serverId,
|
||||
error: error?.response?.data?.detail || error?.response?.data?.message || error.message || 'Failed to read OSPF templates',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
ok: errors.length === 0,
|
||||
results,
|
||||
errors,
|
||||
});
|
||||
} catch (error) {
|
||||
return sendError(res, 500, error.message || 'Failed to load OSPF templates', 'E_OSPF_LOAD');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/mikrotik/ospf-interface-templates/apply
|
||||
* Body: { servers: [{ serverId, templates: [{ interfaceName, cost }] }] }
|
||||
*/
|
||||
async function applyOspfInterfaceTemplates(req, res) {
|
||||
try {
|
||||
const payloadServers = Array.isArray(req.body?.servers) ? req.body.servers : [];
|
||||
if (payloadServers.length === 0) {
|
||||
return sendError(res, 400, 'servers array is required', 'E_BAD_REQUEST');
|
||||
}
|
||||
|
||||
const servers = await readServersFromS3();
|
||||
const results = [];
|
||||
const errors = [];
|
||||
const summary = { updated: 0, skipped: 0, missing: 0, invalid: 0 };
|
||||
|
||||
for (const payload of payloadServers) {
|
||||
const serverRef = String(payload?.serverId || '').trim();
|
||||
const desiredTemplates = Array.isArray(payload?.templates) ? payload.templates : [];
|
||||
const server = servers.find((s) =>
|
||||
[s?.id, s?.ip, s?.dns].map((x) => String(x || '')).includes(serverRef)
|
||||
);
|
||||
|
||||
if (!server || String(server.type || '').toLowerCase() !== 'home') {
|
||||
errors.push({ serverId: serverRef, error: 'HOME server not found' });
|
||||
continue;
|
||||
}
|
||||
|
||||
const serverId = server.id || server.ip || server.dns;
|
||||
const creds = getMikrotikCredentials(server);
|
||||
if (!creds) {
|
||||
errors.push({ serverId, error: 'MikroTik credentials not configured for this server' });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const client = createRosClient(creds);
|
||||
const raw = await rosPrint(client, 'routing/ospf/interface-template');
|
||||
const existing = (Array.isArray(raw) ? raw : []).filter((item) => normalizeOspfInterfaceName(item));
|
||||
|
||||
let updated = 0;
|
||||
let skipped = 0;
|
||||
let missing = 0;
|
||||
let invalid = 0;
|
||||
|
||||
for (const desired of desiredTemplates) {
|
||||
const interfaceName = String(desired?.interfaceName || '').trim();
|
||||
const desiredCost = parseOspfCost(desired?.cost);
|
||||
|
||||
if (!interfaceName || desiredCost == null) {
|
||||
invalid += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = existing.find((item) =>
|
||||
sameOspfInterface(normalizeOspfInterfaceName(item), interfaceName)
|
||||
);
|
||||
|
||||
if (!match || !match['.id']) {
|
||||
missing += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const currentCost = parseOspfCost(match.cost);
|
||||
if (currentCost === desiredCost) {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
await rosSet(client, 'routing/ospf/interface-template', match['.id'], { cost: String(desiredCost) });
|
||||
updated += 1;
|
||||
}
|
||||
|
||||
summary.updated += updated;
|
||||
summary.skipped += skipped;
|
||||
summary.missing += missing;
|
||||
summary.invalid += invalid;
|
||||
|
||||
results.push({
|
||||
serverId,
|
||||
serverLabel: server.dns || server.ip || serverId,
|
||||
updated,
|
||||
skipped,
|
||||
missing,
|
||||
invalid,
|
||||
});
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
serverId,
|
||||
serverLabel: server.dns || server.ip || serverId,
|
||||
error: error?.response?.data?.detail || error?.response?.data?.message || error.message || 'Failed to apply OSPF templates',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return res.json({
|
||||
ok: errors.length === 0,
|
||||
summary,
|
||||
results,
|
||||
errors,
|
||||
});
|
||||
} catch (error) {
|
||||
return sendError(res, 500, error.message || 'Failed to apply OSPF templates', 'E_OSPF_APPLY');
|
||||
}
|
||||
}
|
||||
|
||||
const ADDRESS_LIST_PATH = 'ip/firewall/address-list';
|
||||
const ADDRESS_LIST_REMOVE_CHUNK_SIZE = 100;
|
||||
const ADDRESS_LIST_ADD_CONCURRENCY = 8;
|
||||
@@ -1684,6 +1881,8 @@ module.exports = {
|
||||
speedTestViaTunnel,
|
||||
loadNetworkConfig,
|
||||
getAddressLists,
|
||||
getOspfInterfaceTemplates,
|
||||
applyOspfInterfaceTemplates,
|
||||
applyAddressListSummary,
|
||||
uptimeCheck,
|
||||
getUptimeCache,
|
||||
|
||||
@@ -473,6 +473,8 @@ app.post('/api/mikrotik/speed-test', writeLimiter, mikrotikConfigRoutes.speedTes
|
||||
app.post('/api/mikrotik/apply', mikrotikConfigRoutes.applyMikrotikConfig);
|
||||
app.post('/api/mikrotik/run-script', mikrotikConfigRoutes.runScript);
|
||||
app.get('/api/mikrotik/address-lists', mikrotikConfigRoutes.getAddressLists);
|
||||
app.get('/api/mikrotik/ospf-interface-templates', mikrotikConfigRoutes.getOspfInterfaceTemplates);
|
||||
app.post('/api/mikrotik/ospf-interface-templates/apply', writeLimiter, mikrotikConfigRoutes.applyOspfInterfaceTemplates);
|
||||
app.post('/api/mikrotik/address-lists/apply-summary', writeLimiter, mikrotikConfigRoutes.applyAddressListSummary);
|
||||
|
||||
// === UPTIME MONITOR (проверка доступности: http / internal-ping / external-ping из настроек) ===
|
||||
|
||||
Reference in New Issue
Block a user