fix(switch-compat): physically validate Flexoptix suggestions by port speed + cage mechanics

getFlexoptixSuggestions matched ONLY by form factor, discarding the speed encoded
in each ports_config key (e.g. '100G_QSFP28'). Corrupt transceiver speed_gbps
values (400G/200G/128G/100000G mislabeled as QSFP28) leaked through, so a 100G
switch showed impossible '400G QSFP28' / '100T QSFP28' suggestions.

Now parses (speed, form_factor) from each port key and requires every suggested
module to (a) mechanically seat in the cage — precise port-FF -> accepted-module-FF
map, a QSFP28 cage takes QSFP+/28/56 but never QSFP-DD — and (b) have
speed_gbps <= the port's speed. CX 10000-48Y6C (25G SFP28 + 100G QSFP28) now
returns only valid <=25G SFP / <=100G QSFP modules; 0 physically impossible
entries (was 4 garbage groups). Belt-and-suspenders: even with corrupt speed data,
nothing oversized can reach a customer-facing suggestion.
This commit is contained in:
Rene Fichtmueller 2026-06-10 22:39:24 +00:00
parent 53518fa15a
commit 2b7d5c7037

View File

@ -318,23 +318,40 @@ export async function getCompatibleTransceivers(switchId: string) {
*/ */
export async function getFlexoptixSuggestions(switchId: string) { export async function getFlexoptixSuggestions(switchId: string) {
const result = await pool.query( const result = await pool.query(
`WITH switch_form_factors AS ( `WITH switch_ports AS (
-- Parse each ports_config key 'SPEED_FORMFACTOR' (e.g. '100G_QSFP28') into
-- a numeric port speed (Gbps) + a cage family. Real transceivers are then
-- bounded by the port speed so corrupt/oversized records cannot leak in.
SELECT DISTINCT SELECT DISTINCT
-- numeric speed prefix: 100G->100, 25G->25, 2.5G->2.5, 100M->0.1, 800G->800
CASE
WHEN k ~ '^[0-9.]+M' THEN (substring(k from '^([0-9.]+)M')::numeric / 1000)
WHEN k ~ '^[0-9.]+G' THEN substring(k from '^([0-9.]+)G')::numeric
WHEN k ~ '^[0-9.]+T' THEN (substring(k from '^([0-9.]+)T')::numeric * 1000)
ELSE NULL
END AS port_speed,
-- the port's specific cage type (determines which modules mechanically seat)
CASE CASE
WHEN k ILIKE '%QSFP-DD800%' THEN 'QSFP-DD800' WHEN k ILIKE '%QSFP-DD800%' THEN 'QSFP-DD800'
WHEN k ILIKE '%QSFP-DD%' THEN 'QSFP-DD' WHEN k ILIKE '%QSFP-DD%' THEN 'QSFP-DD'
WHEN k ILIKE '%OSFP224%' THEN 'OSFP224' WHEN k ILIKE '%QSFP112%' THEN 'QSFP112'
WHEN k ILIKE '%OSFP%' THEN 'OSFP' WHEN k ILIKE '%QSFP56%' THEN 'QSFP56'
WHEN k ILIKE '%QSFP28%' THEN 'QSFP28' WHEN k ILIKE '%QSFP28%' THEN 'QSFP28'
WHEN k ILIKE '%QSFP+%' THEN 'QSFP+' WHEN k ILIKE '%QSFP+%' THEN 'QSFP+'
WHEN k ILIKE '%QSFP%' THEN 'QSFP+' WHEN k ILIKE '%QSFP%' THEN 'QSFP+'
WHEN k ILIKE '%OSFP224%' THEN 'OSFP224'
WHEN k ILIKE '%OSFP112%' THEN 'OSFP112'
WHEN k ILIKE '%OSFP%' THEN 'OSFP'
WHEN k ILIKE '%SFP56%' THEN 'SFP56'
WHEN k ILIKE '%SFP28%' THEN 'SFP28' WHEN k ILIKE '%SFP28%' THEN 'SFP28'
WHEN k ILIKE '%SFP+%' THEN 'SFP+' WHEN k ILIKE '%SFP+%' THEN 'SFP+'
WHEN k ILIKE '%SFP%' THEN 'SFP+' WHEN k ILIKE '%SFP%' THEN 'SFP+'
WHEN k ILIKE '%CFP2%' THEN 'CFP2' WHEN k ILIKE '%CFP2%' THEN 'CFP2'
WHEN k ILIKE '%CFP4%' THEN 'CFP4' WHEN k ILIKE '%CFP4%' THEN 'CFP4'
WHEN k ILIKE '%CFP%' THEN 'CFP' WHEN k ILIKE '%CFP%' THEN 'CFP'
END AS form_factor WHEN k ILIKE '%RJ45%' OR k ILIKE '%mGig%' THEN 'RJ45'
ELSE NULL
END AS port_ff
FROM switches sw, FROM switches sw,
jsonb_object_keys(sw.ports_config) AS k jsonb_object_keys(sw.ports_config) AS k
WHERE sw.id = $1 AND sw.ports_config IS NOT NULL WHERE sw.id = $1 AND sw.ports_config IS NOT NULL
@ -367,8 +384,38 @@ export async function getFlexoptixSuggestions(switchId: string) {
LIMIT 1 LIMIT 1
) so ON true ) so ON true
WHERE LOWER(v.name) = 'flexoptix' WHERE LOWER(v.name) = 'flexoptix'
AND t.form_factor IN ( AND t.speed_gbps IS NOT NULL AND t.speed_gbps > 0
SELECT form_factor FROM switch_form_factors WHERE form_factor IS NOT NULL AND EXISTS (
SELECT 1 FROM switch_ports sp
WHERE sp.port_ff IS NOT NULL
AND sp.port_speed IS NOT NULL
-- module must mechanically seat in this port cage. A cage accepts its own
-- type plus smaller/backward-compatible modules, never a larger one:
-- QSFP-DD/OSFP cages take QSFP-family; QSFP28/56 cages take QSFP+/28/56
-- (NOT QSFP-DD); SFP cages take all SFP variants. No cross-family.
AND t.form_factor = ANY (
CASE sp.port_ff
WHEN 'QSFP-DD800' THEN ARRAY['QSFP-DD800','QSFP-DD','QSFP112','QSFP56','QSFP28','QSFP+']
WHEN 'QSFP-DD' THEN ARRAY['QSFP-DD','QSFP112','QSFP56','QSFP28','QSFP+']
WHEN 'QSFP112' THEN ARRAY['QSFP112','QSFP56','QSFP28','QSFP+']
WHEN 'QSFP56' THEN ARRAY['QSFP56','QSFP28','QSFP+']
WHEN 'QSFP28' THEN ARRAY['QSFP28','QSFP+']
WHEN 'QSFP+' THEN ARRAY['QSFP+']
WHEN 'OSFP224' THEN ARRAY['OSFP224','OSFP112','OSFP']
WHEN 'OSFP112' THEN ARRAY['OSFP112','OSFP']
WHEN 'OSFP' THEN ARRAY['OSFP']
WHEN 'SFP56' THEN ARRAY['SFP56','SFP28','SFP+','SFP']
WHEN 'SFP28' THEN ARRAY['SFP28','SFP+','SFP']
WHEN 'SFP+' THEN ARRAY['SFP+','SFP']
WHEN 'CFP2' THEN ARRAY['CFP2']
WHEN 'CFP4' THEN ARRAY['CFP4']
WHEN 'CFP' THEN ARRAY['CFP']
WHEN 'RJ45' THEN ARRAY['RJ45','Copper']
ELSE ARRAY[]::text[]
END
)
-- speed must not exceed the port's speed (slower module in faster cage OK)
AND t.speed_gbps <= sp.port_speed
) )
ORDER BY t.speed_gbps DESC NULLS LAST, t.reach_meters ASC NULLS LAST`, ORDER BY t.speed_gbps DESC NULLS LAST, t.reach_meters ASC NULLS LAST`,
[switchId] [switchId]