Add bulk OLT PON flooding-mode tool; restore README; docs
Adds a second bulk workflow alongside the firmware upgrader: changing PON flooding mode (private<->auto) on OLT NNI services across many OLTs. Flooding mode is encoded by the presence of the "PON FLOOD ID" key on each OLT-CFG["NNI Networks"] entry (present = private, absent = auto). private->auto deletes the key; auto->private sets it to 0. The rest of the OLT-CFG is preserved (PUT-as-replace, same as ONU-CFG). - src/mcms-api.js: listAllOltConfigs / getOltConfig / putOltConfig, plus pure helpers floodModeOfNni, summarizeOltNniNetworks, planFloodChange (planFloodChange clones and never mutates its input) - main.js: api:listOlts + api:executeFloodChange IPC handlers (concurrency 3, GET->plan->PUT, streams olt-flood:progress) - preload.js: expose both channels + onFloodProgress - renderer: OLT inspector view (#view-olts), two-step APPLY confirm, auto-saved pon-olt-flood-*.csv result report - Restore README.md (lost to a filesystem error) and document the new feature in README.md and CLAUDE.md (new section 14) Verified: npm run check passes; pure helpers unit-tested for the no-mutation, idempotency, and rollback invariants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0eb9b55717
commit
80fd7e8d37
7 changed files with 612 additions and 7 deletions
148
src/mcms-api.js
148
src/mcms-api.js
|
|
@ -538,6 +538,44 @@ class McmsClient {
|
|||
return this._listPaginatedByNext('/v3/olts/states/', { pageSize });
|
||||
}
|
||||
|
||||
// --- OLT configuration (PON flooding mode) ---------------------------
|
||||
|
||||
/**
|
||||
* Bulk-fetch all OLT-CFG documents. Same pagination as ONU configs
|
||||
* (page size 100 via `next` cursor — see §3.3 / §3.4 of MCMS_API.md).
|
||||
*/
|
||||
async listAllOltConfigs({ pageSize = 100 } = {}) {
|
||||
return this._listPaginatedByNext('/v3/olts/configs/', { pageSize });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single OLT-CFG document.
|
||||
*/
|
||||
async getOltConfig(oltId) {
|
||||
try {
|
||||
const res = await this._request(
|
||||
'GET', `/v3/olts/configs/${encodeURIComponent(oltId)}/`,
|
||||
);
|
||||
return res.body?.data;
|
||||
} catch (e) {
|
||||
if (e instanceof McmsApiError && e.status === 404) return null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace an OLT-CFG document. PUT-as-replace semantics — caller MUST
|
||||
* GET first, mutate in place, and PUT the entire result. Partial
|
||||
* writes will blank out service config (same gotcha as ONU-CFG).
|
||||
*/
|
||||
async putOltConfig(oltId, fullDoc) {
|
||||
const res = await this._request(
|
||||
'PUT', `/v1/olts/configs/${encodeURIComponent(oltId)}/`,
|
||||
{ body: { data: fullDoc } },
|
||||
);
|
||||
return res.body;
|
||||
}
|
||||
|
||||
// --- Firmware inventory ----------------------------------------------
|
||||
|
||||
async listOnuFirmware() {
|
||||
|
|
@ -637,6 +675,113 @@ class McmsClient {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------- OLT PON flooding mode -------------------------------------
|
||||
// On Tibit OLTs, each NNI service entry under OLT-CFG["NNI Networks"] is
|
||||
// in "private" flood mode when the "PON FLOOD ID" key is *present* (any
|
||||
// value, including 0) and "auto" when it's *absent*. These pure helpers
|
||||
// classify, summarize, and compute a (non-mutating) flood-mode change so
|
||||
// main.js can GET → plan → PUT each OLT-CFG.
|
||||
|
||||
/**
|
||||
* PON flooding mode classification for an "NNI Networks[i]" entry on
|
||||
* an OLT-CFG. On Tibit OLTs, the *presence* of the "PON FLOOD ID" key
|
||||
* (with any value, including 0) puts the service into PRIVATE flood
|
||||
* mode; its *absence* leaves it on AUTO.
|
||||
*
|
||||
* Reference comparison (24:65:e1:cc:af:1e, NNI s0.c76.c0):
|
||||
*
|
||||
* private: { "TAG MATCH": "s0.c76.c0", ..., "PON FLOOD ID": 0, "Learning Limit": 2046 }
|
||||
* auto: { "TAG MATCH": "s0.c76.c0", ..., "Learning Limit": 2046 }
|
||||
*
|
||||
* The only diff is the presence/absence of "PON FLOOD ID".
|
||||
*/
|
||||
function floodModeOfNni(nniNet) {
|
||||
if (!nniNet || typeof nniNet !== 'object') return 'unknown';
|
||||
return Object.prototype.hasOwnProperty.call(nniNet, 'PON FLOOD ID')
|
||||
? 'private'
|
||||
: 'auto';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a uniform summary of every NNI Networks entry on an OLT-CFG,
|
||||
* filtered by a TAG MATCH pattern. Pattern can be:
|
||||
* - exact string (e.g. "s0.c76.c0")
|
||||
* - "*" / "" — match all
|
||||
* - glob with * wildcards (e.g. "s0.c76.*")
|
||||
*/
|
||||
function summarizeOltNniNetworks(oltCfg, tagPattern = '') {
|
||||
const networks = Array.isArray(oltCfg?.['NNI Networks']) ? oltCfg['NNI Networks'] : [];
|
||||
const re = tagPatternToRegex(tagPattern);
|
||||
const out = [];
|
||||
for (let i = 0; i < networks.length; i++) {
|
||||
const n = networks[i];
|
||||
const tag = n?.['TAG MATCH'] || '';
|
||||
if (!re.test(tag)) continue;
|
||||
out.push({
|
||||
index: i,
|
||||
tagMatch: tag,
|
||||
mode: floodModeOfNni(n),
|
||||
ponFloodId: Object.prototype.hasOwnProperty.call(n, 'PON FLOOD ID') ? n['PON FLOOD ID'] : null,
|
||||
learningLimit: n?.['Learning Limit'] ?? null,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function tagPatternToRegex(pattern) {
|
||||
const p = String(pattern || '').trim();
|
||||
if (!p || p === '*') return /.*/;
|
||||
const escaped = p.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
||||
return new RegExp('^' + escaped + '$');
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a PON flooding mode change to a CLONE of an OLT-CFG doc. Does
|
||||
* not mutate the input. Returns { newDoc, changes, unchanged }.
|
||||
*
|
||||
* `targetMode` is 'auto' (delete PON FLOOD ID) or 'private' (set
|
||||
* PON FLOOD ID to ponFloodIdValue, default 0).
|
||||
*/
|
||||
function planFloodChange(oltCfg, {
|
||||
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 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;
|
||||
}
|
||||
if (current === targetMode) {
|
||||
unchanged.push({ index: i, tagMatch: tag, reason: `already in target mode "${targetMode}"` });
|
||||
continue;
|
||||
}
|
||||
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 });
|
||||
}
|
||||
const newDoc = { ...oltCfg, 'NNI Networks': newNetworks };
|
||||
return { newDoc, changes, unchanged };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
McmsClient,
|
||||
McmsApiError,
|
||||
|
|
@ -645,4 +790,7 @@ module.exports = {
|
|||
extractOnuHealth,
|
||||
extractFecHealth, // legacy alias — calls extractOnuHealth
|
||||
formatFecNumber,
|
||||
floodModeOfNni,
|
||||
summarizeOltNniNetworks,
|
||||
planFloodChange,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue