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
61
CLAUDE.md
61
CLAUDE.md
|
|
@ -18,7 +18,8 @@ fleet. Top-level **tabs** (Dashboard / ONU / OLT) switch between:
|
||||||
- **Dashboard** — read-only network telemetry (see §15).
|
- **Dashboard** — read-only network telemetry (see §15).
|
||||||
- **ONU** — the fleet table + firmware-upgrade campaign + verify flows
|
- **ONU** — the fleet table + firmware-upgrade campaign + verify flows
|
||||||
(the original purpose: bulk ONU firmware upgrades).
|
(the original purpose: bulk ONU firmware upgrades).
|
||||||
- **OLT** — bulk OLT PON flooding-mode changes (private↔auto, see §14).
|
- **OLT** — bulk OLT NNI-service edits: PON flooding mode (private↔auto) and
|
||||||
|
DHCPv4/v6 filter (pass↔umt), see §14.
|
||||||
|
|
||||||
Reference deployment: GNXS / Tibit MicroPlug ONUs running Interos Everest
|
Reference deployment: GNXS / Tibit MicroPlug ONUs running Interos Everest
|
||||||
(EV051xxR / EV052xxR firmware family) behind Tibit OLTs.
|
(EV051xxR / EV052xxR firmware family) behind Tibit OLTs.
|
||||||
|
|
@ -655,48 +656,54 @@ Key procedure citations:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 14. OLT PON flooding mode (bulk private↔auto)
|
## 14. OLT NNI-service edits (bulk flood mode + DHCP filter)
|
||||||
|
|
||||||
Second bulk feature, added after the firmware upgrader. Lets an operator
|
Second bulk feature, added after the firmware upgrader. Lets an operator
|
||||||
flip the PON flooding mode of OLT NNI services across many OLTs at once.
|
edit OLT NNI services across many OLTs at once. Two concerns, applied
|
||||||
|
together in one GET→edit→PUT per OLT:
|
||||||
|
|
||||||
### The mutation rule (the whole feature in one fact)
|
### The two mutation rules
|
||||||
|
|
||||||
On Tibit OLTs, a service's flooding mode is encoded by the **presence or
|
Both live on each `OLT-CFG["NNI Networks"][i]` entry, both confirmed
|
||||||
absence of one key** on its `OLT-CFG["NNI Networks"][i]` entry:
|
empirically from operator paste-backs of `s0.c76.c0`:
|
||||||
|
|
||||||
- **`PON FLOOD ID` key present** (any value, including `0`) → **private**
|
1. **Flooding mode** — the **presence/absence of the `PON FLOOD ID` key**:
|
||||||
- **`PON FLOOD ID` key absent** → **auto**
|
present (any value, incl. `0`) → **private**; absent → **auto**. So
|
||||||
|
private→auto is `delete entry['PON FLOOD ID']`; auto→private is
|
||||||
|
`entry['PON FLOOD ID'] = 0`.
|
||||||
|
2. **DHCP filter mode** — a **value** under the nested `Filter` object:
|
||||||
|
`entry.Filter.DHCPv4` / `entry.Filter.DHCPv6`, e.g. `"pass"` ↔ `"umt"`
|
||||||
|
(siblings `EAPOL`/`PPPoE`/`NDP` are left alone). Value replacement, not
|
||||||
|
key presence. **Replace-only-when-present**: an entry with no `Filter`
|
||||||
|
(or no such key) is skipped, never invented.
|
||||||
|
|
||||||
So private→auto is `delete entry['PON FLOOD ID']`; auto→private is
|
Everything else on the entry and the rest of the OLT-CFG is preserved.
|
||||||
`entry['PON FLOOD ID'] = 0`. Everything else on the entry (`TAG MATCH`,
|
Default actions: TAG MATCH `s0.c76.c0`, flood `private→auto`, DHCPv4/v6
|
||||||
`Learning Limit`, …) and the rest of the OLT-CFG is preserved. This was
|
`pass→umt`. All directions are wired, so changes roll back from the same UI.
|
||||||
confirmed empirically by diffing two paste-backs of the same OLT
|
|
||||||
(`24:65:e1:cc:af:1e`, NNI `s0.c76.c0`) in private vs auto — the *only*
|
|
||||||
difference was that key.
|
|
||||||
|
|
||||||
The default action ships as: TAG MATCH = `s0.c76.c0`, mode `private` →
|
|
||||||
`auto`. Both directions are wired so a change can be rolled back from the
|
|
||||||
same UI.
|
|
||||||
|
|
||||||
### Data flow
|
### Data flow
|
||||||
|
|
||||||
```
|
```
|
||||||
listOlts ─► McmsClient.listAllOltConfigs ─► summarizeOltNniNetworks (pure)
|
listOlts ─► McmsClient.listAllOltConfigs ─► summarizeOltNniNetworks (pure)
|
||||||
executeFloodChange ─► per-OLT: getOltConfig ─► planFloodChange (pure, no-mutate)
|
executeFloodChange ─► per-OLT: getOltConfig ─► planNniEdit (pure, no-mutate)
|
||||||
─► putOltConfig (full-doc PUT)
|
(channel name kept for wire compat) ─► putOltConfig (full-doc PUT)
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Pure helpers** live at the bottom of `src/mcms-api.js`:
|
- **Pure helpers** at the bottom of `src/mcms-api.js`: `floodModeOfNni`,
|
||||||
`floodModeOfNni`, `summarizeOltNniNetworks`, `tagPatternToRegex`,
|
`dhcpModeOfNni`, `summarizeOltNniNetworks`, `tagPatternToRegex`,
|
||||||
`planFloodChange`. `planFloodChange` clones the `NNI Networks` array and
|
`planNniEdit`. `planNniEdit(oltCfg, { tagPattern, flood, dhcpv4, dhcpv6 })`
|
||||||
never mutates its input — it returns `{ newDoc, changes, unchanged }`.
|
clones the `NNI Networks` array (and any edited `Filter`) and never mutates
|
||||||
|
its input. Returns `{ newDoc, changes, unchanged }`, where a change is
|
||||||
|
`{ index, tagMatch, edits: [{ type, from, to }] }` — `type` is `'flood'`,
|
||||||
|
`'DHCPv4'`, or `'DHCPv6'`, so one entry can carry several edits.
|
||||||
|
- Each concern is optional (`flood`/`dhcpv4`/`dhcpv6` may be `null`); the UI
|
||||||
|
has three independent action dropdowns.
|
||||||
- **TAG MATCH pattern** supports exact strings, `*`/`""` (match all), and
|
- **TAG MATCH pattern** supports exact strings, `*`/`""` (match all), and
|
||||||
`*` globs (e.g. `s0.c76.*`). `tagPatternToRegex` escapes regex metachars
|
`*` globs (e.g. `s0.c76.*`). `tagPatternToRegex` escapes regex metachars
|
||||||
then turns `*` into `.*`.
|
then turns `*` into `.*`.
|
||||||
- **`fromMode` guard**: only entries currently in `fromMode` are flipped;
|
- **`from` guards**: only entries currently on the `from` mode/value are
|
||||||
entries already in the target mode are recorded in `unchanged` (the
|
flipped; entries already at target are left in `unchanged` (the per-OLT
|
||||||
per-OLT `noop` case), so re-running is idempotent.
|
`noop` case), so re-running is idempotent.
|
||||||
- **Concurrency 3** on the write path (vs 5 for ONU delete, 8 for state
|
- **Concurrency 3** on the write path (vs 5 for ONU delete, 8 for state
|
||||||
fetch) — OLT writes carry far more blast radius.
|
fetch) — OLT writes carry far more blast radius.
|
||||||
- **PUT-as-replace**, same gotcha as ONU-CFG (§5.7): the handler GETs the
|
- **PUT-as-replace**, same gotcha as ONU-CFG (§5.7): the handler GETs the
|
||||||
|
|
|
||||||
35
README.md
35
README.md
|
|
@ -8,8 +8,8 @@ System (MCMS) 6.2** PON Manager. Top tabs split it into:
|
||||||
registration-state breakdown (with an opt-in per-ONU Rx/FEC scan).
|
registration-state breakdown (with an opt-in per-ONU Rx/FEC scan).
|
||||||
Clicking a state jumps to the ONU tab pre-filtered.
|
Clicking a state jumps to the ONU tab pre-filtered.
|
||||||
2. **ONU** — the fleet table plus **bulk ONU firmware upgrades** (below).
|
2. **ONU** — the fleet table plus **bulk ONU firmware upgrades** (below).
|
||||||
3. **OLT** — **bulk OLT PON flooding-mode changes** (private↔auto on NNI
|
3. **OLT** — **bulk OLT NNI-service edits**: PON flooding mode (private↔auto)
|
||||||
services — see "OLT PON flooding mode" below).
|
and DHCPv4/v6 filter (pass↔umt) — see "OLT NNI-service edits" below.
|
||||||
|
|
||||||
## ONU firmware upgrades
|
## ONU firmware upgrades
|
||||||
|
|
||||||
|
|
@ -104,27 +104,36 @@ pon-fleet-upgrader/
|
||||||
└── app.js # UI logic
|
└── app.js # UI logic
|
||||||
```
|
```
|
||||||
|
|
||||||
## OLT PON flooding mode
|
## OLT NNI-service edits
|
||||||
|
|
||||||
From the fleet view, **Open OLT inspector…** opens a second workflow for
|
From the fleet view, **Open OLT inspector…** opens a second workflow for
|
||||||
bulk-checking and changing the PON flooding mode of OLT NNI services.
|
bulk-checking and editing OLT NNI services. Two independent edits can be
|
||||||
|
applied together to every matching service:
|
||||||
|
|
||||||
On Tibit OLTs the flooding mode is encoded by the presence of a single key
|
**Flooding mode** — encoded by the presence of a single key on each
|
||||||
on each `OLT-CFG["NNI Networks"]` entry:
|
`OLT-CFG["NNI Networks"]` entry:
|
||||||
|
|
||||||
| Mode | `PON FLOOD ID` key | Change |
|
| Mode | `PON FLOOD ID` key | Change |
|
||||||
|------|--------------------|--------|
|
|------|--------------------|--------|
|
||||||
| **private** | present (any value, incl. `0`) | → auto: delete the key |
|
| **private** | present (any value, incl. `0`) | → auto: delete the key |
|
||||||
| **auto** | absent | → private: set the key to `0` |
|
| **auto** | absent | → private: set the key to `0` |
|
||||||
|
|
||||||
Workflow: enter a **TAG MATCH** pattern (exact like `s0.c76.c0`, or a `*`
|
**DHCP filter mode** — values nested under each entry's `Filter` object
|
||||||
glob like `s0.c76.*`), **Load OLTs**, select the OLTs to change, pick a
|
(`DHCPv4` / `DHCPv6`), e.g. `pass` ↔ `umt`. Only entries that already have
|
||||||
direction, then **Execute change…** (two-step confirm). Each OLT is
|
the field are touched (the field is never invented); `EAPOL`/`PPPoE`/`NDP`
|
||||||
re-fetched, the matching NNI entries are flipped, and the full OLT-CFG is
|
are left alone.
|
||||||
PUT back. A result CSV is auto-saved to `~/Downloads/pon-olt-flood-*.csv`.
|
|
||||||
|
|
||||||
Both directions are supported, so a change can be rolled back from the same
|
Workflow: enter a **TAG MATCH** pattern (exact like `s0.c76.c0`, or a `*`
|
||||||
screen. The default action targets `s0.c76.c0`, `private` → `auto`.
|
glob like `s0.c76.*`), **Load OLTs**, pick any of the three actions
|
||||||
|
(Flooding / DHCPv4 / DHCPv6), select the OLTs — each matching service shows
|
||||||
|
"→ will change (…)" for the edits that apply — then **Execute changes…**
|
||||||
|
(two-step confirm). Each OLT is re-fetched, the matching NNI entries are
|
||||||
|
edited in one pass, and the full OLT-CFG is PUT back. A result CSV is
|
||||||
|
auto-saved to `~/Downloads/pon-olt-nni-*.csv`.
|
||||||
|
|
||||||
|
All directions are supported (e.g. `umt → pass`), so changes roll back from
|
||||||
|
the same screen. Defaults: `s0.c76.c0`, flood `private → auto`, DHCPv4/v6
|
||||||
|
`pass → umt`.
|
||||||
|
|
||||||
## Known limitations
|
## Known limitations
|
||||||
|
|
||||||
|
|
|
||||||
17
main.js
17
main.js
|
|
@ -10,7 +10,7 @@ const path = require('path');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const {
|
const {
|
||||||
McmsClient, McmsApiError, extractOnuHealth,
|
McmsClient, McmsApiError, extractOnuHealth,
|
||||||
summarizeOltNniNetworks, planFloodChange,
|
summarizeOltNniNetworks, planNniEdit,
|
||||||
} = require('./src/mcms-api');
|
} = require('./src/mcms-api');
|
||||||
const { summarizeDashboard } = require('./src/dashboard');
|
const { summarizeDashboard } = require('./src/dashboard');
|
||||||
const { planUpgrade, activeBankPtr, inactiveBank } = require('./src/bank-strategy');
|
const { planUpgrade, activeBankPtr, inactiveBank } = require('./src/bank-strategy');
|
||||||
|
|
@ -654,9 +654,11 @@ ipcMain.handle('api:listOlts', async (_ev, opts) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply a planned flooding mode change to a list of OLT IDs. For each
|
* Apply a set of NNI-service edits (flood mode and/or DHCPv4/DHCPv6 filter)
|
||||||
* OLT we GET the current CFG (to avoid stomping concurrent edits),
|
* to a list of OLT IDs. For each OLT we GET the current CFG (to avoid
|
||||||
* run planFloodChange, and PUT the result back. Streams progress.
|
* stomping concurrent edits), run planNniEdit, and PUT the result back.
|
||||||
|
* Streams progress. (Channel name kept as executeFloodChange for wire
|
||||||
|
* compatibility.)
|
||||||
*
|
*
|
||||||
* Concurrency is kept low (default 3) — OLT writes carry a much larger
|
* Concurrency is kept low (default 3) — OLT writes carry a much larger
|
||||||
* blast radius than ONU reads, so we deliberately stay below the ONU
|
* blast radius than ONU reads, so we deliberately stay below the ONU
|
||||||
|
|
@ -666,8 +668,7 @@ ipcMain.handle('api:executeFloodChange', async (event, opts) => {
|
||||||
try {
|
try {
|
||||||
const c = requireClient();
|
const c = requireClient();
|
||||||
const {
|
const {
|
||||||
oltIds, tagPattern, fromMode = 'private',
|
oltIds, tagPattern, flood = null, dhcpv4 = null, dhcpv6 = null,
|
||||||
targetMode = 'auto', ponFloodIdValue = 0,
|
|
||||||
concurrency = 3,
|
concurrency = 3,
|
||||||
} = opts || {};
|
} = opts || {};
|
||||||
const queue = [...oltIds];
|
const queue = [...oltIds];
|
||||||
|
|
@ -683,9 +684,7 @@ ipcMain.handle('api:executeFloodChange', async (event, opts) => {
|
||||||
try {
|
try {
|
||||||
const cfg = await c.getOltConfig(id);
|
const cfg = await c.getOltConfig(id);
|
||||||
if (!cfg) throw new Error('OLT not found');
|
if (!cfg) throw new Error('OLT not found');
|
||||||
const plan = planFloodChange(cfg, {
|
const plan = planNniEdit(cfg, { tagPattern, flood, dhcpv4, dhcpv6 });
|
||||||
tagPattern, fromMode, targetMode, ponFloodIdValue,
|
|
||||||
});
|
|
||||||
if (plan.changes.length === 0) {
|
if (plan.changes.length === 0) {
|
||||||
entry = { oltId: id, ok: true, noop: true, changes: [], skipped: plan.unchanged };
|
entry = { oltId: id, ok: true, noop: true, changes: [], skipped: plan.unchanged };
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
112
renderer/app.js
112
renderer/app.js
|
|
@ -1366,7 +1366,7 @@ function confirmTyped({ title, body, requireWord, confirmLabel = 'Confirm', dang
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- OLT inspector: bulk PON flooding mode change --------
|
// -------- OLT inspector: bulk NNI-service edits (flood + DHCP) --------
|
||||||
|
|
||||||
$('#btn-to-olts').addEventListener('click', () => {
|
$('#btn-to-olts').addEventListener('click', () => {
|
||||||
showView('#view-olts');
|
showView('#view-olts');
|
||||||
|
|
@ -1379,6 +1379,10 @@ $('#btn-load-olts').addEventListener('click', loadOlts);
|
||||||
$('#olt-tag-pattern').addEventListener('input', applyOltFilters);
|
$('#olt-tag-pattern').addEventListener('input', applyOltFilters);
|
||||||
$('#olt-mode-filter').addEventListener('change', applyOltFilters);
|
$('#olt-mode-filter').addEventListener('change', applyOltFilters);
|
||||||
$('#olt-name-filter').addEventListener('input', applyOltFilters);
|
$('#olt-name-filter').addEventListener('input', applyOltFilters);
|
||||||
|
// Changing an action re-renders the per-service "→ will change" preview.
|
||||||
|
$('#olt-flood-action').addEventListener('change', renderOlts);
|
||||||
|
$('#olt-dhcpv4-action').addEventListener('change', renderOlts);
|
||||||
|
$('#olt-dhcpv6-action').addEventListener('change', renderOlts);
|
||||||
$('#btn-olt-select-all').addEventListener('click', () => {
|
$('#btn-olt-select-all').addEventListener('click', () => {
|
||||||
for (const r of state.oltFiltered) state.oltSelected.add(r._id);
|
for (const r of state.oltFiltered) state.oltSelected.add(r._id);
|
||||||
renderOlts();
|
renderOlts();
|
||||||
|
|
@ -1406,8 +1410,44 @@ async function loadOlts() {
|
||||||
$('#olt-total').textContent = state.olts.length;
|
$('#olt-total').textContent = state.olts.length;
|
||||||
const totalMatching = state.olts.reduce((acc, r) => acc + r.matchingNni.length, 0);
|
const totalMatching = state.olts.reduce((acc, r) => acc + r.matchingNni.length, 0);
|
||||||
const totalPriv = state.olts.reduce((acc, r) => acc + r.counts.private, 0);
|
const totalPriv = state.olts.reduce((acc, r) => acc + r.counts.private, 0);
|
||||||
|
const totalDhcpPass = state.olts.reduce(
|
||||||
|
(acc, r) => acc + r.matchingNni.filter((n) => n.dhcpv4 === 'pass' || n.dhcpv6 === 'pass').length, 0);
|
||||||
$('#olt-status').textContent =
|
$('#olt-status').textContent =
|
||||||
`${state.olts.length} OLTs loaded. ${totalMatching} service(s) match TAG pattern; ${totalPriv} currently in private mode.`;
|
`${state.olts.length} OLTs loaded. ${totalMatching} matching service(s); ` +
|
||||||
|
`${totalPriv} private flood, ${totalDhcpPass} with DHCP on "pass".`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the three action dropdowns into a planNniEdit-shaped ops object.
|
||||||
|
function oltActionOps() {
|
||||||
|
const dir = (v) => (v ? { from: v.split('2')[0], to: v.split('2')[1] } : null);
|
||||||
|
const flood = $('#olt-flood-action').value;
|
||||||
|
return {
|
||||||
|
flood: flood ? { fromMode: flood.split('2')[0], targetMode: flood.split('2')[1], ponFloodIdValue: 0 } : null,
|
||||||
|
dhcpv4: dir($('#olt-dhcpv4-action').value),
|
||||||
|
dhcpv6: dir($('#olt-dhcpv6-action').value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client-side mirror of planNniEdit: which edits would hit this NNI summary
|
||||||
|
// given the selected actions. DHCP edits only count when the field is present.
|
||||||
|
function nniEditsFor(n, ops) {
|
||||||
|
const edits = [];
|
||||||
|
if (ops.flood && ops.flood.targetMode) {
|
||||||
|
const okFrom = !ops.flood.fromMode || n.mode === ops.flood.fromMode;
|
||||||
|
if (okFrom && n.mode !== ops.flood.targetMode) edits.push('flood');
|
||||||
|
}
|
||||||
|
for (const [op, key, label] of [[ops.dhcpv4, 'dhcpv4', 'DHCPv4'], [ops.dhcpv6, 'dhcpv6', 'DHCPv6']]) {
|
||||||
|
if (!op || n[key] == null) continue;
|
||||||
|
const okFrom = !op.from || n[key] === op.from;
|
||||||
|
if (okFrom && n[key] !== op.to) edits.push(label);
|
||||||
|
}
|
||||||
|
return edits;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dhcpChip(label, val) {
|
||||||
|
if (val == null) return '';
|
||||||
|
const cls = val === 'umt' ? 'status-ok' : val === 'pass' ? 'status-warn' : 'muted';
|
||||||
|
return ` <span class="${cls}">${escapeHtml(label)}=${escapeHtml(String(val))}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyOltFilters() {
|
function applyOltFilters() {
|
||||||
|
|
@ -1424,20 +1464,19 @@ function applyOltFilters() {
|
||||||
function renderOlts() {
|
function renderOlts() {
|
||||||
const tbody = $('#olt-tbody');
|
const tbody = $('#olt-tbody');
|
||||||
tbody.innerHTML = '';
|
tbody.innerHTML = '';
|
||||||
const mode = $('#olt-mode-filter').value;
|
const ops = oltActionOps();
|
||||||
let totalSvcThatWillChange = 0;
|
let totalSvcThatWillChange = 0;
|
||||||
for (const r of state.oltFiltered) {
|
for (const r of state.oltFiltered) {
|
||||||
const tr = document.createElement('tr');
|
const tr = document.createElement('tr');
|
||||||
const selected = state.oltSelected.has(r._id);
|
const selected = state.oltSelected.has(r._id);
|
||||||
if (selected) tr.classList.add('selected');
|
if (selected) tr.classList.add('selected');
|
||||||
const willChangeMode = mode === 'auto' ? 'auto' : 'private';
|
|
||||||
const matchingHtml = r.matchingNni.map((n) => {
|
const matchingHtml = r.matchingNni.map((n) => {
|
||||||
const modeCls = n.mode === 'private' ? 'status-warn' : n.mode === 'auto' ? 'status-ok' : 'muted';
|
const modeCls = n.mode === 'private' ? 'status-warn' : n.mode === 'auto' ? 'status-ok' : 'muted';
|
||||||
const willChange = selected && n.mode === willChangeMode;
|
const edits = selected ? nniEditsFor(n, ops) : [];
|
||||||
if (willChange) totalSvcThatWillChange += 1;
|
if (edits.length) totalSvcThatWillChange += 1;
|
||||||
const willFlag = willChange ? ' <span class="status-info">→ will change</span>' : '';
|
const willFlag = edits.length ? ` <span class="status-info">→ will change (${escapeHtml(edits.join(', '))})</span>` : '';
|
||||||
const fid = n.ponFloodId !== null && n.ponFloodId !== undefined ? ` (PON FLOOD ID=${escapeHtml(String(n.ponFloodId))})` : '';
|
const fid = n.ponFloodId !== null && n.ponFloodId !== undefined ? ` (id=${escapeHtml(String(n.ponFloodId))})` : '';
|
||||||
return `<div><code>${escapeHtml(n.tagMatch)}</code> <span class="${modeCls}">${escapeHtml(n.mode)}</span>${escapeHtml(fid)}${willFlag}</div>`;
|
return `<div><code>${escapeHtml(n.tagMatch)}</code> <span class="${modeCls}">${escapeHtml(n.mode)}</span>${escapeHtml(fid)}${dhcpChip('v4', n.dhcpv4)}${dhcpChip('v6', n.dhcpv6)}${willFlag}</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
tr.innerHTML = `
|
tr.innerHTML = `
|
||||||
<td><input type="checkbox" data-id="${escapeHtml(r._id)}" ${selected ? 'checked' : ''}/></td>
|
<td><input type="checkbox" data-id="${escapeHtml(r._id)}" ${selected ? 'checked' : ''}/></td>
|
||||||
|
|
@ -1466,20 +1505,28 @@ function renderOlts() {
|
||||||
$('#btn-olt-execute').addEventListener('click', async () => {
|
$('#btn-olt-execute').addEventListener('click', async () => {
|
||||||
const ids = Array.from(state.oltSelected);
|
const ids = Array.from(state.oltSelected);
|
||||||
if (!ids.length) return;
|
if (!ids.length) return;
|
||||||
const action = $('#olt-action').value;
|
const ops = oltActionOps();
|
||||||
const fromMode = action === 'auto2private' ? 'auto' : 'private';
|
|
||||||
const targetMode = action === 'auto2private' ? 'private' : 'auto';
|
|
||||||
const tagPattern = $('#olt-tag-pattern').value.trim();
|
const tagPattern = $('#olt-tag-pattern').value.trim();
|
||||||
|
|
||||||
|
const actionLines = [];
|
||||||
|
if (ops.flood) actionLines.push(`Flooding: ${ops.flood.fromMode} → ${ops.flood.targetMode}`);
|
||||||
|
if (ops.dhcpv4) actionLines.push(`DHCPv4: ${ops.dhcpv4.from} → ${ops.dhcpv4.to}`);
|
||||||
|
if (ops.dhcpv6) actionLines.push(`DHCPv6: ${ops.dhcpv6.from} → ${ops.dhcpv6.to}`);
|
||||||
|
if (!actionLines.length) {
|
||||||
|
$('#olt-exec-status').textContent = 'Select at least one action (Flooding / DHCPv4 / DHCPv6).';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const summary =
|
const summary =
|
||||||
`Apply "${fromMode} → ${targetMode}" to NNI services matching "${tagPattern}" ` +
|
`Apply to NNI services matching "${tagPattern}" on ${ids.length} OLT(s):\n • ` +
|
||||||
`on ${ids.length} OLT(s).\n\nThis GETs each OLT-CFG, mutates the matching ` +
|
actionLines.join('\n • ') +
|
||||||
`NNI Networks entries, and PUTs the full document back. The operation is ` +
|
`\n\nEach OLT-CFG is fetched, the matching NNI entries edited, and the full ` +
|
||||||
`irreversible from this app (no undo).`;
|
`document PUT back. No undo from this app.`;
|
||||||
const confirmed = await confirmTyped({
|
const confirmed = await confirmTyped({
|
||||||
title: 'Apply PON flooding-mode change',
|
title: 'Apply OLT NNI changes',
|
||||||
body: summary,
|
body: summary,
|
||||||
requireWord: 'APPLY',
|
requireWord: 'APPLY',
|
||||||
confirmLabel: 'Execute change',
|
confirmLabel: 'Execute changes',
|
||||||
danger: true,
|
danger: true,
|
||||||
});
|
});
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
|
|
@ -1488,15 +1535,13 @@ $('#btn-olt-execute').addEventListener('click', async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#btn-olt-execute').disabled = true;
|
$('#btn-olt-execute').disabled = true;
|
||||||
$('#olt-exec-status').textContent = `Applying change to 0/${ids.length}…`;
|
$('#olt-exec-status').textContent = `Applying to 0/${ids.length}…`;
|
||||||
const unbind = window.api.onFloodProgress(({ done, total, oltId, error, changes }) => {
|
const unbind = window.api.onFloodProgress(({ done, total, oltId, error, changes }) => {
|
||||||
const tail = error ? ` (last error on ${oltId}: ${error.message})`
|
const tail = error ? ` (last error on ${oltId}: ${error.message})`
|
||||||
: changes ? ` (${oltId}: ${changes.length} svc updated)` : '';
|
: (changes && changes.length) ? ` (${oltId}: ${changes.length} svc updated)` : '';
|
||||||
$('#olt-exec-status').textContent = `Applying ${done}/${total}${tail}`;
|
$('#olt-exec-status').textContent = `Applying ${done}/${total}${tail}`;
|
||||||
});
|
});
|
||||||
const res = await window.api.executeFloodChange({
|
const res = await window.api.executeFloodChange({ oltIds: ids, tagPattern, ...ops });
|
||||||
oltIds: ids, tagPattern, fromMode, targetMode,
|
|
||||||
});
|
|
||||||
unbind();
|
unbind();
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
$('#olt-exec-status').textContent = `Bulk apply failed: ${res.error?.message}`;
|
$('#olt-exec-status').textContent = `Bulk apply failed: ${res.error?.message}`;
|
||||||
|
|
@ -1513,20 +1558,23 @@ $('#btn-olt-execute').addEventListener('click', async () => {
|
||||||
}
|
}
|
||||||
$('#olt-exec-status').textContent = msg;
|
$('#olt-exec-status').textContent = msg;
|
||||||
|
|
||||||
// Auto-save a result CSV alongside the upgrade/verify ones.
|
// Auto-save a result CSV alongside the upgrade/verify ones. Each change row
|
||||||
|
// lists every per-NNI edit, e.g. "s0.c76.c0: flood private→auto, DHCPv4 pass→umt".
|
||||||
|
const actionsStr = actionLines.join(' | ');
|
||||||
const headers = [
|
const headers = [
|
||||||
'OLT MAC', 'OLT name', 'Action', 'TAG pattern',
|
'OLT MAC', 'OLT name', 'Actions', 'TAG pattern',
|
||||||
'Result', 'Services changed', 'Services skipped',
|
'Result', 'Services changed', 'Services skipped',
|
||||||
'Change detail', 'Skip detail', 'Error',
|
'Change detail', 'Skip detail', 'Error',
|
||||||
];
|
];
|
||||||
const rows = res.data.map((r) => {
|
const rows = res.data.map((r) => {
|
||||||
const olt = state.olts.find((o) => o._id === r.oltId);
|
const olt = state.olts.find((o) => o._id === r.oltId);
|
||||||
const changeDetail = (r.changes || []).map((c) => `${c.tagMatch}: ${c.fromMode}→${c.toMode}`).join('; ');
|
const changeDetail = (r.changes || []).map((c) =>
|
||||||
const skipDetail = (r.skipped || []).map((s) => `${s.tagMatch}: ${s.reason}`).join('; ');
|
`${c.tagMatch}: ${(c.edits || []).map((e) => `${e.type} ${e.from}→${e.to}`).join(', ')}`).join('; ');
|
||||||
|
const skipDetail = (r.skipped || []).map((sk) => `${sk.tagMatch}: ${sk.reason}`).join('; ');
|
||||||
return {
|
return {
|
||||||
'OLT MAC': r.oltId,
|
'OLT MAC': r.oltId,
|
||||||
'OLT name': olt?.name || '',
|
'OLT name': olt?.name || '',
|
||||||
'Action': `${fromMode}→${targetMode}`,
|
'Actions': actionsStr,
|
||||||
'TAG pattern': tagPattern,
|
'TAG pattern': tagPattern,
|
||||||
'Result': r.ok ? (r.noop ? 'noop' : 'updated') : 'failed',
|
'Result': r.ok ? (r.noop ? 'noop' : 'updated') : 'failed',
|
||||||
'Services changed': (r.changes || []).length,
|
'Services changed': (r.changes || []).length,
|
||||||
|
|
@ -1536,10 +1584,14 @@ $('#btn-olt-execute').addEventListener('click', async () => {
|
||||||
'Error': r.error ? r.error.message : '',
|
'Error': r.error ? r.error.message : '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
const hintParts = [];
|
||||||
|
if (ops.flood) hintParts.push(`flood-${ops.flood.targetMode}`);
|
||||||
|
if (ops.dhcpv4) hintParts.push(`dhcpv4-${ops.dhcpv4.to}`);
|
||||||
|
if (ops.dhcpv6) hintParts.push(`dhcpv6-${ops.dhcpv6.to}`);
|
||||||
const saveRes = await window.api.saveCsvFile({
|
const saveRes = await window.api.saveCsvFile({
|
||||||
rows, headers,
|
rows, headers,
|
||||||
filenameHint: `${fromMode}-to-${targetMode}-${tagPattern.replace(/[^A-Za-z0-9]+/g, '_')}-${ids.length}olts`,
|
filenameHint: `${hintParts.join('-')}-${tagPattern.replace(/[^A-Za-z0-9]+/g, '_')}-${ids.length}olts`,
|
||||||
prefix: 'pon-olt-flood',
|
prefix: 'pon-olt-nni',
|
||||||
});
|
});
|
||||||
if (saveRes.ok) {
|
if (saveRes.ok) {
|
||||||
$('#olt-exec-status').textContent += ` Report → ${saveRes.data.path}`;
|
$('#olt-exec-status').textContent += ` Report → ${saveRes.data.path}`;
|
||||||
|
|
|
||||||
|
|
@ -288,11 +288,11 @@
|
||||||
<label>NNI TAG MATCH pattern
|
<label>NNI TAG MATCH pattern
|
||||||
<input id="olt-tag-pattern" type="text" value="s0.c76.c0" placeholder="e.g. s0.c76.c0 or s0.c76.*" />
|
<input id="olt-tag-pattern" type="text" value="s0.c76.c0" placeholder="e.g. s0.c76.c0 or s0.c76.*" />
|
||||||
</label>
|
</label>
|
||||||
<label>Current mode
|
<label>Flood-mode filter
|
||||||
<select id="olt-mode-filter">
|
<select id="olt-mode-filter">
|
||||||
<option value="private" selected>Private (PON FLOOD ID set)</option>
|
<option value="any" selected>Any</option>
|
||||||
|
<option value="private">Private (PON FLOOD ID set)</option>
|
||||||
<option value="auto">Auto (no PON FLOOD ID)</option>
|
<option value="auto">Auto (no PON FLOOD ID)</option>
|
||||||
<option value="any">Any</option>
|
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label>OLT name contains
|
<label>OLT name contains
|
||||||
|
|
@ -310,13 +310,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Action</h2>
|
<h2>Actions</h2>
|
||||||
<label>Change matching services
|
<p class="muted small">Applied together to every matching NNI service on the selected OLTs, in one GET→edit→PUT per OLT.</p>
|
||||||
<select id="olt-action">
|
<label>Flooding mode
|
||||||
|
<select id="olt-flood-action">
|
||||||
|
<option value="">No change</option>
|
||||||
<option value="private2auto" selected>Private → Auto (remove PON FLOOD ID)</option>
|
<option value="private2auto" selected>Private → Auto (remove PON FLOOD ID)</option>
|
||||||
<option value="auto2private">Auto → Private (set PON FLOOD ID = 0)</option>
|
<option value="auto2private">Auto → Private (set PON FLOOD ID = 0)</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label>DHCPv4 filter
|
||||||
|
<select id="olt-dhcpv4-action">
|
||||||
|
<option value="">No change</option>
|
||||||
|
<option value="pass2umt" selected>pass → umt</option>
|
||||||
|
<option value="umt2pass">umt → pass</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>DHCPv6 filter
|
||||||
|
<select id="olt-dhcpv6-action">
|
||||||
|
<option value="">No change</option>
|
||||||
|
<option value="pass2umt" selected>pass → umt</option>
|
||||||
|
<option value="umt2pass">umt → pass</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="btn-olts-back" class="ghost">← Back to fleet</button>
|
<button id="btn-olts-back" class="ghost">← Back to fleet</button>
|
||||||
|
|
|
||||||
|
|
@ -713,6 +713,17 @@ function floodModeOfNni(nniNet) {
|
||||||
: 'auto';
|
: '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,
|
* Build a uniform summary of every NNI Networks entry on an OLT-CFG,
|
||||||
* filtered by a TAG MATCH pattern. Pattern can be:
|
* filtered by a TAG MATCH pattern. Pattern can be:
|
||||||
|
|
@ -733,6 +744,8 @@ function summarizeOltNniNetworks(oltCfg, tagPattern = '') {
|
||||||
tagMatch: tag,
|
tagMatch: tag,
|
||||||
mode: floodModeOfNni(n),
|
mode: floodModeOfNni(n),
|
||||||
ponFloodId: Object.prototype.hasOwnProperty.call(n, 'PON FLOOD ID') ? n['PON FLOOD ID'] : null,
|
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,
|
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
|
* Apply a set of NNI-service edits to a CLONE of an OLT-CFG. Non-mutating —
|
||||||
* not mutate the input. Returns { newDoc, changes, unchanged }.
|
* 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
|
* ops:
|
||||||
* PON FLOOD ID to ponFloodIdValue, default 0).
|
* 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, {
|
function planNniEdit(oltCfg, { tagPattern, flood = null, dhcpv4 = null, dhcpv6 = null } = {}) {
|
||||||
tagPattern,
|
|
||||||
fromMode = 'private', // only flip entries currently in this mode (null = any)
|
|
||||||
targetMode = 'auto',
|
|
||||||
ponFloodIdValue = 0,
|
|
||||||
}) {
|
|
||||||
const networks = Array.isArray(oltCfg?.['NNI Networks']) ? oltCfg['NNI Networks'] : [];
|
const networks = Array.isArray(oltCfg?.['NNI Networks']) ? oltCfg['NNI Networks'] : [];
|
||||||
const re = tagPatternToRegex(tagPattern);
|
const re = tagPatternToRegex(tagPattern);
|
||||||
const newNetworks = networks.map((n) => ({ ...n }));
|
const newNetworks = networks.map((n) => ({ ...n }));
|
||||||
const changes = [];
|
const changes = [];
|
||||||
const unchanged = [];
|
const unchanged = [];
|
||||||
|
|
||||||
for (let i = 0; i < newNetworks.length; i++) {
|
for (let i = 0; i < newNetworks.length; i++) {
|
||||||
const n = newNetworks[i];
|
const n = newNetworks[i];
|
||||||
const tag = n?.['TAG MATCH'] || '';
|
const tag = n?.['TAG MATCH'] || '';
|
||||||
if (!re.test(tag)) { unchanged.push({ index: i, tagMatch: tag, reason: 'tag does not match' }); continue; }
|
if (!re.test(tag)) { unchanged.push({ index: i, tagMatch: tag, reason: 'tag does not match' }); continue; }
|
||||||
const current = floodModeOfNni(n);
|
|
||||||
if (fromMode && current !== fromMode) {
|
const edits = [];
|
||||||
unchanged.push({ index: i, tagMatch: tag, reason: `already in mode "${current}", not "${fromMode}"` });
|
|
||||||
continue;
|
// 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}"` });
|
// DHCP filter modes — Filter.DHCPv4 / Filter.DHCPv6, replace-when-present.
|
||||||
continue;
|
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') {
|
if (edits.length) changes.push({ index: i, tagMatch: tag, edits });
|
||||||
removedPonFloodId = n['PON FLOOD ID'];
|
else unchanged.push({ index: i, tagMatch: tag, reason: 'nothing to change (already at target / from-mode mismatch / field absent)' });
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDoc = { ...oltCfg, 'NNI Networks': newNetworks };
|
const newDoc = { ...oltCfg, 'NNI Networks': newNetworks };
|
||||||
return { newDoc, changes, unchanged };
|
return { newDoc, changes, unchanged };
|
||||||
}
|
}
|
||||||
|
|
@ -802,6 +832,7 @@ module.exports = {
|
||||||
extractFecHealth, // legacy alias — calls extractOnuHealth
|
extractFecHealth, // legacy alias — calls extractOnuHealth
|
||||||
formatFecNumber,
|
formatFecNumber,
|
||||||
floodModeOfNni,
|
floodModeOfNni,
|
||||||
|
dhcpModeOfNni,
|
||||||
summarizeOltNniNetworks,
|
summarizeOltNniNetworks,
|
||||||
planFloodChange,
|
planNniEdit,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ const crypto = require('crypto');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
McmsClient, McmsApiError, extractOnuHealth,
|
McmsClient, McmsApiError, extractOnuHealth,
|
||||||
summarizeOltNniNetworks, planFloodChange,
|
summarizeOltNniNetworks, planNniEdit,
|
||||||
} = require('../src/mcms-api');
|
} = require('../src/mcms-api');
|
||||||
const { planUpgrade } = require('../src/bank-strategy');
|
const { planUpgrade } = require('../src/bank-strategy');
|
||||||
const { summarizeDashboard } = require('../src/dashboard');
|
const { summarizeDashboard } = require('../src/dashboard');
|
||||||
|
|
@ -454,14 +454,14 @@ const streamHandlers = {
|
||||||
|
|
||||||
async executeFloodChange(s, body, write) {
|
async executeFloodChange(s, body, write) {
|
||||||
const {
|
const {
|
||||||
oltIds, tagPattern, fromMode = 'private', targetMode = 'auto',
|
oltIds, tagPattern, flood = null, dhcpv4 = null, dhcpv6 = null,
|
||||||
ponFloodIdValue = 0, concurrency = 3,
|
concurrency = 3,
|
||||||
} = body;
|
} = body;
|
||||||
const results = await runPool(oltIds, concurrency, async (id) => {
|
const results = await runPool(oltIds, concurrency, async (id) => {
|
||||||
try {
|
try {
|
||||||
const cfg = await s.client.getOltConfig(id);
|
const cfg = await s.client.getOltConfig(id);
|
||||||
if (!cfg) throw new Error('OLT not found');
|
if (!cfg) throw new Error('OLT not found');
|
||||||
const plan = planFloodChange(cfg, { tagPattern, fromMode, targetMode, ponFloodIdValue });
|
const plan = planNniEdit(cfg, { tagPattern, flood, dhcpv4, dhcpv6 });
|
||||||
if (plan.changes.length === 0) return { oltId: id, ok: true, noop: true, changes: [], skipped: plan.unchanged };
|
if (plan.changes.length === 0) return { oltId: id, ok: true, noop: true, changes: [], skipped: plan.unchanged };
|
||||||
await s.client.putOltConfig(id, plan.newDoc);
|
await s.client.putOltConfig(id, plan.newDoc);
|
||||||
return { oltId: id, ok: true, changes: plan.changes, skipped: plan.unchanged };
|
return { oltId: id, ok: true, changes: plan.changes, skipped: plan.unchanged };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue