Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 79 additions & 53 deletions osism/tasks/conductor/sonic/config_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,11 @@ def generate_sonic_config(device, hwsku, device_as_mapping=None, config_version=
if breakout_info["breakout_cfgs"]:
config["BREAKOUT_CFG"].update(breakout_info["breakout_cfgs"])
if breakout_info["breakout_ports"]:
config["BREAKOUT_PORTS"].update(breakout_info["breakout_ports"])
# Project each entry to the SONiC schema ({port: {"master": <port>}});
# the full stash (declared/lanes/speed) stays in breakout_info for the
# PORT-building helpers and must not leak into the owned table.
for child, entry in breakout_info["breakout_ports"].items():
config["BREAKOUT_PORTS"][child] = {"master": entry["master"]}

# Add port channel configuration
_add_portchannel_configuration(config, portchannel_info)
Expand Down Expand Up @@ -649,33 +653,45 @@ def _add_port_configurations(
port_speed = sonic_speed

if port_name in breakout_info["breakout_ports"]:
bp = breakout_info["breakout_ports"][port_name]
# Get the master port to determine original speed and lanes
master_port = breakout_info["breakout_ports"][port_name]["master"]
master_port = bp["master"]

# Override with individual breakout port speed from NetBox if available
if port_name in netbox_interfaces and netbox_interfaces[port_name]["speed"]:
port_speed = str(int(netbox_interfaces[port_name]["speed"]))
logger.debug(
f"Using NetBox speed {port_speed} Mbps for breakout port {port_name}"
if bp.get("declared"):
# Declared-mode stash is authoritative: it takes precedence over
# the NetBox-speed override and the inferred lane calculation.
port_speed = str(bp["speed"])
port_lanes = bp["lanes"]
else:
# Override with individual breakout port speed from NetBox if available
if (
port_name in netbox_interfaces
and netbox_interfaces[port_name]["speed"]
):
port_speed = str(int(netbox_interfaces[port_name]["speed"]))
logger.debug(
f"Using NetBox speed {port_speed} Mbps for breakout port {port_name}"
)
elif master_port in breakout_info["breakout_cfgs"]:
# Fallback to extracting speed from breakout mode
brkout_mode = breakout_info["breakout_cfgs"][master_port][
"brkout_mode"
]
if "10G" in brkout_mode:
port_speed = "10000"
elif "25G" in brkout_mode:
port_speed = "25000"
elif "50G" in brkout_mode:
port_speed = "50000"
elif "100G" in brkout_mode:
port_speed = "100000"
elif "200G" in brkout_mode:
port_speed = "200000"

# Calculate individual lane for this breakout port
port_lanes = _calculate_breakout_port_lane(
port_name, master_port, port_config
)
elif master_port in breakout_info["breakout_cfgs"]:
# Fallback to extracting speed from breakout mode
brkout_mode = breakout_info["breakout_cfgs"][master_port]["brkout_mode"]
if "10G" in brkout_mode:
port_speed = "10000"
elif "25G" in brkout_mode:
port_speed = "25000"
elif "50G" in brkout_mode:
port_speed = "50000"
elif "100G" in brkout_mode:
port_speed = "100000"
elif "200G" in brkout_mode:
port_speed = "200000"

# Calculate individual lane for this breakout port
port_lanes = _calculate_breakout_port_lane(
port_name, master_port, port_config
)

# Generate correct alias based on port name and speed
interface_speed = int(port_speed) if port_speed else None
Expand Down Expand Up @@ -857,32 +873,47 @@ def _add_missing_breakout_ports(
for port_name in breakout_info["breakout_ports"]:
if port_name not in config["PORT"]:
# Get the master port to determine configuration
master_port = breakout_info["breakout_ports"][port_name]["master"]
bp = breakout_info["breakout_ports"][port_name]
master_port = bp["master"]

# Override with individual breakout port speed from NetBox if available
# Note: netbox_interfaces speeds are already normalized to Mbps
if port_name in netbox_interfaces and netbox_interfaces[port_name]["speed"]:
port_speed = str(int(netbox_interfaces[port_name]["speed"]))
logger.debug(
f"Using NetBox speed {port_speed} Mbps for missing breakout port {port_name}"
)
elif master_port in breakout_info["breakout_cfgs"]:
# Fallback to extracting speed from breakout mode
brkout_mode = breakout_info["breakout_cfgs"][master_port]["brkout_mode"]
if "10G" in brkout_mode:
port_speed = "10000"
elif "25G" in brkout_mode:
port_speed = "25000"
elif "50G" in brkout_mode:
port_speed = "50000"
elif "100G" in brkout_mode:
port_speed = "100000"
elif "200G" in brkout_mode:
port_speed = "200000"
if bp.get("declared"):
port_speed = str(bp["speed"])
port_lanes = bp["lanes"]
else:
# Override with individual breakout port speed from NetBox if available
# Note: netbox_interfaces speeds are already normalized to Mbps
if (
port_name in netbox_interfaces
and netbox_interfaces[port_name]["speed"]
):
port_speed = str(int(netbox_interfaces[port_name]["speed"]))
logger.debug(
f"Using NetBox speed {port_speed} Mbps for missing breakout port {port_name}"
)
elif master_port in breakout_info["breakout_cfgs"]:
# Fallback to extracting speed from breakout mode
brkout_mode = breakout_info["breakout_cfgs"][master_port][
"brkout_mode"
]
if "10G" in brkout_mode:
port_speed = "10000"
elif "25G" in brkout_mode:
port_speed = "25000"
elif "50G" in brkout_mode:
port_speed = "50000"
elif "100G" in brkout_mode:
port_speed = "100000"
elif "200G" in brkout_mode:
port_speed = "200000"
else:
port_speed = "25000" # Default fallback
else:
port_speed = "25000" # Default fallback
else:
port_speed = "25000" # Default fallback

# Calculate individual lane for this breakout port
port_lanes = _calculate_breakout_port_lane(
port_name, master_port, port_config
)

# Set admin_status based on connection or port channel membership
admin_status = (
Expand All @@ -905,11 +936,6 @@ def _add_missing_breakout_ports(
if master_port in port_config:
port_index = port_config[master_port]["index"]

# Calculate individual lane for this breakout port
port_lanes = _calculate_breakout_port_lane(
port_name, master_port, port_config
)

port_data = {
"admin_status": admin_status,
"alias": correct_alias,
Expand Down
179 changes: 179 additions & 0 deletions osism/tasks/conductor/sonic/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,51 @@
_port_config_cache: dict[str, dict[str, dict[str, str]]] = {}


def get_declared_breakout_modes(device):
cf = getattr(device, "custom_fields", None)
if not isinstance(cf, dict):
return {}
sp = cf.get("sonic_parameters")
if not isinstance(sp, dict):
return {}
bk = sp.get("breakout")
return bk if isinstance(bk, dict) else {}


_MODE_RE = re.compile(r"(\d+)x(\d+)G")


def _parse_breakout_mode(mode):
if not isinstance(mode, str):
return None
m = _MODE_RE.fullmatch(mode.strip())
if not m:
return None
count, g = int(m.group(1)), int(m.group(2))
if count < 2 or g <= 0:
return None
return count, g * 1000


def _parse_lanes(lanes):
if not isinstance(lanes, str):
return []
s = lanes.strip()
if not s:
return []
try:
if "," in s:
parts = [p.strip() for p in s.split(",")]
return parts if all(p.isdigit() for p in parts) else []
if "-" in s:
a, b = s.split("-", 1)
a, b = int(a), int(b)
return [str(n) for n in range(a, b + 1)] if a <= b else []
return [s] if s.isdigit() else []
except (ValueError, TypeError):
return []


def get_speed_from_port_type(port_type):
"""Get speed from port type when speed is not provided.

Expand Down Expand Up @@ -641,6 +686,12 @@ def get_connected_interfaces(device, portchannel_info=None):
return _get_connected_interfaces(device, portchannel_info)


def _breakout_child_names(master, count, lanes_per_child):
"""Names a breakout of ``master`` into ``count`` children would occupy."""
base = int(master[len("Ethernet") :])
return [f"Ethernet{base + i * lanes_per_child}" for i in range(count)]


def _breakout_child_collisions(children, master, port_config):
"""Children that are separate ports of this HWSKU rather than free slots.

Expand All @@ -655,6 +706,49 @@ def _breakout_child_collisions(children, master, port_config):
return [c for c in children if c != master and c in port_config]


def _emit_breakout(
master,
count,
speed_mbps,
port_config,
breakout_cfgs,
breakout_ports,
suppressed_masters,
):
lanes = _parse_lanes(port_config[master]["lanes"])
lpc = len(lanes) // count
# Refuse before mutating anything: a child slot that is a port in its own
# right must keep its own configuration. Leaving breakout_cfgs unset keeps
# the master a normal port, so the whole declaration is dropped rather
# than half-applied.
children = _breakout_child_names(master, count, lpc)
collisions = _breakout_child_collisions(children, master, port_config)
if collisions:
logger.error(
f"Declared breakout {count}x{speed_mbps // 1000}G for {master} "
f"would claim {', '.join(collisions)}, which are separate ports "
f"on this HWSKU; refusing the declaration"
)
return
# Read the master index and build its breakout_cfgs entry before staging
# any children, so a missing "index" fails cleanly without leaving orphan
# breakout_ports entries behind.
master_cfg = {
"breakout_owner": "MANUAL",
"brkout_mode": f"{count}x{speed_mbps // 1000}G",
"port": f"1/{port_config[master]['index']}",
}
for i, child in enumerate(children):
breakout_ports[child] = {
"master": master,
"declared": True,
"lanes": ",".join(lanes[i * lpc : (i + 1) * lpc]),
"speed": speed_mbps,
}
breakout_cfgs[master] = master_cfg
suppressed_masters.add(master)


def detect_breakout_ports(device):
"""Detect breakout ports from NetBox device interfaces using the centralized breakout logic.

Expand Down Expand Up @@ -703,6 +797,81 @@ def detect_breakout_ports(device):
logger.warning(f"Could not load port config for {device_hwsku}: {e}")
return {"breakout_cfgs": breakout_cfgs, "breakout_ports": breakout_ports}

suppressed_masters: set = set()

# Declared-mode pass: honor explicit breakout map before inference
modes = get_declared_breakout_modes(device)
master_to_keys: dict = {}
for key, mode in modes.items():
try:
if not isinstance(key, str):
master = None
elif re.fullmatch(r"Ethernet\d+", key):
master = key
elif re.fullmatch(r"Eth1/\d+", key):
resolved = _map_interface_name_to_sonic(
key, interface_names, port_config, device_hwsku
)
master = (
resolved if re.fullmatch(r"Ethernet\d+", resolved) else None
)
else:
master = None
master_to_keys.setdefault(master, []).append((key, mode))
except Exception as e:
logger.error(f"Error normalizing declared breakout key {key!r}: {e}")

for master, key_mode_list in master_to_keys.items():
try:
if master is None:
# Multiple keys can normalize to None simply because none of
# them resolves to a known port; that is not a collision.
for key, _mode in key_mode_list:
logger.error(
f"Declared breakout key {key!r} could not be "
f"resolved to a known port"
)
continue
if len(key_mode_list) >= 2:
suppressed_masters.add(master)
logger.error(
f"Declared breakout collision for {master}: "
f"keys {[k for k, _ in key_mode_list]}"
)
continue
key, mode = key_mode_list[0]
if master not in port_config:
logger.error(
f"Declared breakout key {key!r} could not be resolved to a known port"
)
continue
suppressed_masters.add(master)
parsed = _parse_breakout_mode(mode)
if parsed is None:
logger.error(
f"Declared breakout mode {mode!r} for {master} is invalid"
)
continue
count, speed_mbps = parsed
L = len(_parse_lanes(port_config[master]["lanes"]))
if L == 0 or L % count != 0:
logger.error(
f"Declared breakout {mode!r} for {master}: "
f"{L} lanes not divisible by {count}"
)
continue
_emit_breakout(
master,
count,
speed_mbps,
port_config,
breakout_cfgs,
breakout_ports,
suppressed_masters,
)
except Exception as e:
logger.error(f"Error processing declared breakout for {master!r}: {e}")

# Process interfaces that match breakout patterns
processed_groups = set()

Expand Down Expand Up @@ -778,6 +947,9 @@ def detect_breakout_ports(device):
)
continue

if master_port in suppressed_masters:
continue

# Calculate physical port number (1/1 -> port 1, 1/2 -> port 2, etc.)
physical_port_num = f"{module}/{port}"

Expand Down Expand Up @@ -879,6 +1051,10 @@ def detect_breakout_ports(device):
if len(sonic_400g_breakout_group) == 4:
processed_groups.add(group_key_400g)
master_port = f"Ethernet{base_port_400g}"

if master_port in suppressed_masters:
continue

brkout_mode = "4x100G"

# Calculate physical port number for 400G ports
Expand Down Expand Up @@ -990,6 +1166,9 @@ def detect_breakout_ports(device):
if not brkout_mode:
continue # Skip unsupported speeds

if master_port in suppressed_masters:
continue

# Calculate physical port number (Ethernet0-3 -> port 1/1, Ethernet4-7 -> port 1/2, etc.)
physical_port_index = (base_port // 4) + 1
physical_port_num = f"1/{physical_port_index}"
Expand Down
Loading