DECT basestations rely on multicast for handset discovery/registration.
When Meraki switches have IGMP snooping enabled without a querier —
or per-switch overrides that deviate from a DECT-safe policy — those
frames are pruned and DECT handsets silently fail to register.
- integrations/meraki/switches.js: getSwitchMulticastSettings,
setSwitchMulticastSettings, and pure summarizeMulticast verdict fn.
- services/phoneService.js: fires the multicast fetch as soon as the
network id is known, overlapping DECT enrichment; attaches
data.multicast summary to the collector output. Non-fatal on error.
- services/renderers/phoneStatusRenderer.js: emits a single warning
line inside the DECT Basestations section only when needsFix is
true, itemizing which parts deviate (default snoop, default flood,
N overrides).
- commands/igmpFix.js: frozen DECT_SAFE_MULTICAST_PAYLOAD constant
({snoop:false, flood:true, overrides:[]}), adaptive-card builder,
and confirm/cancel handlers.
- commands/phoneStatus.js: appends the adaptive card when needsFix
and the trigger came from chat (skipped on HTTP path).
- index.js: IGMP_FIX_ACTIONS set + dispatch branch mirroring the
HOST_ASSIGN pattern (one-shot pending lookup, censorActionCard,
domain call).
- utils/pendingIgmpFixes.js: 15-min TTL pending-card store.
- tests: 11 summarizer cases (all deviation permutations + malformed
input), 4 renderer cases (each warning-line shape + silence when
needsFix false or no DECT), and a frozen-constant regression guard
on the PUT payload. Full suite: 47/47 passing.
115 lines
4.2 KiB
JavaScript
115 lines
4.2 KiB
JavaScript
// src/integrations/meraki/switches.js
|
|
//
|
|
// Switch-level Meraki calls. Currently scoped to the network-wide
|
|
// multicast (IGMP snooping / flood-unknown-multicast) settings —
|
|
// added to support DECT troubleshooting where snooping-without-a-
|
|
// querier prunes multicast traffic and breaks handset registration.
|
|
//
|
|
// The Meraki endpoint is:
|
|
// GET/PUT /networks/{networkId}/switch/routing/multicast
|
|
//
|
|
// Payload shape:
|
|
// {
|
|
// defaultSettings: {
|
|
// igmpSnoopingEnabled: boolean,
|
|
// floodUnknownMulticastTrafficEnabled: boolean,
|
|
// },
|
|
// overrides: [
|
|
// { switches?: [serial, ...], stacks?: [stackId, ...], igmpSnoopingEnabled, floodUnknownMulticastTrafficEnabled },
|
|
// ...
|
|
// ],
|
|
// }
|
|
//
|
|
// Target end-state for DECT stores (from the plan): network default
|
|
// snoop=false + flood=true, and NO overrides at all (network-level
|
|
// policy, every switch agrees by convention). That target is exported
|
|
// as a constant from commands/igmpFix.js and used verbatim by the fix
|
|
// path — this module intentionally stays policy-free.
|
|
|
|
import { merakiAxios } from './client.js';
|
|
import { logger } from '../../utils/logger.js';
|
|
|
|
/**
|
|
* Fetch the current switch multicast settings for a network.
|
|
* Returns the raw API response on success, or throws on network /
|
|
* auth failure so the caller can decide how to degrade.
|
|
*/
|
|
export async function getSwitchMulticastSettings(networkId) {
|
|
if (!networkId) {
|
|
throw new Error('getSwitchMulticastSettings: networkId is required');
|
|
}
|
|
const res = await merakiAxios.get(`/networks/${networkId}/switch/routing/multicast`);
|
|
return res.data;
|
|
}
|
|
|
|
/**
|
|
* PUT the full multicast payload for a network. The Meraki endpoint
|
|
* treats this as a full replacement — whatever `overrides` array you
|
|
* send becomes the new authoritative list. That's why the fix path
|
|
* sends `overrides: []` to wipe everything rather than trying to
|
|
* patch individual entries.
|
|
*/
|
|
export async function setSwitchMulticastSettings(networkId, payload) {
|
|
if (!networkId) {
|
|
throw new Error('setSwitchMulticastSettings: networkId is required');
|
|
}
|
|
const res = await merakiAxios.put(
|
|
`/networks/${networkId}/switch/routing/multicast`,
|
|
payload,
|
|
);
|
|
logger('meraki:switches', `Updated multicast settings for network ${networkId}`, 'debug');
|
|
return res.data;
|
|
}
|
|
|
|
/**
|
|
* Pure summarizer — turns a raw multicast settings payload into a
|
|
* "needs fix?" verdict for DECT-safe policy. Kept side-effect-free
|
|
* so the unit test can call it with fixture data (no Meraki mock).
|
|
*
|
|
* "Needs fix" is true when:
|
|
* - Network default `igmpSnoopingEnabled !== false`, OR
|
|
* - Network default `floodUnknownMulticastTrafficEnabled !== true`, OR
|
|
* - Any override deviates on either setting.
|
|
*
|
|
* An override that happens to match the target defaults is redundant
|
|
* (not deviating) — the multicast endpoint only stores these two
|
|
* fields per override, so a matching override serves no purpose but
|
|
* doesn't break anything either. We treat it as non-deviating so we
|
|
* don't nag operators about harmless historical config.
|
|
*
|
|
* Malformed / missing input returns `needsFix: false` with both
|
|
* boolean flags false — the caller stores `data.multicast.error` in
|
|
* that case and the renderer stays silent, per the "diagnostic bot
|
|
* shouldn't become a new source of noise" rule.
|
|
*
|
|
* @param {object} raw Raw response from `getSwitchMulticastSettings`.
|
|
* @returns {{
|
|
* defaultOk: boolean,
|
|
* defaultSnoopOn: boolean,
|
|
* defaultFloodOff: boolean,
|
|
* deviatingOverrides: object[],
|
|
* needsFix: boolean,
|
|
* raw: object,
|
|
* }}
|
|
*/
|
|
export function summarizeMulticast(raw) {
|
|
const def = raw?.defaultSettings || {};
|
|
const defaultSnoopOn = def.igmpSnoopingEnabled === true;
|
|
const defaultFloodOff = def.floodUnknownMulticastTrafficEnabled === false;
|
|
const defaultOk = !defaultSnoopOn && !defaultFloodOff;
|
|
|
|
const overrides = Array.isArray(raw?.overrides) ? raw.overrides : [];
|
|
const deviatingOverrides = overrides.filter((o) =>
|
|
o?.igmpSnoopingEnabled !== false ||
|
|
o?.floodUnknownMulticastTrafficEnabled !== true
|
|
);
|
|
|
|
return {
|
|
defaultOk,
|
|
defaultSnoopOn,
|
|
defaultFloodOff,
|
|
deviatingOverrides,
|
|
needsFix: !defaultOk || deviatingOverrides.length > 0,
|
|
raw,
|
|
};
|
|
}
|