collabSupport/integrations/meraki/switches.js
jmcqueen d112e45eb6 Detect and inline-remediate Meraki IGMP snooping in /phonestatus
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.
2026-07-02 14:10:33 -04:00

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,
};
}