osism 0.20250616.0__py3-none-any.whl → 0.20250621.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- osism/commands/baremetal.py +23 -3
- osism/commands/manage.py +25 -1
- osism/commands/sync.py +27 -7
- osism/settings.py +1 -0
- osism/tasks/conductor/__init__.py +2 -2
- osism/tasks/conductor/ironic.py +6 -2
- osism/tasks/conductor/sonic/__init__.py +26 -0
- osism/tasks/conductor/sonic/bgp.py +87 -0
- osism/tasks/conductor/sonic/cache.py +114 -0
- osism/tasks/conductor/sonic/config_generator.py +908 -0
- osism/tasks/conductor/sonic/connections.py +389 -0
- osism/tasks/conductor/sonic/constants.py +79 -0
- osism/tasks/conductor/sonic/device.py +82 -0
- osism/tasks/conductor/sonic/exporter.py +226 -0
- osism/tasks/conductor/sonic/interface.py +789 -0
- osism/tasks/conductor/sonic/sync.py +190 -0
- {osism-0.20250616.0.dist-info → osism-0.20250621.0.dist-info}/METADATA +2 -2
- {osism-0.20250616.0.dist-info → osism-0.20250621.0.dist-info}/RECORD +24 -15
- {osism-0.20250616.0.dist-info → osism-0.20250621.0.dist-info}/entry_points.txt +4 -0
- osism-0.20250621.0.dist-info/licenses/AUTHORS +1 -0
- osism-0.20250621.0.dist-info/pbr.json +1 -0
- osism/tasks/conductor/sonic.py +0 -1401
- osism-0.20250616.0.dist-info/licenses/AUTHORS +0 -1
- osism-0.20250616.0.dist-info/pbr.json +0 -1
- {osism-0.20250616.0.dist-info → osism-0.20250621.0.dist-info}/WHEEL +0 -0
- {osism-0.20250616.0.dist-info → osism-0.20250621.0.dist-info}/licenses/LICENSE +0 -0
- {osism-0.20250616.0.dist-info → osism-0.20250621.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,789 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
|
3
|
+
"""Interface conversion and port detection functions for SONiC configuration."""
|
4
|
+
|
5
|
+
import copy
|
6
|
+
import os
|
7
|
+
import re
|
8
|
+
from loguru import logger
|
9
|
+
|
10
|
+
from .constants import PORT_TYPE_TO_SPEED_MAP, HIGH_SPEED_PORTS, PORT_CONFIG_PATH
|
11
|
+
from .cache import get_cached_device_interfaces
|
12
|
+
|
13
|
+
# Global cache for port configurations to avoid repeated file reads
|
14
|
+
_port_config_cache: dict[str, dict[str, dict[str, str]]] = {}
|
15
|
+
|
16
|
+
|
17
|
+
def get_speed_from_port_type(port_type):
|
18
|
+
"""Get speed from port type when speed is not provided.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
port_type: NetBox interface type value (e.g., "10gbase-x-sfpp", "100gbase-x-qsfp28")
|
22
|
+
|
23
|
+
Returns:
|
24
|
+
int: Speed in Mbps, or None if port type is not recognized
|
25
|
+
"""
|
26
|
+
if not port_type:
|
27
|
+
return None
|
28
|
+
|
29
|
+
# Convert to lowercase for case-insensitive matching
|
30
|
+
port_type_lower = str(port_type).lower()
|
31
|
+
|
32
|
+
# Try to get speed from mapping
|
33
|
+
speed = PORT_TYPE_TO_SPEED_MAP.get(port_type_lower)
|
34
|
+
|
35
|
+
if speed:
|
36
|
+
logger.debug(f"Resolved port type '{port_type}' to speed {speed} Mbps")
|
37
|
+
else:
|
38
|
+
logger.warning(f"Unknown port type '{port_type}', unable to determine speed")
|
39
|
+
|
40
|
+
return speed
|
41
|
+
|
42
|
+
|
43
|
+
def convert_netbox_interface_to_sonic(device_interface, device=None):
|
44
|
+
"""Convert NetBox interface name to SONiC interface name with device-specific mapping.
|
45
|
+
|
46
|
+
Args:
|
47
|
+
device_interface: NetBox interface object or interface name string
|
48
|
+
device: NetBox device object (required if device_interface is string)
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
str: SONiC interface name (e.g., "Ethernet0", "Ethernet4")
|
52
|
+
"""
|
53
|
+
# Extract interface name and determine device context
|
54
|
+
if isinstance(device_interface, str):
|
55
|
+
# Legacy mode: interface name as string
|
56
|
+
interface_name = device_interface
|
57
|
+
if device is None:
|
58
|
+
logger.warning(
|
59
|
+
"Device object required when interface is provided as string"
|
60
|
+
)
|
61
|
+
return interface_name
|
62
|
+
else:
|
63
|
+
# New mode: interface object
|
64
|
+
interface_name = device_interface.name
|
65
|
+
if device is None:
|
66
|
+
logger.warning(
|
67
|
+
"Device object required for device-specific interface mapping"
|
68
|
+
)
|
69
|
+
return interface_name
|
70
|
+
|
71
|
+
# Check if this is already in SONiC format (Ethernet*)
|
72
|
+
if interface_name.startswith("Ethernet"):
|
73
|
+
return interface_name
|
74
|
+
|
75
|
+
# Get HWSKU from device sonic_parameters
|
76
|
+
device_hwsku = None
|
77
|
+
if (
|
78
|
+
hasattr(device, "custom_fields")
|
79
|
+
and "sonic_parameters" in device.custom_fields
|
80
|
+
and device.custom_fields["sonic_parameters"]
|
81
|
+
and "hwsku" in device.custom_fields["sonic_parameters"]
|
82
|
+
):
|
83
|
+
device_hwsku = device.custom_fields["sonic_parameters"]["hwsku"]
|
84
|
+
|
85
|
+
if not device_hwsku:
|
86
|
+
logger.warning(f"No HWSKU found for device {device.name}")
|
87
|
+
return interface_name
|
88
|
+
|
89
|
+
# Get all device interfaces for breakout detection (using cache)
|
90
|
+
try:
|
91
|
+
all_interfaces = get_cached_device_interfaces(device.id)
|
92
|
+
interface_names = [iface.name for iface in all_interfaces]
|
93
|
+
except Exception as e:
|
94
|
+
logger.warning(f"Could not fetch device interfaces: {e}")
|
95
|
+
return interface_name
|
96
|
+
|
97
|
+
# Get port configuration for HWSKU
|
98
|
+
try:
|
99
|
+
port_config = get_port_config(device_hwsku)
|
100
|
+
if not port_config:
|
101
|
+
logger.warning(f"No port config found for HWSKU {device_hwsku}")
|
102
|
+
return interface_name
|
103
|
+
except Exception as e:
|
104
|
+
logger.warning(f"Could not load port config for {device_hwsku}: {e}")
|
105
|
+
return interface_name
|
106
|
+
|
107
|
+
# Handle different interface naming patterns
|
108
|
+
return _map_interface_name_to_sonic(
|
109
|
+
interface_name, interface_names, port_config, device_hwsku
|
110
|
+
)
|
111
|
+
|
112
|
+
|
113
|
+
def _map_interface_name_to_sonic(
|
114
|
+
interface_name, all_interface_names, port_config, device_hwsku
|
115
|
+
):
|
116
|
+
"""Map interface name to SONiC format based on port config and breakout detection.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
interface_name: The interface name to map
|
120
|
+
all_interface_names: List of all interface names on the device
|
121
|
+
port_config: Port configuration dictionary from HWSKU
|
122
|
+
device_hwsku: Hardware SKU name for logging
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
str: SONiC interface name
|
126
|
+
"""
|
127
|
+
# Check for EthX/Y/Z format (potential breakout)
|
128
|
+
breakout_match = re.match(r"Eth(\d+)/(\d+)/(\d+)", interface_name)
|
129
|
+
if breakout_match:
|
130
|
+
return _handle_breakout_interface(
|
131
|
+
interface_name, all_interface_names, port_config, device_hwsku
|
132
|
+
)
|
133
|
+
|
134
|
+
# Check for EthX/Y format (standard format)
|
135
|
+
standard_match = re.match(r"Eth(\d+)/(\d+)", interface_name)
|
136
|
+
if standard_match:
|
137
|
+
return _handle_standard_interface(interface_name, port_config, device_hwsku)
|
138
|
+
|
139
|
+
# For any other format, try to find by alias in port config
|
140
|
+
for sonic_port, config in port_config.items():
|
141
|
+
if config.get("alias") == interface_name:
|
142
|
+
logger.debug(f"Found {interface_name} -> {sonic_port} via alias mapping")
|
143
|
+
return sonic_port
|
144
|
+
|
145
|
+
logger.warning(
|
146
|
+
f"Could not map interface {interface_name} using HWSKU {device_hwsku}"
|
147
|
+
)
|
148
|
+
return interface_name
|
149
|
+
|
150
|
+
|
151
|
+
def _handle_breakout_interface(
|
152
|
+
interface_name, all_interface_names, port_config, device_hwsku
|
153
|
+
):
|
154
|
+
"""Handle EthX/Y/Z format interfaces with breakout detection.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
interface_name: Interface name in EthX/Y/Z format
|
158
|
+
all_interface_names: List of all interface names on the device
|
159
|
+
port_config: Port configuration dictionary
|
160
|
+
device_hwsku: Hardware SKU name for logging
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
str: SONiC interface name
|
164
|
+
"""
|
165
|
+
match = re.match(r"Eth(\d+)/(\d+)/(\d+)", interface_name)
|
166
|
+
if not match:
|
167
|
+
return interface_name
|
168
|
+
|
169
|
+
module = int(match.group(1))
|
170
|
+
port = int(match.group(2))
|
171
|
+
subport = int(match.group(3))
|
172
|
+
|
173
|
+
# Find all interfaces with same module and port (potential breakout group)
|
174
|
+
breakout_group = []
|
175
|
+
for iface_name in all_interface_names:
|
176
|
+
breakout_match = re.match(r"Eth(\d+)/(\d+)/(\d+)", iface_name)
|
177
|
+
if breakout_match:
|
178
|
+
iface_module = int(breakout_match.group(1))
|
179
|
+
iface_port = int(breakout_match.group(2))
|
180
|
+
iface_subport = int(breakout_match.group(3))
|
181
|
+
|
182
|
+
if iface_module == module and iface_port == port:
|
183
|
+
breakout_group.append((iface_subport, iface_name))
|
184
|
+
|
185
|
+
# Check if this is a breakout port (more than one interface with same module/port)
|
186
|
+
if len(breakout_group) > 1:
|
187
|
+
# Sort by subport number
|
188
|
+
breakout_group.sort(key=lambda x: x[0])
|
189
|
+
|
190
|
+
# Find the alias for the interface with the smallest subport
|
191
|
+
min_subport_interface = breakout_group[0][1]
|
192
|
+
|
193
|
+
# Map the min subport interface to find base SONiC name
|
194
|
+
base_sonic_name = _find_sonic_name_by_alias_mapping(
|
195
|
+
min_subport_interface, port_config
|
196
|
+
)
|
197
|
+
if base_sonic_name:
|
198
|
+
# Extract base port number (e.g., "Ethernet0" -> 0)
|
199
|
+
base_match = re.match(r"Ethernet(\d+)", base_sonic_name)
|
200
|
+
if base_match:
|
201
|
+
base_port_num = int(base_match.group(1))
|
202
|
+
|
203
|
+
# Calculate offset from minimum subport
|
204
|
+
min_subport = breakout_group[0][0]
|
205
|
+
current_offset = subport - min_subport
|
206
|
+
|
207
|
+
sonic_port_num = base_port_num + current_offset
|
208
|
+
result = f"Ethernet{sonic_port_num}"
|
209
|
+
|
210
|
+
logger.debug(
|
211
|
+
f"Breakout mapping: {interface_name} -> {result} (base: {base_sonic_name}, offset: {current_offset})"
|
212
|
+
)
|
213
|
+
return result
|
214
|
+
|
215
|
+
# Not a breakout or couldn't find base mapping, try direct alias mapping
|
216
|
+
return _find_sonic_name_by_alias_mapping(interface_name, port_config)
|
217
|
+
|
218
|
+
|
219
|
+
def _handle_standard_interface(interface_name, port_config, device_hwsku):
|
220
|
+
"""Handle EthX/Y format interfaces.
|
221
|
+
|
222
|
+
Args:
|
223
|
+
interface_name: Interface name in EthX/Y format
|
224
|
+
port_config: Port configuration dictionary
|
225
|
+
device_hwsku: Hardware SKU name for logging
|
226
|
+
|
227
|
+
Returns:
|
228
|
+
str: SONiC interface name
|
229
|
+
"""
|
230
|
+
return _find_sonic_name_by_alias_mapping(interface_name, port_config)
|
231
|
+
|
232
|
+
|
233
|
+
def _find_sonic_name_by_alias_mapping(interface_name, port_config):
|
234
|
+
"""Find SONiC interface name by mapping through alias in port config.
|
235
|
+
|
236
|
+
The mapping works as follows:
|
237
|
+
- tenGigE1 alias maps to Eth1/1/1 or Eth1/1
|
238
|
+
- tenGigE48 alias maps to Eth1/48/1 or Eth1/48
|
239
|
+
- hundredGigE49 alias maps to Eth1/49/1 or Eth1/49
|
240
|
+
|
241
|
+
Args:
|
242
|
+
interface_name: NetBox interface name (e.g., "Eth1/1" or "Eth1/1/1")
|
243
|
+
port_config: Port configuration dictionary
|
244
|
+
|
245
|
+
Returns:
|
246
|
+
str: SONiC interface name or original name if not found
|
247
|
+
"""
|
248
|
+
# Create reverse mapping: expected NetBox name -> alias -> SONiC name
|
249
|
+
for sonic_port, config in port_config.items():
|
250
|
+
alias = config.get("alias", "")
|
251
|
+
if not alias:
|
252
|
+
continue
|
253
|
+
|
254
|
+
# Extract number from alias (e.g., tenGigE1 -> 1, hundredGigE49 -> 49)
|
255
|
+
alias_match = re.search(r"(\d+)$", alias)
|
256
|
+
if not alias_match:
|
257
|
+
continue
|
258
|
+
|
259
|
+
alias_num = int(alias_match.group(1))
|
260
|
+
|
261
|
+
# Generate expected NetBox interface names for this alias
|
262
|
+
expected_names = [
|
263
|
+
f"Eth1/{alias_num}", # Standard format
|
264
|
+
f"Eth1/{alias_num}/1", # Breakout format (first subport)
|
265
|
+
]
|
266
|
+
|
267
|
+
if interface_name in expected_names:
|
268
|
+
logger.debug(
|
269
|
+
f"Alias mapping: {interface_name} -> {sonic_port} via alias {alias}"
|
270
|
+
)
|
271
|
+
return sonic_port
|
272
|
+
|
273
|
+
logger.warning(f"No alias mapping found for {interface_name}")
|
274
|
+
return interface_name
|
275
|
+
|
276
|
+
|
277
|
+
def convert_sonic_interface_to_alias(
|
278
|
+
sonic_interface_name, interface_speed=None, is_breakout=False
|
279
|
+
):
|
280
|
+
"""Convert SONiC interface name to NetBox-style alias.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
sonic_interface_name: SONiC interface name (e.g., "Ethernet0", "Ethernet4")
|
284
|
+
interface_speed: Interface speed in Mbps (optional, for speed-based calculation)
|
285
|
+
is_breakout: Whether this is a breakout port (adds subport notation)
|
286
|
+
|
287
|
+
Returns:
|
288
|
+
str: NetBox-style alias (e.g., "Eth1/1", "Eth1/2" or "Eth1/1/1", "Eth1/1/2" for breakout)
|
289
|
+
|
290
|
+
Examples:
|
291
|
+
- Regular 100G ports: Ethernet0 -> Eth1/1, Ethernet4 -> Eth1/2, Ethernet8 -> Eth1/3
|
292
|
+
- Regular other speeds: Ethernet0 -> Eth1/1, Ethernet1 -> Eth1/2, Ethernet2 -> Eth1/3
|
293
|
+
- Breakout ports: Ethernet0 -> Eth1/1/1, Ethernet1 -> Eth1/1/2, Ethernet2 -> Eth1/1/3, Ethernet3 -> Eth1/1/4
|
294
|
+
"""
|
295
|
+
# Extract port number from SONiC format (Ethernet0, Ethernet4, etc.)
|
296
|
+
match = re.match(r"Ethernet(\d+)", sonic_interface_name)
|
297
|
+
if not match:
|
298
|
+
# If it doesn't match expected pattern, return as-is
|
299
|
+
return sonic_interface_name
|
300
|
+
|
301
|
+
sonic_port_number = int(match.group(1))
|
302
|
+
|
303
|
+
if is_breakout:
|
304
|
+
# For breakout ports: Ethernet0 -> Eth1/1/1, Ethernet1 -> Eth1/1/2, etc.
|
305
|
+
# Calculate base port (master port) and subport number
|
306
|
+
base_port = (sonic_port_number // 4) * 4 # Get base port (0, 4, 8, 12, ...)
|
307
|
+
subport = (sonic_port_number % 4) + 1 # Get subport number (1, 2, 3, 4)
|
308
|
+
|
309
|
+
# Calculate physical port number for the base port
|
310
|
+
physical_port = (base_port // 4) + 1 # Convert to 1-based indexing
|
311
|
+
|
312
|
+
# Assume module 1 for now - could be extended for multi-module systems
|
313
|
+
module = 1
|
314
|
+
|
315
|
+
return f"Eth{module}/{physical_port}/{subport}"
|
316
|
+
else:
|
317
|
+
# For regular ports: use speed-based calculation
|
318
|
+
# Determine speed category and multiplier
|
319
|
+
if interface_speed and interface_speed in HIGH_SPEED_PORTS:
|
320
|
+
# High-speed ports use 4x multiplier (lanes)
|
321
|
+
multiplier = 4
|
322
|
+
else:
|
323
|
+
# Default for 1G, 10G, 25G ports - sequential numbering
|
324
|
+
multiplier = 1
|
325
|
+
|
326
|
+
# Calculate physical port number
|
327
|
+
physical_port = (
|
328
|
+
sonic_port_number // multiplier
|
329
|
+
) + 1 # Convert to 1-based indexing
|
330
|
+
|
331
|
+
# Assume module 1 for now - could be extended for multi-module systems
|
332
|
+
module = 1
|
333
|
+
|
334
|
+
return f"Eth{module}/{physical_port}"
|
335
|
+
|
336
|
+
|
337
|
+
def get_port_config(hwsku):
|
338
|
+
"""Get port configuration for a given HWSKU. Uses caching to avoid repeated file reads.
|
339
|
+
|
340
|
+
Args:
|
341
|
+
hwsku: Hardware SKU name (e.g., 'Accton-AS5835-54T')
|
342
|
+
|
343
|
+
Returns:
|
344
|
+
dict: Port configuration with port names as keys and their properties as values
|
345
|
+
Example: {'Ethernet0': {'lanes': '2', 'alias': 'tenGigE1', 'index': '1', 'speed': '10000', 'valid_speeds': '10000,25000'}}
|
346
|
+
"""
|
347
|
+
global _port_config_cache # noqa F824
|
348
|
+
|
349
|
+
# Check if already cached
|
350
|
+
if hwsku in _port_config_cache:
|
351
|
+
logger.debug(f"Using cached port config for HWSKU {hwsku}")
|
352
|
+
# Return a deep copy to ensure isolation between devices
|
353
|
+
return copy.deepcopy(_port_config_cache[hwsku])
|
354
|
+
|
355
|
+
port_config = {}
|
356
|
+
config_path = f"{PORT_CONFIG_PATH}/{hwsku}.ini"
|
357
|
+
|
358
|
+
if not os.path.exists(config_path):
|
359
|
+
logger.error(f"Port config file not found: {config_path}")
|
360
|
+
# Cache empty config to avoid repeated file system checks
|
361
|
+
_port_config_cache[hwsku] = port_config
|
362
|
+
return port_config
|
363
|
+
|
364
|
+
try:
|
365
|
+
with open(config_path, "r") as f:
|
366
|
+
for line in f:
|
367
|
+
line = line.strip()
|
368
|
+
# Skip comments and empty lines
|
369
|
+
if not line or line.startswith("#"):
|
370
|
+
continue
|
371
|
+
|
372
|
+
parts = line.split()
|
373
|
+
if len(parts) >= 5:
|
374
|
+
port_name = parts[0]
|
375
|
+
port_config[port_name] = {
|
376
|
+
"lanes": parts[1],
|
377
|
+
"alias": parts[2],
|
378
|
+
"index": parts[3],
|
379
|
+
"speed": parts[4],
|
380
|
+
}
|
381
|
+
# Check for optional valid_speeds column (6th column)
|
382
|
+
if len(parts) >= 6:
|
383
|
+
port_config[port_name]["valid_speeds"] = parts[5]
|
384
|
+
|
385
|
+
# Cache the loaded configuration
|
386
|
+
_port_config_cache[hwsku] = port_config
|
387
|
+
logger.debug(
|
388
|
+
f"Cached port config for HWSKU {hwsku} with {len(port_config)} ports"
|
389
|
+
)
|
390
|
+
|
391
|
+
except Exception as e:
|
392
|
+
logger.error(f"Error parsing port config file {config_path}: {e}")
|
393
|
+
# Cache empty config on error to avoid repeated attempts
|
394
|
+
_port_config_cache[hwsku] = port_config
|
395
|
+
|
396
|
+
# Return a deep copy to ensure isolation between devices
|
397
|
+
return copy.deepcopy(port_config)
|
398
|
+
|
399
|
+
|
400
|
+
def clear_port_config_cache():
|
401
|
+
"""Clear the port configuration cache. Should be called at the start of sync_sonic."""
|
402
|
+
global _port_config_cache
|
403
|
+
_port_config_cache = {}
|
404
|
+
logger.debug("Cleared port configuration cache")
|
405
|
+
|
406
|
+
|
407
|
+
# Deprecated: Use connections.get_connected_interfaces instead
|
408
|
+
# This function is kept for backward compatibility but delegates to the new module
|
409
|
+
def get_connected_interfaces(device, portchannel_info=None):
|
410
|
+
"""Get list of interface names that are connected to other devices.
|
411
|
+
|
412
|
+
Args:
|
413
|
+
device: NetBox device object
|
414
|
+
portchannel_info: Optional port channel info dict from detect_port_channels
|
415
|
+
|
416
|
+
Returns:
|
417
|
+
tuple: (set of connected interfaces, set of connected port channels)
|
418
|
+
"""
|
419
|
+
# Import here to avoid circular imports
|
420
|
+
from .connections import get_connected_interfaces as _get_connected_interfaces
|
421
|
+
|
422
|
+
return _get_connected_interfaces(device, portchannel_info)
|
423
|
+
|
424
|
+
|
425
|
+
def detect_breakout_ports(device):
|
426
|
+
"""Detect breakout ports from NetBox device interfaces using the centralized breakout logic.
|
427
|
+
|
428
|
+
Args:
|
429
|
+
device: NetBox device object
|
430
|
+
|
431
|
+
Returns:
|
432
|
+
dict: Dictionary with breakout port information
|
433
|
+
{
|
434
|
+
'breakout_cfgs': {port_name: {'brkout_mode': mode, 'port': port}},
|
435
|
+
'breakout_ports': {port_name: {'master': master_port}}
|
436
|
+
}
|
437
|
+
"""
|
438
|
+
breakout_cfgs = {}
|
439
|
+
breakout_ports = {}
|
440
|
+
|
441
|
+
try:
|
442
|
+
# Get all interfaces for the device (using cache)
|
443
|
+
interfaces = get_cached_device_interfaces(device.id)
|
444
|
+
interface_names = [iface.name for iface in interfaces]
|
445
|
+
|
446
|
+
# Get HWSKU for port config
|
447
|
+
device_hwsku = None
|
448
|
+
if (
|
449
|
+
hasattr(device, "custom_fields")
|
450
|
+
and "sonic_parameters" in device.custom_fields
|
451
|
+
and device.custom_fields["sonic_parameters"]
|
452
|
+
and "hwsku" in device.custom_fields["sonic_parameters"]
|
453
|
+
):
|
454
|
+
device_hwsku = device.custom_fields["sonic_parameters"]["hwsku"]
|
455
|
+
|
456
|
+
if not device_hwsku:
|
457
|
+
logger.warning(f"No HWSKU found for device {device.name}")
|
458
|
+
return {"breakout_cfgs": breakout_cfgs, "breakout_ports": breakout_ports}
|
459
|
+
|
460
|
+
# Get port configuration for the HWSKU
|
461
|
+
try:
|
462
|
+
port_config = get_port_config(device_hwsku)
|
463
|
+
if not port_config:
|
464
|
+
logger.warning(f"No port config found for HWSKU {device_hwsku}")
|
465
|
+
return {
|
466
|
+
"breakout_cfgs": breakout_cfgs,
|
467
|
+
"breakout_ports": breakout_ports,
|
468
|
+
}
|
469
|
+
except Exception as e:
|
470
|
+
logger.warning(f"Could not load port config for {device_hwsku}: {e}")
|
471
|
+
return {"breakout_cfgs": breakout_cfgs, "breakout_ports": breakout_ports}
|
472
|
+
|
473
|
+
# Process interfaces that match breakout patterns
|
474
|
+
processed_groups = set()
|
475
|
+
|
476
|
+
for interface in interfaces:
|
477
|
+
interface_name = interface.name
|
478
|
+
|
479
|
+
# Check for EthX/Y/Z format (NetBox breakout notation)
|
480
|
+
breakout_match = re.match(r"Eth(\d+)/(\d+)/(\d+)", interface_name)
|
481
|
+
if breakout_match:
|
482
|
+
module = int(breakout_match.group(1))
|
483
|
+
port = int(breakout_match.group(2))
|
484
|
+
subport = int(breakout_match.group(3))
|
485
|
+
|
486
|
+
# Create group key to avoid processing the same group multiple times
|
487
|
+
group_key = f"{module}/{port}"
|
488
|
+
if group_key in processed_groups:
|
489
|
+
continue
|
490
|
+
processed_groups.add(group_key)
|
491
|
+
|
492
|
+
# Use the centralized breakout logic
|
493
|
+
sonic_name = _handle_breakout_interface(
|
494
|
+
interface_name, interface_names, port_config, device_hwsku
|
495
|
+
)
|
496
|
+
|
497
|
+
# If the breakout logic returned a valid SONiC name, we have a breakout group
|
498
|
+
if sonic_name.startswith("Ethernet") and sonic_name != interface_name:
|
499
|
+
# Find all interfaces in this breakout group
|
500
|
+
breakout_group = []
|
501
|
+
for iface in interfaces:
|
502
|
+
iface_match = re.match(r"Eth(\d+)/(\d+)/(\d+)", iface.name)
|
503
|
+
if iface_match:
|
504
|
+
iface_module = int(iface_match.group(1))
|
505
|
+
iface_port = int(iface_match.group(2))
|
506
|
+
iface_subport = int(iface_match.group(3))
|
507
|
+
|
508
|
+
if iface_module == module and iface_port == port:
|
509
|
+
breakout_group.append((iface_subport, iface))
|
510
|
+
|
511
|
+
# Check if we have a valid breakout group (more than one interface)
|
512
|
+
if len(breakout_group) > 1:
|
513
|
+
# Sort by subport number
|
514
|
+
breakout_group.sort(key=lambda x: x[0])
|
515
|
+
|
516
|
+
# Find the master port (interface with smallest subport)
|
517
|
+
min_subport_interface = breakout_group[0][1]
|
518
|
+
master_sonic_name = _handle_breakout_interface(
|
519
|
+
min_subport_interface.name,
|
520
|
+
interface_names,
|
521
|
+
port_config,
|
522
|
+
device_hwsku,
|
523
|
+
)
|
524
|
+
|
525
|
+
if master_sonic_name.startswith("Ethernet"):
|
526
|
+
# Extract base port number
|
527
|
+
base_match = re.match(r"Ethernet(\d+)", master_sonic_name)
|
528
|
+
if base_match:
|
529
|
+
base_port_num = int(base_match.group(1))
|
530
|
+
master_port = f"Ethernet{base_port_num}"
|
531
|
+
|
532
|
+
# Determine breakout mode based on number of subports and speed
|
533
|
+
num_subports = len(breakout_group)
|
534
|
+
interface_speed = getattr(
|
535
|
+
breakout_group[0][1], "speed", None
|
536
|
+
)
|
537
|
+
if (
|
538
|
+
not interface_speed
|
539
|
+
and hasattr(breakout_group[0][1], "type")
|
540
|
+
and breakout_group[0][1].type
|
541
|
+
):
|
542
|
+
interface_speed = get_speed_from_port_type(
|
543
|
+
breakout_group[0][1].type.value
|
544
|
+
)
|
545
|
+
|
546
|
+
# Calculate breakout mode
|
547
|
+
if interface_speed == 25000 and num_subports == 4:
|
548
|
+
brkout_mode = "4x25G"
|
549
|
+
elif interface_speed == 50000 and num_subports == 4:
|
550
|
+
brkout_mode = "4x50G"
|
551
|
+
elif interface_speed == 100000 and num_subports == 4:
|
552
|
+
brkout_mode = "4x100G"
|
553
|
+
elif interface_speed == 200000 and num_subports == 4:
|
554
|
+
brkout_mode = "4x200G"
|
555
|
+
else:
|
556
|
+
logger.debug(
|
557
|
+
f"Unsupported breakout configuration: {num_subports} ports at {interface_speed} Mbps"
|
558
|
+
)
|
559
|
+
continue
|
560
|
+
|
561
|
+
# Add breakout config for master port
|
562
|
+
breakout_cfgs[master_port] = {
|
563
|
+
"brkout_mode": brkout_mode,
|
564
|
+
}
|
565
|
+
|
566
|
+
# Add all subports to breakout_ports
|
567
|
+
min_subport = breakout_group[0][0]
|
568
|
+
for subport, iface in breakout_group:
|
569
|
+
current_offset = subport - min_subport
|
570
|
+
sonic_port_num = base_port_num + current_offset
|
571
|
+
port_name = f"Ethernet{sonic_port_num}"
|
572
|
+
breakout_ports[port_name] = {"master": master_port}
|
573
|
+
|
574
|
+
logger.debug(
|
575
|
+
f"Detected breakout group: {group_key} -> {master_port} ({brkout_mode}) with {len(breakout_group)} ports"
|
576
|
+
)
|
577
|
+
|
578
|
+
# Also check for SONiC format breakout (Ethernet0, Ethernet1, Ethernet2, Ethernet3)
|
579
|
+
# Only process SONiC breakout if we have explicitly configured breakout ports in NetBox,
|
580
|
+
# not automatically assume consecutive Ethernet ports are breakouts
|
581
|
+
sonic_match = re.match(r"Ethernet(\d+)", interface_name)
|
582
|
+
if sonic_match:
|
583
|
+
port_num = int(sonic_match.group(1))
|
584
|
+
# Check if this could be part of a breakout group (consecutive Ethernet ports)
|
585
|
+
base_port = (port_num // 4) * 4
|
586
|
+
group_key = f"sonic_{base_port}"
|
587
|
+
|
588
|
+
if group_key in processed_groups:
|
589
|
+
continue
|
590
|
+
processed_groups.add(group_key)
|
591
|
+
|
592
|
+
# Find potential breakout group (4 consecutive Ethernet ports)
|
593
|
+
sonic_breakout_group = []
|
594
|
+
for i in range(4):
|
595
|
+
ethernet_name = f"Ethernet{base_port + i}"
|
596
|
+
for iface in interfaces:
|
597
|
+
if iface.name == ethernet_name:
|
598
|
+
# Check if this interface has a speed that suggests breakout
|
599
|
+
iface_speed = getattr(iface, "speed", None)
|
600
|
+
if (
|
601
|
+
not iface_speed
|
602
|
+
and hasattr(iface, "type")
|
603
|
+
and iface.type
|
604
|
+
):
|
605
|
+
iface_speed = get_speed_from_port_type(iface.type.value)
|
606
|
+
|
607
|
+
# Only consider as breakout if speed is 50G or less AND we have 4 consecutive ports
|
608
|
+
# This prevents regular 100G ports from being treated as breakout ports
|
609
|
+
if (
|
610
|
+
iface_speed and iface_speed <= 50000
|
611
|
+
): # 50G or less suggests breakout
|
612
|
+
sonic_breakout_group.append((base_port + i, iface))
|
613
|
+
break
|
614
|
+
|
615
|
+
# If we found 4 consecutive interfaces with true breakout speeds (≤50G)
|
616
|
+
if len(sonic_breakout_group) == 4:
|
617
|
+
master_port = f"Ethernet{base_port}"
|
618
|
+
|
619
|
+
# Determine breakout mode based on speed
|
620
|
+
interface_speed = getattr(sonic_breakout_group[0][1], "speed", None)
|
621
|
+
if (
|
622
|
+
not interface_speed
|
623
|
+
and hasattr(sonic_breakout_group[0][1], "type")
|
624
|
+
and sonic_breakout_group[0][1].type
|
625
|
+
):
|
626
|
+
interface_speed = get_speed_from_port_type(
|
627
|
+
sonic_breakout_group[0][1].type.value
|
628
|
+
)
|
629
|
+
|
630
|
+
if interface_speed == 25000:
|
631
|
+
brkout_mode = "4x25G"
|
632
|
+
elif interface_speed == 50000:
|
633
|
+
brkout_mode = "4x50G"
|
634
|
+
else:
|
635
|
+
continue # Skip unsupported speeds
|
636
|
+
|
637
|
+
# Calculate physical port number
|
638
|
+
physical_port_num = (base_port // 4) + 1
|
639
|
+
|
640
|
+
# Add breakout config for master port
|
641
|
+
breakout_cfgs[master_port] = {
|
642
|
+
"brkout_mode": brkout_mode,
|
643
|
+
}
|
644
|
+
|
645
|
+
# Add all ports to breakout_ports
|
646
|
+
for port_num, iface in sonic_breakout_group:
|
647
|
+
port_name = f"Ethernet{port_num}"
|
648
|
+
breakout_ports[port_name] = {"master": master_port}
|
649
|
+
|
650
|
+
logger.debug(
|
651
|
+
f"Detected SONiC breakout group: Ethernet{base_port}-{base_port + 3} -> {master_port} ({brkout_mode})"
|
652
|
+
)
|
653
|
+
|
654
|
+
except Exception as e:
|
655
|
+
logger.warning(f"Could not detect breakout ports for device {device.name}: {e}")
|
656
|
+
|
657
|
+
return {"breakout_cfgs": breakout_cfgs, "breakout_ports": breakout_ports}
|
658
|
+
|
659
|
+
|
660
|
+
def detect_port_channels(device):
|
661
|
+
"""Detect port channels (LAGs) from NetBox device interfaces.
|
662
|
+
|
663
|
+
Args:
|
664
|
+
device: NetBox device object
|
665
|
+
|
666
|
+
Returns:
|
667
|
+
dict: Dictionary with port channel information
|
668
|
+
{
|
669
|
+
'portchannels': {
|
670
|
+
'PortChannel1': {
|
671
|
+
'members': ['Ethernet120', 'Ethernet124'],
|
672
|
+
'admin_status': 'up',
|
673
|
+
'fast_rate': 'true',
|
674
|
+
'min_links': '1',
|
675
|
+
'mtu': '9100'
|
676
|
+
}
|
677
|
+
},
|
678
|
+
'member_mapping': {
|
679
|
+
'Ethernet120': 'PortChannel1',
|
680
|
+
'Ethernet124': 'PortChannel1'
|
681
|
+
}
|
682
|
+
}
|
683
|
+
"""
|
684
|
+
portchannels = {}
|
685
|
+
member_mapping = {}
|
686
|
+
|
687
|
+
try:
|
688
|
+
# Get all interfaces for the device (using cache)
|
689
|
+
interfaces = get_cached_device_interfaces(device.id)
|
690
|
+
|
691
|
+
# First pass: find LAG interfaces
|
692
|
+
lag_interfaces = []
|
693
|
+
for interface in interfaces:
|
694
|
+
# Check if this is a LAG interface
|
695
|
+
if hasattr(interface, "type") and interface.type:
|
696
|
+
if interface.type.value == "lag":
|
697
|
+
lag_interfaces.append(interface)
|
698
|
+
logger.debug(f"Found LAG interface: {interface.name}")
|
699
|
+
|
700
|
+
# Second pass: map members to LAGs
|
701
|
+
for interface in interfaces:
|
702
|
+
# Check if this interface has a LAG parent
|
703
|
+
if hasattr(interface, "lag") and interface.lag:
|
704
|
+
lag_parent = interface.lag
|
705
|
+
|
706
|
+
# Convert NetBox interface name to SONiC format
|
707
|
+
interface_speed = getattr(interface, "speed", None)
|
708
|
+
if (
|
709
|
+
not interface_speed
|
710
|
+
and hasattr(interface, "type")
|
711
|
+
and interface.type
|
712
|
+
):
|
713
|
+
interface_speed = get_speed_from_port_type(interface.type.value)
|
714
|
+
|
715
|
+
sonic_interface_name = convert_netbox_interface_to_sonic(
|
716
|
+
interface, device
|
717
|
+
)
|
718
|
+
|
719
|
+
# Extract port channel number from LAG name
|
720
|
+
# Common patterns: PortChannel1, Port-Channel1, LAG1, ae1, bond1
|
721
|
+
pc_number = None
|
722
|
+
if re.match(r"(?i)portchannel(\d+)", lag_parent.name):
|
723
|
+
match = re.match(r"(?i)portchannel(\d+)", lag_parent.name)
|
724
|
+
pc_number = match.group(1)
|
725
|
+
elif re.match(r"(?i)port-channel(\d+)", lag_parent.name):
|
726
|
+
match = re.match(r"(?i)port-channel(\d+)", lag_parent.name)
|
727
|
+
pc_number = match.group(1)
|
728
|
+
elif re.match(r"(?i)lag(\d+)", lag_parent.name):
|
729
|
+
match = re.match(r"(?i)lag(\d+)", lag_parent.name)
|
730
|
+
pc_number = match.group(1)
|
731
|
+
elif re.match(r"(?i)ae(\d+)", lag_parent.name):
|
732
|
+
match = re.match(r"(?i)ae(\d+)", lag_parent.name)
|
733
|
+
pc_number = match.group(1)
|
734
|
+
elif re.match(r"(?i)bond(\d+)", lag_parent.name):
|
735
|
+
match = re.match(r"(?i)bond(\d+)", lag_parent.name)
|
736
|
+
pc_number = match.group(1)
|
737
|
+
else:
|
738
|
+
# Try to extract any number from the name
|
739
|
+
numbers = re.findall(r"\d+", lag_parent.name)
|
740
|
+
if numbers:
|
741
|
+
pc_number = numbers[0]
|
742
|
+
else:
|
743
|
+
# Generate a number based on the LAG interface order
|
744
|
+
pc_number = (
|
745
|
+
str(lag_interfaces.index(lag_parent) + 1)
|
746
|
+
if lag_parent in lag_interfaces
|
747
|
+
else "1"
|
748
|
+
)
|
749
|
+
|
750
|
+
portchannel_name = f"PortChannel{pc_number}"
|
751
|
+
|
752
|
+
# Add member to mapping
|
753
|
+
member_mapping[sonic_interface_name] = portchannel_name
|
754
|
+
|
755
|
+
# Initialize port channel if not exists
|
756
|
+
if portchannel_name not in portchannels:
|
757
|
+
portchannels[portchannel_name] = {
|
758
|
+
"members": [],
|
759
|
+
"admin_status": "up",
|
760
|
+
"fast_rate": "true",
|
761
|
+
"min_links": "1",
|
762
|
+
"mtu": "9100",
|
763
|
+
}
|
764
|
+
|
765
|
+
# Add member to port channel
|
766
|
+
if (
|
767
|
+
sonic_interface_name
|
768
|
+
not in portchannels[portchannel_name]["members"]
|
769
|
+
):
|
770
|
+
portchannels[portchannel_name]["members"].append(
|
771
|
+
sonic_interface_name
|
772
|
+
)
|
773
|
+
|
774
|
+
logger.debug(
|
775
|
+
f"Added interface {sonic_interface_name} to {portchannel_name}"
|
776
|
+
)
|
777
|
+
|
778
|
+
# Sort members in each port channel for consistent ordering
|
779
|
+
for pc_name in portchannels:
|
780
|
+
portchannels[pc_name]["members"].sort(
|
781
|
+
key=lambda x: (
|
782
|
+
int(re.search(r"\d+", x).group()) if re.search(r"\d+", x) else 0
|
783
|
+
)
|
784
|
+
)
|
785
|
+
|
786
|
+
except Exception as e:
|
787
|
+
logger.warning(f"Could not detect port channels for device {device.name}: {e}")
|
788
|
+
|
789
|
+
return {"portchannels": portchannels, "member_mapping": member_mapping}
|