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