olt: add bulk DHCPv4/DHCPv6 filter edit alongside flood mode
Generalises the OLT bulk tool from flood-mode-only to arbitrary NNI-service
edits. DHCP modes live nested under each NNI entry's Filter object
(Filter.DHCPv4 / Filter.DHCPv6, e.g. "pass" <-> "umt"), confirmed from a
real OLT-CFG paste-back.
- src/mcms-api.js: planFloodChange -> planNniEdit(oltCfg, {tagPattern, flood,
dhcpv4, dhcpv6}); each concern optional. DHCP is value-replacement,
replace-only-when-present (never invents the key; leaves EAPOL/PPPoE/NDP).
Still non-mutating (clones the entry AND the Filter). Returns changes with
per-entry edits[]. summarizeOltNniNetworks surfaces dhcpv4/dhcpv6; new
dhcpModeOfNni helper.
- main.js + web/server.js: executeFloodChange handler passes flood/dhcpv4/
dhcpv6 ops through (channel name kept for wire compat).
- renderer: OLT inspector now has three action dropdowns (Flooding / DHCPv4 /
DHCPv6, defaults flood private->auto + DHCP pass->umt); table shows DHCP
chips + per-service "will change (flood, DHCPv4, …)"; flood-mode filter
defaults to Any so DHCP-on-pass auto-flood OLTs still show; CSV report
lists every per-NNI edit (pon-olt-nni-*.csv).
Verified: node --check; planNniEdit unit-tested against the real fixture +
a pass variant (13/13: no-mutation, EAPOL/PPPoE/NDP preserved,
replace-when-present, flood/DHCP independence); OLT view rendered offscreen
showing correct per-service "will change" markers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
de037c8795
commit
5e3d9271a8
7 changed files with 232 additions and 118 deletions
|
|
@ -713,6 +713,17 @@ function floodModeOfNni(nniNet) {
|
|||
: 'auto';
|
||||
}
|
||||
|
||||
// DHCP relay/filter mode for an NNI entry. On Tibit OLTs these live under a
|
||||
// nested `Filter` object alongside EAPOL/PPPoE/NDP, e.g.
|
||||
// "Filter": { "DHCPv4": "umt", "DHCPv6": "umt", "EAPOL": "pass", ... }
|
||||
// `ver` is 'DHCPv4' | 'DHCPv6'. Returns the value (e.g. "umt" / "pass") or
|
||||
// null if there is no Filter or the key is absent.
|
||||
function dhcpModeOfNni(nniNet, ver) {
|
||||
const filter = nniNet && typeof nniNet.Filter === 'object' && nniNet.Filter ? nniNet.Filter : null;
|
||||
if (!filter) return null;
|
||||
return Object.prototype.hasOwnProperty.call(filter, ver) ? filter[ver] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a uniform summary of every NNI Networks entry on an OLT-CFG,
|
||||
* filtered by a TAG MATCH pattern. Pattern can be:
|
||||
|
|
@ -733,6 +744,8 @@ function summarizeOltNniNetworks(oltCfg, tagPattern = '') {
|
|||
tagMatch: tag,
|
||||
mode: floodModeOfNni(n),
|
||||
ponFloodId: Object.prototype.hasOwnProperty.call(n, 'PON FLOOD ID') ? n['PON FLOOD ID'] : null,
|
||||
dhcpv4: dhcpModeOfNni(n, 'DHCPv4'),
|
||||
dhcpv6: dhcpModeOfNni(n, 'DHCPv6'),
|
||||
learningLimit: n?.['Learning Limit'] ?? null,
|
||||
});
|
||||
}
|
||||
|
|
@ -747,48 +760,65 @@ function tagPatternToRegex(pattern) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Apply a PON flooding mode change to a CLONE of an OLT-CFG doc. Does
|
||||
* not mutate the input. Returns { newDoc, changes, unchanged }.
|
||||
* Apply a set of NNI-service edits to a CLONE of an OLT-CFG. Non-mutating —
|
||||
* the input doc is never touched (the matched entries and any edited `Filter`
|
||||
* are cloned before mutation). Returns { newDoc, changes, unchanged } where a
|
||||
* change is `{ index, tagMatch, edits: [{ type, from, to }] }` (an entry can
|
||||
* carry several edits) and unchanged entries record a reason.
|
||||
*
|
||||
* `targetMode` is 'auto' (delete PON FLOOD ID) or 'private' (set
|
||||
* PON FLOOD ID to ponFloodIdValue, default 0).
|
||||
* ops:
|
||||
* tagPattern — which NNI entries to touch (exact / "*" / glob)
|
||||
* flood — { fromMode='private'|'auto'|null, targetMode, ponFloodIdValue=0 } | null
|
||||
* targetMode 'auto' deletes PON FLOOD ID; 'private' sets it.
|
||||
* dhcpv4 — { from='pass'|null, to='umt' } | null (Filter.DHCPv4)
|
||||
* dhcpv6 — { from='pass'|null, to='umt' } | null (Filter.DHCPv6)
|
||||
*
|
||||
* DHCP edits are replace-only-when-present: an entry with no `Filter` (or no
|
||||
* such key) is left alone rather than inventing the field. A null `from` means
|
||||
* "from any current value"; a set `from` only flips entries currently on it.
|
||||
*/
|
||||
function planFloodChange(oltCfg, {
|
||||
tagPattern,
|
||||
fromMode = 'private', // only flip entries currently in this mode (null = any)
|
||||
targetMode = 'auto',
|
||||
ponFloodIdValue = 0,
|
||||
}) {
|
||||
function planNniEdit(oltCfg, { tagPattern, flood = null, dhcpv4 = null, dhcpv6 = null } = {}) {
|
||||
const networks = Array.isArray(oltCfg?.['NNI Networks']) ? oltCfg['NNI Networks'] : [];
|
||||
const re = tagPatternToRegex(tagPattern);
|
||||
const newNetworks = networks.map((n) => ({ ...n }));
|
||||
const changes = [];
|
||||
const unchanged = [];
|
||||
|
||||
for (let i = 0; i < newNetworks.length; i++) {
|
||||
const n = newNetworks[i];
|
||||
const tag = n?.['TAG MATCH'] || '';
|
||||
if (!re.test(tag)) { unchanged.push({ index: i, tagMatch: tag, reason: 'tag does not match' }); continue; }
|
||||
const current = floodModeOfNni(n);
|
||||
if (fromMode && current !== fromMode) {
|
||||
unchanged.push({ index: i, tagMatch: tag, reason: `already in mode "${current}", not "${fromMode}"` });
|
||||
continue;
|
||||
|
||||
const edits = [];
|
||||
|
||||
// Flood mode — presence of PON FLOOD ID.
|
||||
if (flood && flood.targetMode) {
|
||||
const cur = floodModeOfNni(n);
|
||||
const okFrom = !flood.fromMode || cur === flood.fromMode;
|
||||
if (okFrom && cur !== flood.targetMode) {
|
||||
if (flood.targetMode === 'auto') delete n['PON FLOOD ID'];
|
||||
else if (flood.targetMode === 'private') n['PON FLOOD ID'] = flood.ponFloodIdValue ?? 0;
|
||||
edits.push({ type: 'flood', from: cur, to: flood.targetMode });
|
||||
}
|
||||
}
|
||||
if (current === targetMode) {
|
||||
unchanged.push({ index: i, tagMatch: tag, reason: `already in target mode "${targetMode}"` });
|
||||
continue;
|
||||
|
||||
// DHCP filter modes — Filter.DHCPv4 / Filter.DHCPv6, replace-when-present.
|
||||
for (const [op, fieldKey] of [[dhcpv4, 'DHCPv4'], [dhcpv6, 'DHCPv6']]) {
|
||||
if (!op || !op.to) continue;
|
||||
const filter = n.Filter;
|
||||
if (!filter || typeof filter !== 'object' || !Object.prototype.hasOwnProperty.call(filter, fieldKey)) continue;
|
||||
const cur = filter[fieldKey];
|
||||
const okFrom = !op.from || cur === op.from;
|
||||
if (okFrom && cur !== op.to) {
|
||||
n.Filter = { ...filter, [fieldKey]: op.to }; // clone Filter; never mutate the input
|
||||
edits.push({ type: fieldKey, from: cur, to: op.to });
|
||||
}
|
||||
}
|
||||
let removedPonFloodId;
|
||||
if (targetMode === 'auto') {
|
||||
removedPonFloodId = n['PON FLOOD ID'];
|
||||
delete n['PON FLOOD ID'];
|
||||
} else if (targetMode === 'private') {
|
||||
n['PON FLOOD ID'] = ponFloodIdValue;
|
||||
} else {
|
||||
unchanged.push({ index: i, tagMatch: tag, reason: `unknown target mode "${targetMode}"` });
|
||||
continue;
|
||||
}
|
||||
changes.push({ index: i, tagMatch: tag, fromMode: current, toMode: targetMode, removedPonFloodId });
|
||||
|
||||
if (edits.length) changes.push({ index: i, tagMatch: tag, edits });
|
||||
else unchanged.push({ index: i, tagMatch: tag, reason: 'nothing to change (already at target / from-mode mismatch / field absent)' });
|
||||
}
|
||||
|
||||
const newDoc = { ...oltCfg, 'NNI Networks': newNetworks };
|
||||
return { newDoc, changes, unchanged };
|
||||
}
|
||||
|
|
@ -802,6 +832,7 @@ module.exports = {
|
|||
extractFecHealth, // legacy alias — calls extractOnuHealth
|
||||
formatFecNumber,
|
||||
floodModeOfNni,
|
||||
dhcpModeOfNni,
|
||||
summarizeOltNniNetworks,
|
||||
planFloodChange,
|
||||
planNniEdit,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue