osism 0.20250616.0__py3-none-any.whl → 0.20250627.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/api.py +49 -5
- osism/commands/baremetal.py +23 -3
- osism/commands/manage.py +276 -1
- osism/commands/reconciler.py +8 -1
- osism/commands/sync.py +27 -7
- osism/settings.py +1 -0
- osism/tasks/conductor/__init__.py +2 -2
- osism/tasks/conductor/ironic.py +21 -19
- 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 +1000 -0
- osism/tasks/conductor/sonic/connections.py +389 -0
- osism/tasks/conductor/sonic/constants.py +80 -0
- osism/tasks/conductor/sonic/device.py +82 -0
- osism/tasks/conductor/sonic/exporter.py +226 -0
- osism/tasks/conductor/sonic/interface.py +940 -0
- osism/tasks/conductor/sonic/sync.py +215 -0
- osism/tasks/reconciler.py +12 -2
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/METADATA +3 -3
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/RECORD +27 -18
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/entry_points.txt +3 -0
- osism-0.20250627.0.dist-info/licenses/AUTHORS +1 -0
- osism-0.20250627.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.20250627.0.dist-info}/WHEEL +0 -0
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/licenses/LICENSE +0 -0
- {osism-0.20250616.0.dist-info → osism-0.20250627.0.dist-info}/top_level.txt +0 -0
osism/tasks/conductor/sonic.py
DELETED
@@ -1,1401 +0,0 @@
|
|
1
|
-
# SPDX-License-Identifier: Apache-2.0
|
2
|
-
|
3
|
-
import ipaddress
|
4
|
-
import json
|
5
|
-
import os
|
6
|
-
import re
|
7
|
-
from loguru import logger
|
8
|
-
|
9
|
-
from osism import utils
|
10
|
-
from osism import settings
|
11
|
-
from osism.tasks.conductor.netbox import (
|
12
|
-
get_device_loopbacks,
|
13
|
-
get_device_oob_ip,
|
14
|
-
get_device_vlans,
|
15
|
-
get_nb_device_query_list_sonic,
|
16
|
-
)
|
17
|
-
|
18
|
-
# Default AS prefix for local ASN calculation
|
19
|
-
DEFAULT_LOCAL_AS_PREFIX = 4200
|
20
|
-
|
21
|
-
# Port type to speed mapping (in Mbps)
|
22
|
-
PORT_TYPE_TO_SPEED_MAP = {
|
23
|
-
# RJ45/BASE-T Types
|
24
|
-
"100base-tx": 100, # 100Mbps RJ45
|
25
|
-
"1000base-t": 1000, # 1G RJ45
|
26
|
-
"2.5gbase-t": 2500, # 2.5G RJ45
|
27
|
-
"5gbase-t": 5000, # 5G RJ45
|
28
|
-
"10gbase-t": 10000, # 10G RJ45
|
29
|
-
# CX4
|
30
|
-
"10gbase-cx4": 10000, # 10G CX4
|
31
|
-
# 1G Optical
|
32
|
-
"1000base-x-gbic": 1000, # 1G GBIC
|
33
|
-
"1000base-x-sfp": 1000, # 1G SFP
|
34
|
-
# 10G Optical
|
35
|
-
"10gbase-x-sfpp": 10000, # 10G SFP+
|
36
|
-
"10gbase-x-xfp": 10000, # 10G XFP
|
37
|
-
"10gbase-x-xenpak": 10000, # 10G XENPAK
|
38
|
-
"10gbase-x-x2": 10000, # 10G X2
|
39
|
-
# 25G Optical
|
40
|
-
"25gbase-x-sfp28": 25000, # 25G SFP28
|
41
|
-
# 40G Optical
|
42
|
-
"40gbase-x-qsfpp": 40000, # 40G QSFP+
|
43
|
-
# 50G Optical
|
44
|
-
"50gbase-x-sfp28": 50000, # 50G SFP28
|
45
|
-
# 100G Optical
|
46
|
-
"100gbase-x-cfp": 100000, # 100G CFP
|
47
|
-
"100gbase-x-cfp2": 100000, # 100G CFP2
|
48
|
-
"100gbase-x-cfp4": 100000, # 100G CFP4
|
49
|
-
"100gbase-x-cpak": 100000, # 100G CPAK
|
50
|
-
"100gbase-x-qsfp28": 100000, # 100G QSFP28
|
51
|
-
# 200G Optical
|
52
|
-
"200gbase-x-cfp2": 200000, # 200G CFP2
|
53
|
-
"200gbase-x-qsfp56": 200000, # 200G QSFP56
|
54
|
-
# 400G Optical
|
55
|
-
"400gbase-x-qsfpdd": 400000, # 400G QSFP-DD
|
56
|
-
"400gbase-x-osfp": 400000, # 400G OSFP
|
57
|
-
# Virtual interface
|
58
|
-
"virtual": 0, # Virtual interface (no physical speed)
|
59
|
-
}
|
60
|
-
|
61
|
-
|
62
|
-
def calculate_local_asn_from_ipv4(
|
63
|
-
ipv4_address: str, prefix: int = DEFAULT_LOCAL_AS_PREFIX
|
64
|
-
) -> int:
|
65
|
-
"""Calculate AS number from IPv4 address.
|
66
|
-
|
67
|
-
Args:
|
68
|
-
ipv4_address: IPv4 address in format "192.168.45.123/32" or "192.168.45.123"
|
69
|
-
prefix: Four-digit prefix for AS number (default: 4200)
|
70
|
-
|
71
|
-
Returns:
|
72
|
-
AS number calculated as prefix + 3rd octet (padded) + 4th octet (padded)
|
73
|
-
Example: 192.168.45.123 with prefix 4200 -> 4200045123
|
74
|
-
|
75
|
-
Raises:
|
76
|
-
ValueError: If IP address format is invalid
|
77
|
-
"""
|
78
|
-
try:
|
79
|
-
# Remove CIDR notation if present
|
80
|
-
ip_only = ipv4_address.split("/")[0]
|
81
|
-
octets = ip_only.split(".")
|
82
|
-
|
83
|
-
if len(octets) != 4:
|
84
|
-
raise ValueError(f"Invalid IPv4 address format: {ipv4_address}")
|
85
|
-
|
86
|
-
# AS = prefix + third octet (3 digits) + fourth octet (3 digits)
|
87
|
-
# Example: 192.168.45.123 -> 4200 + 045 + 123 = 4200045123
|
88
|
-
third_octet = int(octets[2])
|
89
|
-
fourth_octet = int(octets[3])
|
90
|
-
|
91
|
-
if not (0 <= third_octet <= 255 and 0 <= fourth_octet <= 255):
|
92
|
-
raise ValueError(f"Invalid octet values in: {ipv4_address}")
|
93
|
-
|
94
|
-
return int(f"{prefix}{third_octet:03d}{fourth_octet:03d}")
|
95
|
-
except (IndexError, ValueError) as e:
|
96
|
-
raise ValueError(f"Failed to calculate AS from {ipv4_address}: {str(e)}")
|
97
|
-
|
98
|
-
|
99
|
-
def get_speed_from_port_type(port_type):
|
100
|
-
"""Get speed from port type when speed is not provided.
|
101
|
-
|
102
|
-
Args:
|
103
|
-
port_type: NetBox interface type value (e.g., "10gbase-x-sfpp", "100gbase-x-qsfp28")
|
104
|
-
|
105
|
-
Returns:
|
106
|
-
int: Speed in Mbps, or None if port type is not recognized
|
107
|
-
"""
|
108
|
-
if not port_type:
|
109
|
-
return None
|
110
|
-
|
111
|
-
# Convert to lowercase for case-insensitive matching
|
112
|
-
port_type_lower = str(port_type).lower()
|
113
|
-
|
114
|
-
# Try to get speed from mapping
|
115
|
-
speed = PORT_TYPE_TO_SPEED_MAP.get(port_type_lower)
|
116
|
-
|
117
|
-
if speed:
|
118
|
-
logger.debug(f"Resolved port type '{port_type}' to speed {speed} Mbps")
|
119
|
-
else:
|
120
|
-
logger.warning(f"Unknown port type '{port_type}', unable to determine speed")
|
121
|
-
|
122
|
-
return speed
|
123
|
-
|
124
|
-
|
125
|
-
def convert_netbox_interface_to_sonic(interface_name, interface_speed=None):
|
126
|
-
"""Convert NetBox interface name to SONiC interface name.
|
127
|
-
|
128
|
-
Args:
|
129
|
-
interface_name: NetBox interface name (e.g., "Eth1/1", "Eth1/2")
|
130
|
-
interface_speed: Interface speed in Mbps (optional, for future high-speed ports)
|
131
|
-
|
132
|
-
Returns:
|
133
|
-
str: SONiC interface name (e.g., "Ethernet0", "Ethernet4")
|
134
|
-
|
135
|
-
Examples:
|
136
|
-
- 100G ports: Eth1/1 -> Ethernet0, Eth1/2 -> Ethernet4, Eth1/3 -> Ethernet8
|
137
|
-
- Other speeds: Eth1/1 -> Ethernet0, Eth1/2 -> Ethernet1, Eth1/3 -> Ethernet2
|
138
|
-
"""
|
139
|
-
# Check if this is already in SONiC format (Ethernet*)
|
140
|
-
if interface_name.startswith("Ethernet"):
|
141
|
-
return interface_name
|
142
|
-
|
143
|
-
# Extract port number from NetBox format (Eth1/1, Eth1/2, etc.)
|
144
|
-
match = re.match(r"Eth(\d+)/(\d+)", interface_name)
|
145
|
-
if not match:
|
146
|
-
# If it doesn't match expected pattern, return as-is
|
147
|
-
return interface_name
|
148
|
-
|
149
|
-
module = int(match.group(1))
|
150
|
-
port = int(match.group(2))
|
151
|
-
|
152
|
-
# Calculate base port number (assuming module 1 starts at port 1)
|
153
|
-
port_number = port - 1 # Convert to 0-based indexing
|
154
|
-
|
155
|
-
# Determine speed category and multiplier
|
156
|
-
high_speed_ports = {
|
157
|
-
100000,
|
158
|
-
200000,
|
159
|
-
400000,
|
160
|
-
800000,
|
161
|
-
} # 100G, 200G, 400G, 800G in Mbps
|
162
|
-
|
163
|
-
if interface_speed and interface_speed in high_speed_ports:
|
164
|
-
# High-speed ports use 4x multiplier (lanes)
|
165
|
-
multiplier = 4
|
166
|
-
else:
|
167
|
-
# Default for 1G, 10G, 25G ports - sequential numbering
|
168
|
-
multiplier = 1
|
169
|
-
|
170
|
-
sonic_port_number = port_number * multiplier
|
171
|
-
|
172
|
-
return f"Ethernet{sonic_port_number}"
|
173
|
-
|
174
|
-
|
175
|
-
def convert_sonic_interface_to_alias(
|
176
|
-
sonic_interface_name, interface_speed=None, is_breakout=False
|
177
|
-
):
|
178
|
-
"""Convert SONiC interface name to NetBox-style alias.
|
179
|
-
|
180
|
-
Args:
|
181
|
-
sonic_interface_name: SONiC interface name (e.g., "Ethernet0", "Ethernet4")
|
182
|
-
interface_speed: Interface speed in Mbps (optional, for speed-based calculation)
|
183
|
-
is_breakout: Whether this is a breakout port (adds subport notation)
|
184
|
-
|
185
|
-
Returns:
|
186
|
-
str: NetBox-style alias (e.g., "Eth1/1", "Eth1/2" or "Eth1/1/1", "Eth1/1/2" for breakout)
|
187
|
-
|
188
|
-
Examples:
|
189
|
-
- Regular 100G ports: Ethernet0 -> Eth1/1, Ethernet4 -> Eth1/2, Ethernet8 -> Eth1/3
|
190
|
-
- Regular other speeds: Ethernet0 -> Eth1/1, Ethernet1 -> Eth1/2, Ethernet2 -> Eth1/3
|
191
|
-
- Breakout ports: Ethernet0 -> Eth1/1/1, Ethernet1 -> Eth1/1/2, Ethernet2 -> Eth1/1/3, Ethernet3 -> Eth1/1/4
|
192
|
-
"""
|
193
|
-
# Extract port number from SONiC format (Ethernet0, Ethernet4, etc.)
|
194
|
-
match = re.match(r"Ethernet(\d+)", sonic_interface_name)
|
195
|
-
if not match:
|
196
|
-
# If it doesn't match expected pattern, return as-is
|
197
|
-
return sonic_interface_name
|
198
|
-
|
199
|
-
sonic_port_number = int(match.group(1))
|
200
|
-
|
201
|
-
if is_breakout:
|
202
|
-
# For breakout ports: Ethernet0 -> Eth1/1/1, Ethernet1 -> Eth1/1/2, etc.
|
203
|
-
# Calculate base port (master port) and subport number
|
204
|
-
base_port = (sonic_port_number // 4) * 4 # Get base port (0, 4, 8, 12, ...)
|
205
|
-
subport = (sonic_port_number % 4) + 1 # Get subport number (1, 2, 3, 4)
|
206
|
-
|
207
|
-
# Calculate physical port number for the base port
|
208
|
-
physical_port = (base_port // 4) + 1 # Convert to 1-based indexing
|
209
|
-
|
210
|
-
# Assume module 1 for now - could be extended for multi-module systems
|
211
|
-
module = 1
|
212
|
-
|
213
|
-
return f"Eth{module}/{physical_port}/{subport}"
|
214
|
-
else:
|
215
|
-
# For regular ports: use speed-based calculation
|
216
|
-
# Determine speed category and multiplier
|
217
|
-
high_speed_ports = {
|
218
|
-
100000,
|
219
|
-
200000,
|
220
|
-
400000,
|
221
|
-
800000,
|
222
|
-
} # 100G, 200G, 400G, 800G in Mbps
|
223
|
-
|
224
|
-
if interface_speed and interface_speed in high_speed_ports:
|
225
|
-
# High-speed ports use 4x multiplier (lanes)
|
226
|
-
multiplier = 4
|
227
|
-
else:
|
228
|
-
# Default for 1G, 10G, 25G ports - sequential numbering
|
229
|
-
multiplier = 1
|
230
|
-
|
231
|
-
# Calculate physical port number
|
232
|
-
physical_port = (
|
233
|
-
sonic_port_number // multiplier
|
234
|
-
) + 1 # Convert to 1-based indexing
|
235
|
-
|
236
|
-
# Assume module 1 for now - could be extended for multi-module systems
|
237
|
-
module = 1
|
238
|
-
|
239
|
-
return f"Eth{module}/{physical_port}"
|
240
|
-
|
241
|
-
|
242
|
-
# Constants
|
243
|
-
DEFAULT_SONIC_ROLES = [
|
244
|
-
"accessleaf",
|
245
|
-
"borderleaf",
|
246
|
-
"computeleaf",
|
247
|
-
"dataleaf",
|
248
|
-
"leaf",
|
249
|
-
"serviceleaf",
|
250
|
-
"spine",
|
251
|
-
"storageleaf",
|
252
|
-
"switch",
|
253
|
-
"transferleaf",
|
254
|
-
]
|
255
|
-
|
256
|
-
DEFAULT_SONIC_VERSION = "4.5.0"
|
257
|
-
|
258
|
-
|
259
|
-
def get_device_platform(device, hwsku):
|
260
|
-
"""Get platform for device from sonic_parameters or generate from HWSKU.
|
261
|
-
|
262
|
-
Args:
|
263
|
-
device: NetBox device object
|
264
|
-
hwsku: Hardware SKU name
|
265
|
-
|
266
|
-
Returns:
|
267
|
-
str: Platform string (e.g., 'x86_64-accton_as7326_56x-r0')
|
268
|
-
"""
|
269
|
-
platform = None
|
270
|
-
if (
|
271
|
-
hasattr(device, "custom_fields")
|
272
|
-
and "sonic_parameters" in device.custom_fields
|
273
|
-
and device.custom_fields["sonic_parameters"]
|
274
|
-
and "platform" in device.custom_fields["sonic_parameters"]
|
275
|
-
):
|
276
|
-
platform = device.custom_fields["sonic_parameters"]["platform"]
|
277
|
-
|
278
|
-
if not platform:
|
279
|
-
# Generate platform from hwsku: x86_64-{hwsku_lower_with_underscores}-r0
|
280
|
-
hwsku_formatted = hwsku.lower().replace("-", "_")
|
281
|
-
platform = f"x86_64-{hwsku_formatted}-r0"
|
282
|
-
|
283
|
-
return platform
|
284
|
-
|
285
|
-
|
286
|
-
def get_device_hostname(device):
|
287
|
-
"""Get hostname for device from inventory_hostname custom field or device name.
|
288
|
-
|
289
|
-
Args:
|
290
|
-
device: NetBox device object
|
291
|
-
|
292
|
-
Returns:
|
293
|
-
str: Hostname for the device
|
294
|
-
"""
|
295
|
-
hostname = device.name
|
296
|
-
if (
|
297
|
-
hasattr(device, "custom_fields")
|
298
|
-
and "inventory_hostname" in device.custom_fields
|
299
|
-
and device.custom_fields["inventory_hostname"]
|
300
|
-
):
|
301
|
-
hostname = device.custom_fields["inventory_hostname"]
|
302
|
-
|
303
|
-
return hostname
|
304
|
-
|
305
|
-
|
306
|
-
def get_device_mac_address(device):
|
307
|
-
"""Get MAC address from device's management interface.
|
308
|
-
|
309
|
-
Args:
|
310
|
-
device: NetBox device object
|
311
|
-
|
312
|
-
Returns:
|
313
|
-
str: MAC address or default '00:00:00:00:00:00'
|
314
|
-
"""
|
315
|
-
mac_address = "00:00:00:00:00:00" # Default MAC
|
316
|
-
try:
|
317
|
-
# Get all interfaces for the device
|
318
|
-
interfaces = utils.nb.dcim.interfaces.filter(device_id=device.id)
|
319
|
-
for interface in interfaces:
|
320
|
-
# Check if interface is marked as management only
|
321
|
-
if interface.mgmt_only:
|
322
|
-
if interface.mac_address:
|
323
|
-
mac_address = interface.mac_address
|
324
|
-
logger.debug(
|
325
|
-
f"Using MAC address {mac_address} from management interface {interface.name}"
|
326
|
-
)
|
327
|
-
break
|
328
|
-
except Exception as e:
|
329
|
-
logger.warning(f"Could not get MAC address for device {device.name}: {e}")
|
330
|
-
|
331
|
-
return mac_address
|
332
|
-
|
333
|
-
|
334
|
-
def get_device_version(device):
|
335
|
-
"""Get SONiC version for device from sonic_parameters or use default.
|
336
|
-
|
337
|
-
Args:
|
338
|
-
device: NetBox device object
|
339
|
-
|
340
|
-
Returns:
|
341
|
-
str: SONiC version formatted for VERSION field (e.g., 'version_4_5_0')
|
342
|
-
"""
|
343
|
-
version = DEFAULT_SONIC_VERSION
|
344
|
-
if (
|
345
|
-
hasattr(device, "custom_fields")
|
346
|
-
and "sonic_parameters" in device.custom_fields
|
347
|
-
and device.custom_fields["sonic_parameters"]
|
348
|
-
and "version" in device.custom_fields["sonic_parameters"]
|
349
|
-
):
|
350
|
-
version = device.custom_fields["sonic_parameters"]["version"]
|
351
|
-
|
352
|
-
# Format version for VERSION field: "4.5.0" -> "version_4_5_0"
|
353
|
-
version_formatted = f"version_{version.replace('.', '_')}"
|
354
|
-
return version_formatted
|
355
|
-
|
356
|
-
|
357
|
-
def get_port_config(hwsku):
|
358
|
-
"""Get port configuration for a given HWSKU.
|
359
|
-
|
360
|
-
Args:
|
361
|
-
hwsku: Hardware SKU name (e.g., 'Accton-AS5835-54T')
|
362
|
-
|
363
|
-
Returns:
|
364
|
-
dict: Port configuration with port names as keys and their properties as values
|
365
|
-
Example: {'Ethernet0': {'lanes': '2', 'alias': 'tenGigE1', 'index': '1', 'speed': '10000'}}
|
366
|
-
"""
|
367
|
-
port_config = {}
|
368
|
-
config_path = f"/etc/sonic/port_config/{hwsku}.ini"
|
369
|
-
|
370
|
-
if not os.path.exists(config_path):
|
371
|
-
logger.error(f"Port config file not found: {config_path}")
|
372
|
-
return port_config
|
373
|
-
|
374
|
-
try:
|
375
|
-
with open(config_path, "r") as f:
|
376
|
-
for line in f:
|
377
|
-
line = line.strip()
|
378
|
-
# Skip comments and empty lines
|
379
|
-
if not line or line.startswith("#"):
|
380
|
-
continue
|
381
|
-
|
382
|
-
parts = line.split()
|
383
|
-
if len(parts) >= 5:
|
384
|
-
port_name = parts[0]
|
385
|
-
port_config[port_name] = {
|
386
|
-
"lanes": parts[1],
|
387
|
-
"alias": parts[2],
|
388
|
-
"index": parts[3],
|
389
|
-
"speed": parts[4],
|
390
|
-
}
|
391
|
-
except Exception as e:
|
392
|
-
logger.error(f"Error parsing port config file {config_path}: {e}")
|
393
|
-
|
394
|
-
return port_config
|
395
|
-
|
396
|
-
|
397
|
-
def save_config_to_netbox(device, config):
|
398
|
-
"""Save SONiC configuration to NetBox device config context.
|
399
|
-
|
400
|
-
Args:
|
401
|
-
device: NetBox device object
|
402
|
-
config: SONiC configuration dictionary
|
403
|
-
"""
|
404
|
-
try:
|
405
|
-
# Get existing config contexts for the device
|
406
|
-
config_contexts = utils.nb.extras.config_contexts.filter(device_id=device.id)
|
407
|
-
|
408
|
-
# Look for existing SONiC config context
|
409
|
-
sonic_context = None
|
410
|
-
for context in config_contexts:
|
411
|
-
if context.name == f"SONiC Config - {device.name}":
|
412
|
-
sonic_context = context
|
413
|
-
break
|
414
|
-
|
415
|
-
# Prepare config context data
|
416
|
-
context_data = {
|
417
|
-
"name": f"SONiC Config - {device.name}",
|
418
|
-
"weight": 1000,
|
419
|
-
"data": {"sonic_config": config},
|
420
|
-
"is_active": True,
|
421
|
-
}
|
422
|
-
|
423
|
-
if sonic_context:
|
424
|
-
# Update existing config context
|
425
|
-
sonic_context.data = {"sonic_config": config}
|
426
|
-
sonic_context.save()
|
427
|
-
logger.info(f"Updated SONiC config context for device {device.name}")
|
428
|
-
else:
|
429
|
-
# Create new config context
|
430
|
-
new_context = utils.nb.extras.config_contexts.create(**context_data)
|
431
|
-
# Assign the config context to the device
|
432
|
-
new_context.devices = [device.id]
|
433
|
-
new_context.save()
|
434
|
-
logger.info(f"Created new SONiC config context for device {device.name}")
|
435
|
-
|
436
|
-
except Exception as e:
|
437
|
-
logger.error(f"Failed to save config context for device {device.name}: {e}")
|
438
|
-
|
439
|
-
|
440
|
-
def export_config_to_file(device, config):
|
441
|
-
"""Export SONiC configuration to local file.
|
442
|
-
|
443
|
-
Args:
|
444
|
-
device: NetBox device object
|
445
|
-
config: SONiC configuration dictionary
|
446
|
-
"""
|
447
|
-
try:
|
448
|
-
# Get configuration from settings
|
449
|
-
export_dir = settings.SONIC_EXPORT_DIR
|
450
|
-
prefix = settings.SONIC_EXPORT_PREFIX
|
451
|
-
suffix = settings.SONIC_EXPORT_SUFFIX
|
452
|
-
|
453
|
-
# Create export directory if it doesn't exist
|
454
|
-
os.makedirs(export_dir, exist_ok=True)
|
455
|
-
|
456
|
-
# Get device hostname from inventory_hostname custom field or device name
|
457
|
-
hostname = get_device_hostname(device)
|
458
|
-
|
459
|
-
# Generate filename: prefix + hostname + suffix
|
460
|
-
filename = f"{prefix}{hostname}{suffix}"
|
461
|
-
filepath = os.path.join(export_dir, filename)
|
462
|
-
|
463
|
-
# Export configuration to JSON file
|
464
|
-
with open(filepath, "w") as f:
|
465
|
-
json.dump(config, f, indent=2)
|
466
|
-
|
467
|
-
logger.info(f"Exported SONiC config for device {device.name} to {filepath}")
|
468
|
-
|
469
|
-
except Exception as e:
|
470
|
-
logger.error(f"Failed to export config for device {device.name}: {e}")
|
471
|
-
|
472
|
-
|
473
|
-
def detect_breakout_ports(device):
|
474
|
-
"""Detect breakout ports from NetBox device interfaces.
|
475
|
-
|
476
|
-
Args:
|
477
|
-
device: NetBox device object
|
478
|
-
|
479
|
-
Returns:
|
480
|
-
dict: Dictionary with breakout port information
|
481
|
-
{
|
482
|
-
'breakout_cfgs': {port_name: {'brkout_mode': mode, 'port': port}},
|
483
|
-
'breakout_ports': {port_name: {'master': master_port}}
|
484
|
-
}
|
485
|
-
"""
|
486
|
-
breakout_cfgs = {}
|
487
|
-
breakout_ports = {}
|
488
|
-
|
489
|
-
try:
|
490
|
-
# Get all interfaces for the device
|
491
|
-
interfaces = list(utils.nb.dcim.interfaces.filter(device_id=device.id))
|
492
|
-
|
493
|
-
# Group interfaces by potential breakout groups
|
494
|
-
# First, handle SONiC format (Ethernet0, Ethernet1, Ethernet2, Ethernet3)
|
495
|
-
sonic_groups = {}
|
496
|
-
# Second, handle NetBox format (Eth1/1/1, Eth1/1/2, Eth1/1/3, Eth1/1/4)
|
497
|
-
netbox_groups = {}
|
498
|
-
|
499
|
-
for interface in interfaces:
|
500
|
-
interface_speed = getattr(interface, "speed", None)
|
501
|
-
# If speed is not set, try to get it from port type
|
502
|
-
if not interface_speed and hasattr(interface, "type") and interface.type:
|
503
|
-
interface_speed = get_speed_from_port_type(interface.type.value)
|
504
|
-
|
505
|
-
# Skip if not high-speed port (100G, 400G, 800G)
|
506
|
-
if not interface_speed or interface_speed not in {
|
507
|
-
100000,
|
508
|
-
200000,
|
509
|
-
400000,
|
510
|
-
800000,
|
511
|
-
}:
|
512
|
-
continue
|
513
|
-
|
514
|
-
# Check for SONiC format breakout (Ethernet0, Ethernet1, Ethernet2, Ethernet3)
|
515
|
-
sonic_match = re.match(r"Ethernet(\d+)", interface.name)
|
516
|
-
if sonic_match:
|
517
|
-
port_num = int(sonic_match.group(1))
|
518
|
-
# Group by base port (0, 4, 8, 12, ...)
|
519
|
-
base_port = (port_num // 4) * 4
|
520
|
-
if base_port not in sonic_groups:
|
521
|
-
sonic_groups[base_port] = []
|
522
|
-
sonic_groups[base_port].append((port_num, interface))
|
523
|
-
|
524
|
-
# Check for NetBox format breakout (Eth1/1/1, Eth1/1/2, Eth1/1/3, Eth1/1/4)
|
525
|
-
netbox_match = re.match(r"Eth(\d+)/(\d+)/(\d+)", interface.name)
|
526
|
-
if netbox_match:
|
527
|
-
module = int(netbox_match.group(1))
|
528
|
-
port = int(netbox_match.group(2))
|
529
|
-
subport = int(netbox_match.group(3))
|
530
|
-
group_key = f"{module}/{port}"
|
531
|
-
if group_key not in netbox_groups:
|
532
|
-
netbox_groups[group_key] = []
|
533
|
-
netbox_groups[group_key].append((subport, interface))
|
534
|
-
|
535
|
-
# Process SONiC format breakout groups
|
536
|
-
for base_port, port_list in sonic_groups.items():
|
537
|
-
# Check if we have exactly 4 consecutive ports
|
538
|
-
if len(port_list) == 4:
|
539
|
-
port_list.sort(key=lambda x: x[0]) # Sort by port number
|
540
|
-
expected_ports = [base_port + i for i in range(4)]
|
541
|
-
actual_ports = [port[0] for port in port_list]
|
542
|
-
|
543
|
-
if actual_ports == expected_ports:
|
544
|
-
# This is a valid breakout group
|
545
|
-
master_port = f"Ethernet{base_port}"
|
546
|
-
|
547
|
-
# Calculate breakout mode based on speed
|
548
|
-
interface_speed = getattr(port_list[0][1], "speed", None)
|
549
|
-
if interface_speed == 100000: # 100G -> 4x25G
|
550
|
-
brkout_mode = "4x25G"
|
551
|
-
elif interface_speed == 200000: # 200G -> 4x50G
|
552
|
-
brkout_mode = "4x50G"
|
553
|
-
elif interface_speed == 400000: # 400G -> 4x100G
|
554
|
-
brkout_mode = "4x100G"
|
555
|
-
elif interface_speed == 800000: # 800G -> 4x200G
|
556
|
-
brkout_mode = "4x200G"
|
557
|
-
else:
|
558
|
-
continue # Skip unsupported speeds
|
559
|
-
|
560
|
-
# Add breakout config for master port
|
561
|
-
physical_port_num = (base_port // 4) + 1
|
562
|
-
breakout_cfgs[master_port] = {
|
563
|
-
"breakout_owner": "MANUAL",
|
564
|
-
"brkout_mode": brkout_mode,
|
565
|
-
"port": f"1/{physical_port_num}",
|
566
|
-
}
|
567
|
-
|
568
|
-
# Add all ports to breakout_ports
|
569
|
-
for port_num, interface in port_list:
|
570
|
-
port_name = f"Ethernet{port_num}"
|
571
|
-
breakout_ports[port_name] = {"master": master_port}
|
572
|
-
|
573
|
-
# Process NetBox format breakout groups
|
574
|
-
for group_key, port_list in netbox_groups.items():
|
575
|
-
# Check if we have exactly 4 subports
|
576
|
-
if len(port_list) == 4:
|
577
|
-
port_list.sort(key=lambda x: x[0]) # Sort by subport number
|
578
|
-
expected_subports = [1, 2, 3, 4]
|
579
|
-
actual_subports = [port[0] for port in port_list]
|
580
|
-
|
581
|
-
if actual_subports == expected_subports:
|
582
|
-
# This is a valid breakout group - convert to SONiC format
|
583
|
-
module, port = group_key.split("/")
|
584
|
-
|
585
|
-
# Calculate base SONiC port number (assuming 4x multiplier for high-speed)
|
586
|
-
base_sonic_port = (int(port) - 1) * 4
|
587
|
-
master_port = f"Ethernet{base_sonic_port}"
|
588
|
-
|
589
|
-
# Calculate breakout mode based on speed
|
590
|
-
interface_speed = getattr(port_list[0][1], "speed", None)
|
591
|
-
if interface_speed == 100000: # 100G -> 4x25G
|
592
|
-
brkout_mode = "4x25G"
|
593
|
-
elif interface_speed == 200000: # 200G -> 4x50G
|
594
|
-
brkout_mode = "4x50G"
|
595
|
-
elif interface_speed == 400000: # 400G -> 4x100G
|
596
|
-
brkout_mode = "4x100G"
|
597
|
-
elif interface_speed == 800000: # 800G -> 4x200G
|
598
|
-
brkout_mode = "4x200G"
|
599
|
-
else:
|
600
|
-
continue # Skip unsupported speeds
|
601
|
-
|
602
|
-
# Add breakout config for master port
|
603
|
-
breakout_cfgs[master_port] = {
|
604
|
-
"breakout_owner": "MANUAL",
|
605
|
-
"brkout_mode": brkout_mode,
|
606
|
-
"port": f"{module}/{port}",
|
607
|
-
}
|
608
|
-
|
609
|
-
# Add all subports to breakout_ports (converted to SONiC format)
|
610
|
-
for subport, interface in port_list:
|
611
|
-
sonic_port_num = base_sonic_port + (subport - 1)
|
612
|
-
port_name = f"Ethernet{sonic_port_num}"
|
613
|
-
breakout_ports[port_name] = {"master": master_port}
|
614
|
-
|
615
|
-
except Exception as e:
|
616
|
-
logger.warning(f"Could not detect breakout ports for device {device.name}: {e}")
|
617
|
-
|
618
|
-
return {"breakout_cfgs": breakout_cfgs, "breakout_ports": breakout_ports}
|
619
|
-
|
620
|
-
|
621
|
-
def get_connected_interfaces(device):
|
622
|
-
"""Get list of interface names that are connected to other devices.
|
623
|
-
|
624
|
-
Args:
|
625
|
-
device: NetBox device object
|
626
|
-
|
627
|
-
Returns:
|
628
|
-
set: Set of interface names that are connected
|
629
|
-
"""
|
630
|
-
connected_interfaces = set()
|
631
|
-
|
632
|
-
try:
|
633
|
-
# Get all interfaces for the device
|
634
|
-
interfaces = utils.nb.dcim.interfaces.filter(device_id=device.id)
|
635
|
-
|
636
|
-
for interface in interfaces:
|
637
|
-
# Skip management-only interfaces
|
638
|
-
if hasattr(interface, "mgmt_only") and interface.mgmt_only:
|
639
|
-
continue
|
640
|
-
|
641
|
-
# Check if interface is connected via cable
|
642
|
-
if hasattr(interface, "cable") and interface.cable:
|
643
|
-
# Convert NetBox interface name to SONiC format
|
644
|
-
interface_speed = getattr(interface, "speed", None)
|
645
|
-
# If speed is not set, try to get it from port type
|
646
|
-
if (
|
647
|
-
not interface_speed
|
648
|
-
and hasattr(interface, "type")
|
649
|
-
and interface.type
|
650
|
-
):
|
651
|
-
interface_speed = get_speed_from_port_type(interface.type.value)
|
652
|
-
sonic_interface_name = convert_netbox_interface_to_sonic(
|
653
|
-
interface.name, interface_speed
|
654
|
-
)
|
655
|
-
connected_interfaces.add(sonic_interface_name)
|
656
|
-
# Alternative check using is_connected property if available
|
657
|
-
elif hasattr(interface, "is_connected") and interface.is_connected:
|
658
|
-
# Convert NetBox interface name to SONiC format
|
659
|
-
interface_speed = getattr(interface, "speed", None)
|
660
|
-
# If speed is not set, try to get it from port type
|
661
|
-
if (
|
662
|
-
not interface_speed
|
663
|
-
and hasattr(interface, "type")
|
664
|
-
and interface.type
|
665
|
-
):
|
666
|
-
interface_speed = get_speed_from_port_type(interface.type.value)
|
667
|
-
sonic_interface_name = convert_netbox_interface_to_sonic(
|
668
|
-
interface.name, interface_speed
|
669
|
-
)
|
670
|
-
connected_interfaces.add(sonic_interface_name)
|
671
|
-
|
672
|
-
except Exception as e:
|
673
|
-
logger.warning(
|
674
|
-
f"Could not get interface connections for device {device.name}: {e}"
|
675
|
-
)
|
676
|
-
|
677
|
-
return connected_interfaces
|
678
|
-
|
679
|
-
|
680
|
-
def generate_sonic_config(device, hwsku):
|
681
|
-
"""Generate minimal SONiC config.json for a device.
|
682
|
-
|
683
|
-
Args:
|
684
|
-
device: NetBox device object
|
685
|
-
hwsku: Hardware SKU name
|
686
|
-
|
687
|
-
Returns:
|
688
|
-
dict: Minimal SONiC configuration dictionary
|
689
|
-
"""
|
690
|
-
# Get port configuration for the HWSKU
|
691
|
-
port_config = get_port_config(hwsku)
|
692
|
-
|
693
|
-
# Get connected interfaces to determine admin_status
|
694
|
-
connected_interfaces = get_connected_interfaces(device)
|
695
|
-
|
696
|
-
# Get OOB IP for management interface
|
697
|
-
oob_ip_result = get_device_oob_ip(device)
|
698
|
-
|
699
|
-
# Get VLAN configuration from NetBox
|
700
|
-
vlan_info = get_device_vlans(device)
|
701
|
-
|
702
|
-
# Get Loopback configuration from NetBox
|
703
|
-
loopback_info = get_device_loopbacks(device)
|
704
|
-
|
705
|
-
# Get breakout port configuration from NetBox
|
706
|
-
breakout_info = detect_breakout_ports(device)
|
707
|
-
|
708
|
-
# Get all interfaces from NetBox with their speeds and types
|
709
|
-
netbox_interfaces = {}
|
710
|
-
try:
|
711
|
-
interfaces = utils.nb.dcim.interfaces.filter(device_id=device.id)
|
712
|
-
for interface in interfaces:
|
713
|
-
# Convert NetBox interface name to SONiC format for lookup
|
714
|
-
interface_speed = getattr(interface, "speed", None)
|
715
|
-
# If speed is not set, try to get it from port type
|
716
|
-
if not interface_speed and hasattr(interface, "type") and interface.type:
|
717
|
-
interface_speed = get_speed_from_port_type(interface.type.value)
|
718
|
-
sonic_name = convert_netbox_interface_to_sonic(
|
719
|
-
interface.name, interface_speed
|
720
|
-
)
|
721
|
-
netbox_interfaces[sonic_name] = {
|
722
|
-
"speed": interface_speed,
|
723
|
-
"type": (
|
724
|
-
getattr(interface.type, "value", None)
|
725
|
-
if hasattr(interface, "type") and interface.type
|
726
|
-
else None
|
727
|
-
),
|
728
|
-
"netbox_name": interface.name,
|
729
|
-
}
|
730
|
-
except Exception as e:
|
731
|
-
logger.warning(f"Could not get interface details from NetBox: {e}")
|
732
|
-
|
733
|
-
# Get device metadata using helper functions
|
734
|
-
platform = get_device_platform(device, hwsku)
|
735
|
-
hostname = get_device_hostname(device)
|
736
|
-
mac_address = get_device_mac_address(device)
|
737
|
-
version = get_device_version(device)
|
738
|
-
|
739
|
-
# Try to load base configuration from /etc/sonic/config_db.json
|
740
|
-
base_config_path = "/etc/sonic/config_db.json"
|
741
|
-
config = {}
|
742
|
-
|
743
|
-
try:
|
744
|
-
if os.path.exists(base_config_path):
|
745
|
-
with open(base_config_path, "r") as f:
|
746
|
-
config = json.load(f)
|
747
|
-
logger.info(f"Loaded base configuration from {base_config_path}")
|
748
|
-
except Exception as e:
|
749
|
-
logger.warning(
|
750
|
-
f"Could not load base configuration from {base_config_path}: {e}"
|
751
|
-
)
|
752
|
-
|
753
|
-
# Ensure all required sections exist in the config
|
754
|
-
required_sections = {
|
755
|
-
"DEVICE_METADATA": {},
|
756
|
-
"PORT": {},
|
757
|
-
"INTERFACE": {},
|
758
|
-
"VLAN": {},
|
759
|
-
"VLAN_MEMBER": {},
|
760
|
-
"VLAN_INTERFACE": {},
|
761
|
-
"MGMT_INTERFACE": {},
|
762
|
-
"LOOPBACK": {},
|
763
|
-
"LOOPBACK_INTERFACE": {},
|
764
|
-
"BREAKOUT_CFG": {},
|
765
|
-
"BREAKOUT_PORTS": {},
|
766
|
-
"BGP_GLOBALS": {},
|
767
|
-
"BGP_NEIGHBOR": {},
|
768
|
-
"BGP_NEIGHBOR_AF": {},
|
769
|
-
"BGP_GLOBALS_AF_NETWORK": {},
|
770
|
-
"NTP_SERVER": {},
|
771
|
-
"VERSIONS": {},
|
772
|
-
}
|
773
|
-
|
774
|
-
for section, default_value in required_sections.items():
|
775
|
-
if section not in config:
|
776
|
-
config[section] = default_value
|
777
|
-
|
778
|
-
# Update DEVICE_METADATA with NetBox information
|
779
|
-
if "localhost" not in config["DEVICE_METADATA"]:
|
780
|
-
config["DEVICE_METADATA"]["localhost"] = {}
|
781
|
-
|
782
|
-
config["DEVICE_METADATA"]["localhost"].update(
|
783
|
-
{
|
784
|
-
"hostname": hostname,
|
785
|
-
"hwsku": hwsku,
|
786
|
-
"platform": platform,
|
787
|
-
"mac": mac_address,
|
788
|
-
"type": "LeafRouter",
|
789
|
-
}
|
790
|
-
)
|
791
|
-
|
792
|
-
# Update VERSIONS if not present
|
793
|
-
if "DATABASE" not in config["VERSIONS"]:
|
794
|
-
config["VERSIONS"]["DATABASE"] = {"VERSION": version}
|
795
|
-
|
796
|
-
# Add BGP_GLOBALS configuration with router_id set to primary IP address
|
797
|
-
primary_ip = None
|
798
|
-
if device.primary_ip4:
|
799
|
-
primary_ip = str(device.primary_ip4.address).split("/")[0]
|
800
|
-
elif device.primary_ip6:
|
801
|
-
primary_ip = str(device.primary_ip6.address).split("/")[0]
|
802
|
-
|
803
|
-
if primary_ip:
|
804
|
-
if "default" not in config["BGP_GLOBALS"]:
|
805
|
-
config["BGP_GLOBALS"]["default"] = {}
|
806
|
-
config["BGP_GLOBALS"]["default"]["router_id"] = primary_ip
|
807
|
-
|
808
|
-
# Calculate and add local_asn from router_id (only for IPv4)
|
809
|
-
if device.primary_ip4:
|
810
|
-
try:
|
811
|
-
local_asn = calculate_local_asn_from_ipv4(primary_ip)
|
812
|
-
config["BGP_GLOBALS"]["default"]["local_asn"] = str(local_asn)
|
813
|
-
except ValueError as e:
|
814
|
-
logger.warning(
|
815
|
-
f"Could not calculate local ASN for device {device.name}: {e}"
|
816
|
-
)
|
817
|
-
|
818
|
-
# Add port configurations in sorted order
|
819
|
-
# Sort ports naturally (Ethernet0, Ethernet4, Ethernet8, ...)
|
820
|
-
def natural_sort_key(port_name):
|
821
|
-
"""Extract numeric part from port name for natural sorting."""
|
822
|
-
match = re.search(r"(\d+)", port_name)
|
823
|
-
return int(match.group(1)) if match else 0
|
824
|
-
|
825
|
-
sorted_ports = sorted(port_config.keys(), key=natural_sort_key)
|
826
|
-
|
827
|
-
for port_name in sorted_ports:
|
828
|
-
port_info = port_config[port_name]
|
829
|
-
|
830
|
-
# Set admin_status to "up" if port is connected, otherwise "down"
|
831
|
-
admin_status = "up" if port_name in connected_interfaces else "down"
|
832
|
-
|
833
|
-
# Check if this port is a breakout port and adjust speed and lanes accordingly
|
834
|
-
port_speed = port_info["speed"]
|
835
|
-
port_lanes = port_info["lanes"]
|
836
|
-
|
837
|
-
# Override with NetBox data if available and hardware config has no speed
|
838
|
-
if port_name in netbox_interfaces:
|
839
|
-
netbox_speed = netbox_interfaces[port_name]["speed"]
|
840
|
-
if netbox_speed and (not port_speed or port_speed == "0"):
|
841
|
-
logger.info(
|
842
|
-
f"Using NetBox speed {netbox_speed} for port {port_name} (hardware config had: {port_speed})"
|
843
|
-
)
|
844
|
-
port_speed = str(netbox_speed)
|
845
|
-
|
846
|
-
if port_name in breakout_info["breakout_ports"]:
|
847
|
-
# Get the master port to determine original speed and lanes
|
848
|
-
master_port = breakout_info["breakout_ports"][port_name]["master"]
|
849
|
-
if master_port in breakout_info["breakout_cfgs"]:
|
850
|
-
brkout_mode = breakout_info["breakout_cfgs"][master_port]["brkout_mode"]
|
851
|
-
# Extract breakout speed from mode (e.g., "4x25G" -> "25000")
|
852
|
-
if "25G" in brkout_mode:
|
853
|
-
port_speed = "25000"
|
854
|
-
elif "50G" in brkout_mode:
|
855
|
-
port_speed = "50000"
|
856
|
-
elif "100G" in brkout_mode:
|
857
|
-
port_speed = "100000"
|
858
|
-
elif "200G" in brkout_mode:
|
859
|
-
port_speed = "200000"
|
860
|
-
|
861
|
-
# Calculate individual lane for this breakout port (always for breakout ports)
|
862
|
-
# Get master port's lanes from port_config
|
863
|
-
if master_port in port_config:
|
864
|
-
master_lanes = port_config[master_port]["lanes"]
|
865
|
-
# Parse lane range (e.g., "1,2,3,4" or "1-4")
|
866
|
-
if "," in master_lanes:
|
867
|
-
lanes_list = [int(lane.strip()) for lane in master_lanes.split(",")]
|
868
|
-
elif "-" in master_lanes:
|
869
|
-
start, end = map(int, master_lanes.split("-"))
|
870
|
-
lanes_list = list(range(start, end + 1))
|
871
|
-
else:
|
872
|
-
# Single lane or simple number
|
873
|
-
lanes_list = [int(master_lanes)]
|
874
|
-
|
875
|
-
# Calculate which lane this breakout port should use
|
876
|
-
port_match = re.match(r"Ethernet(\d+)", port_name)
|
877
|
-
if port_match:
|
878
|
-
sonic_port_num = int(port_match.group(1))
|
879
|
-
master_port_match = re.match(r"Ethernet(\d+)", master_port)
|
880
|
-
if master_port_match:
|
881
|
-
master_port_num = int(master_port_match.group(1))
|
882
|
-
# Calculate subport index (0, 1, 2, 3 for 4x breakout)
|
883
|
-
subport_index = sonic_port_num - master_port_num
|
884
|
-
if 0 <= subport_index < len(lanes_list):
|
885
|
-
port_lanes = str(lanes_list[subport_index])
|
886
|
-
logger.debug(
|
887
|
-
f"Breakout port {port_name}: master={master_port}, master_lanes={master_lanes}, subport_index={subport_index}, assigned_lane={port_lanes}"
|
888
|
-
)
|
889
|
-
else:
|
890
|
-
logger.warning(
|
891
|
-
f"Breakout port {port_name}: subport_index {subport_index} out of range for lanes_list {lanes_list}"
|
892
|
-
)
|
893
|
-
|
894
|
-
# Generate correct alias based on port name and speed
|
895
|
-
interface_speed = int(port_speed) if port_speed else None
|
896
|
-
is_breakout_port = port_name in breakout_info["breakout_ports"]
|
897
|
-
correct_alias = convert_sonic_interface_to_alias(
|
898
|
-
port_name, interface_speed, is_breakout_port
|
899
|
-
)
|
900
|
-
|
901
|
-
# Use master port index for breakout ports
|
902
|
-
port_index = port_info["index"]
|
903
|
-
if is_breakout_port:
|
904
|
-
master_port = breakout_info["breakout_ports"][port_name]["master"]
|
905
|
-
if master_port in port_config:
|
906
|
-
port_index = port_config[master_port]["index"]
|
907
|
-
|
908
|
-
config["PORT"][port_name] = {
|
909
|
-
"admin_status": admin_status,
|
910
|
-
"alias": correct_alias,
|
911
|
-
"index": port_index,
|
912
|
-
"lanes": port_lanes,
|
913
|
-
"speed": port_speed,
|
914
|
-
"mtu": "9100",
|
915
|
-
"adv_speeds": "all",
|
916
|
-
"autoneg": "off",
|
917
|
-
"link_training": "off",
|
918
|
-
"unreliable_los": "auto",
|
919
|
-
}
|
920
|
-
|
921
|
-
# Add breakout ports that might not be in the original port_config
|
922
|
-
for port_name in breakout_info["breakout_ports"]:
|
923
|
-
if port_name not in config["PORT"]:
|
924
|
-
# Get the master port to determine configuration
|
925
|
-
master_port = breakout_info["breakout_ports"][port_name]["master"]
|
926
|
-
if master_port in breakout_info["breakout_cfgs"]:
|
927
|
-
brkout_mode = breakout_info["breakout_cfgs"][master_port]["brkout_mode"]
|
928
|
-
|
929
|
-
# Extract breakout speed from mode
|
930
|
-
if "25G" in brkout_mode:
|
931
|
-
port_speed = "25000"
|
932
|
-
elif "50G" in brkout_mode:
|
933
|
-
port_speed = "50000"
|
934
|
-
elif "100G" in brkout_mode:
|
935
|
-
port_speed = "100000"
|
936
|
-
elif "200G" in brkout_mode:
|
937
|
-
port_speed = "200000"
|
938
|
-
else:
|
939
|
-
port_speed = "25000" # Default fallback
|
940
|
-
|
941
|
-
# Set admin_status based on connection
|
942
|
-
admin_status = "up" if port_name in connected_interfaces else "down"
|
943
|
-
|
944
|
-
# Generate correct alias (breakout port always gets subport notation)
|
945
|
-
interface_speed = int(port_speed)
|
946
|
-
correct_alias = convert_sonic_interface_to_alias(
|
947
|
-
port_name, interface_speed, is_breakout=True
|
948
|
-
)
|
949
|
-
|
950
|
-
# Use master port index for breakout ports
|
951
|
-
port_index = "1" # Default fallback
|
952
|
-
if master_port in port_config:
|
953
|
-
port_index = port_config[master_port]["index"]
|
954
|
-
|
955
|
-
# Calculate individual lane for this breakout port
|
956
|
-
port_lanes = "1" # Default fallback
|
957
|
-
port_match = re.match(r"Ethernet(\d+)", port_name)
|
958
|
-
if master_port in port_config and port_match:
|
959
|
-
master_lanes = port_config[master_port]["lanes"]
|
960
|
-
# Parse lane range (e.g., "1,2,3,4" or "1-4")
|
961
|
-
if "," in master_lanes:
|
962
|
-
lanes_list = [
|
963
|
-
int(lane.strip()) for lane in master_lanes.split(",")
|
964
|
-
]
|
965
|
-
elif "-" in master_lanes:
|
966
|
-
start, end = map(int, master_lanes.split("-"))
|
967
|
-
lanes_list = list(range(start, end + 1))
|
968
|
-
else:
|
969
|
-
# Single lane or simple number
|
970
|
-
lanes_list = [int(master_lanes)]
|
971
|
-
|
972
|
-
# Calculate which lane this breakout port should use
|
973
|
-
sonic_port_num = int(port_match.group(1))
|
974
|
-
master_port_match = re.match(r"Ethernet(\d+)", master_port)
|
975
|
-
if master_port_match:
|
976
|
-
master_port_num = int(master_port_match.group(1))
|
977
|
-
# Calculate subport index (0, 1, 2, 3 for 4x breakout)
|
978
|
-
subport_index = sonic_port_num - master_port_num
|
979
|
-
if 0 <= subport_index < len(lanes_list):
|
980
|
-
port_lanes = str(lanes_list[subport_index])
|
981
|
-
logger.debug(
|
982
|
-
f"Breakout port {port_name}: master={master_port}, master_lanes={master_lanes}, subport_index={subport_index}, assigned_lane={port_lanes}"
|
983
|
-
)
|
984
|
-
else:
|
985
|
-
logger.warning(
|
986
|
-
f"Breakout port {port_name}: subport_index {subport_index} out of range for lanes_list {lanes_list}"
|
987
|
-
)
|
988
|
-
|
989
|
-
config["PORT"][port_name] = {
|
990
|
-
"admin_status": admin_status,
|
991
|
-
"alias": correct_alias,
|
992
|
-
"index": port_index,
|
993
|
-
"lanes": port_lanes,
|
994
|
-
"speed": port_speed,
|
995
|
-
"mtu": "9100",
|
996
|
-
"adv_speeds": "all",
|
997
|
-
"autoneg": "off",
|
998
|
-
"link_training": "off",
|
999
|
-
"unreliable_los": "auto",
|
1000
|
-
}
|
1001
|
-
|
1002
|
-
# Add tagged VLANs to PORT configuration
|
1003
|
-
# Build a mapping of ports to their tagged VLANs
|
1004
|
-
port_tagged_vlans = {}
|
1005
|
-
for vid, members in vlan_info["vlan_members"].items():
|
1006
|
-
for netbox_interface_name, tagging_mode in members.items():
|
1007
|
-
# Convert NetBox interface name to SONiC format
|
1008
|
-
# Try to find speed from netbox_interfaces
|
1009
|
-
speed = None
|
1010
|
-
for sonic_name, iface_info in netbox_interfaces.items():
|
1011
|
-
if iface_info["netbox_name"] == netbox_interface_name:
|
1012
|
-
speed = iface_info["speed"]
|
1013
|
-
break
|
1014
|
-
sonic_interface_name = convert_netbox_interface_to_sonic(
|
1015
|
-
netbox_interface_name, speed
|
1016
|
-
)
|
1017
|
-
|
1018
|
-
# Only add if this is a tagged VLAN (not untagged)
|
1019
|
-
if tagging_mode == "tagged":
|
1020
|
-
if sonic_interface_name not in port_tagged_vlans:
|
1021
|
-
port_tagged_vlans[sonic_interface_name] = []
|
1022
|
-
port_tagged_vlans[sonic_interface_name].append(str(vid))
|
1023
|
-
|
1024
|
-
# Update PORT configuration with tagged VLANs
|
1025
|
-
for port_name in config["PORT"]:
|
1026
|
-
if port_name in port_tagged_vlans:
|
1027
|
-
# Sort the VLAN IDs numerically for consistent ordering
|
1028
|
-
tagged_vlans = sorted(port_tagged_vlans[port_name], key=int)
|
1029
|
-
config["PORT"][port_name]["tagged_vlans"] = tagged_vlans
|
1030
|
-
|
1031
|
-
# Add INTERFACE configuration for connected interfaces (except management-only)
|
1032
|
-
# This enables IPv6 link-local only mode for all connected non-management interfaces
|
1033
|
-
for port_name in config["PORT"]:
|
1034
|
-
# Check if this port is in the connected interfaces set
|
1035
|
-
if port_name in connected_interfaces:
|
1036
|
-
# Add interface to INTERFACE section with ipv6_use_link_local_only enabled
|
1037
|
-
config["INTERFACE"][port_name] = {"ipv6_use_link_local_only": "enable"}
|
1038
|
-
|
1039
|
-
# Add BGP_NEIGHBOR_AF configuration for connected interfaces (except management-only)
|
1040
|
-
# This enables BGP for both IPv4 and IPv6 unicast on all connected non-management interfaces
|
1041
|
-
for port_name in config["PORT"]:
|
1042
|
-
# Check if this port is in the connected interfaces set
|
1043
|
-
if port_name in connected_interfaces:
|
1044
|
-
# Add BGP neighbor address family configuration for IPv4 and IPv6
|
1045
|
-
ipv4_key = f"default|{port_name}|ipv4_unicast"
|
1046
|
-
ipv6_key = f"default|{port_name}|ipv6_unicast"
|
1047
|
-
|
1048
|
-
config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "true"}
|
1049
|
-
config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "true"}
|
1050
|
-
|
1051
|
-
# Add BGP_NEIGHBOR configuration for connected interfaces (except management-only and virtual)
|
1052
|
-
# This configures BGP neighbors as external peers with IPv6-only mode
|
1053
|
-
for port_name in config["PORT"]:
|
1054
|
-
# Check if this port is in the connected interfaces set
|
1055
|
-
if port_name in connected_interfaces:
|
1056
|
-
# Add BGP neighbor configuration
|
1057
|
-
neighbor_key = f"default|{port_name}"
|
1058
|
-
config["BGP_NEIGHBOR"][neighbor_key] = {
|
1059
|
-
"peer_type": "external",
|
1060
|
-
"v6only": "true",
|
1061
|
-
}
|
1062
|
-
|
1063
|
-
# Add additional BGP_NEIGHBOR configuration using Loopback0 IP addresses from connected devices
|
1064
|
-
try:
|
1065
|
-
# Get all interfaces for the device to find connected devices
|
1066
|
-
interfaces = utils.nb.dcim.interfaces.filter(device_id=device.id)
|
1067
|
-
|
1068
|
-
for interface in interfaces:
|
1069
|
-
# Skip management-only interfaces
|
1070
|
-
if hasattr(interface, "mgmt_only") and interface.mgmt_only:
|
1071
|
-
continue
|
1072
|
-
|
1073
|
-
# Check if interface is connected via cable
|
1074
|
-
if hasattr(interface, "cable") and interface.cable:
|
1075
|
-
# Convert NetBox interface name to SONiC format to check if it's in our PORT config
|
1076
|
-
interface_speed = getattr(interface, "speed", None)
|
1077
|
-
# If speed is not set, try to get it from port type
|
1078
|
-
if (
|
1079
|
-
not interface_speed
|
1080
|
-
and hasattr(interface, "type")
|
1081
|
-
and interface.type
|
1082
|
-
):
|
1083
|
-
interface_speed = get_speed_from_port_type(interface.type.value)
|
1084
|
-
sonic_interface_name = convert_netbox_interface_to_sonic(
|
1085
|
-
interface.name, interface_speed
|
1086
|
-
)
|
1087
|
-
|
1088
|
-
# Only process if this interface is in our PORT configuration
|
1089
|
-
if (
|
1090
|
-
sonic_interface_name in config["PORT"]
|
1091
|
-
and sonic_interface_name in connected_interfaces
|
1092
|
-
):
|
1093
|
-
try:
|
1094
|
-
# Get the cable and find the connected device
|
1095
|
-
cable = interface.cable
|
1096
|
-
connected_device = None
|
1097
|
-
|
1098
|
-
# Try to get cable terminations (modern NetBox API)
|
1099
|
-
if hasattr(cable, "a_terminations") and hasattr(
|
1100
|
-
cable, "b_terminations"
|
1101
|
-
):
|
1102
|
-
for termination in list(cable.a_terminations) + list(
|
1103
|
-
cable.b_terminations
|
1104
|
-
):
|
1105
|
-
# Termination is the interface object directly
|
1106
|
-
if (
|
1107
|
-
hasattr(termination, "device")
|
1108
|
-
and termination.device.id != device.id
|
1109
|
-
):
|
1110
|
-
connected_device = termination.device
|
1111
|
-
break
|
1112
|
-
|
1113
|
-
# Fallback: try legacy cable API structure
|
1114
|
-
if not connected_device:
|
1115
|
-
if hasattr(cable, "termination_a") and hasattr(
|
1116
|
-
cable, "termination_b"
|
1117
|
-
):
|
1118
|
-
if cable.termination_a.device.id != device.id:
|
1119
|
-
connected_device = cable.termination_a.device
|
1120
|
-
elif cable.termination_b.device.id != device.id:
|
1121
|
-
connected_device = cable.termination_b.device
|
1122
|
-
|
1123
|
-
if connected_device:
|
1124
|
-
# Check if connected device has the required tag
|
1125
|
-
has_osism_tag = False
|
1126
|
-
if connected_device.tags:
|
1127
|
-
has_osism_tag = any(
|
1128
|
-
tag.slug == "managed-by-osism"
|
1129
|
-
for tag in connected_device.tags
|
1130
|
-
)
|
1131
|
-
|
1132
|
-
if has_osism_tag:
|
1133
|
-
# Get Loopback0 IP addresses from the connected device
|
1134
|
-
connected_device_interfaces = (
|
1135
|
-
utils.nb.dcim.interfaces.filter(
|
1136
|
-
device_id=connected_device.id
|
1137
|
-
)
|
1138
|
-
)
|
1139
|
-
|
1140
|
-
for conn_interface in connected_device_interfaces:
|
1141
|
-
# Look for Loopback0 interface
|
1142
|
-
if conn_interface.name == "Loopback0":
|
1143
|
-
# Get IP addresses assigned to this Loopback0 interface
|
1144
|
-
ip_addresses = (
|
1145
|
-
utils.nb.ipam.ip_addresses.filter(
|
1146
|
-
assigned_object_id=conn_interface.id,
|
1147
|
-
)
|
1148
|
-
)
|
1149
|
-
|
1150
|
-
for ip_addr in ip_addresses:
|
1151
|
-
if ip_addr.address:
|
1152
|
-
# Extract just the IP address without prefix
|
1153
|
-
ip_only = ip_addr.address.split("/")[0]
|
1154
|
-
neighbor_key = f"default|{ip_only}"
|
1155
|
-
config["BGP_NEIGHBOR"][neighbor_key] = {
|
1156
|
-
"peer_type": "external"
|
1157
|
-
}
|
1158
|
-
break
|
1159
|
-
else:
|
1160
|
-
logger.debug(
|
1161
|
-
f"Skipping BGP neighbor for device {connected_device.name}: missing 'managed-by-osism' tag"
|
1162
|
-
)
|
1163
|
-
|
1164
|
-
except Exception as e:
|
1165
|
-
logger.warning(
|
1166
|
-
f"Could not get connected device for interface {interface.name}: {e}"
|
1167
|
-
)
|
1168
|
-
|
1169
|
-
except Exception as e:
|
1170
|
-
logger.warning(f"Could not process BGP neighbors for device {device.name}: {e}")
|
1171
|
-
|
1172
|
-
# Add NTP_SERVER configuration using Loopback0 IP addresses from devices with manager or metalbox roles
|
1173
|
-
try:
|
1174
|
-
# Get devices with manager or metalbox device roles
|
1175
|
-
devices_manager = utils.nb.dcim.devices.filter(role="manager")
|
1176
|
-
devices_metalbox = utils.nb.dcim.devices.filter(role="metalbox")
|
1177
|
-
|
1178
|
-
# Combine both device lists
|
1179
|
-
ntp_devices = list(devices_manager) + list(devices_metalbox)
|
1180
|
-
|
1181
|
-
for ntp_device in ntp_devices:
|
1182
|
-
# Get interfaces for this device to find Loopback0
|
1183
|
-
device_interfaces = utils.nb.dcim.interfaces.filter(device_id=ntp_device.id)
|
1184
|
-
|
1185
|
-
for interface in device_interfaces:
|
1186
|
-
# Look for Loopback0 interface
|
1187
|
-
if interface.name == "Loopback0":
|
1188
|
-
# Get IP addresses assigned to this Loopback0 interface
|
1189
|
-
ip_addresses = utils.nb.ipam.ip_addresses.filter(
|
1190
|
-
assigned_object_id=interface.id,
|
1191
|
-
)
|
1192
|
-
|
1193
|
-
for ip_addr in ip_addresses:
|
1194
|
-
if ip_addr.address:
|
1195
|
-
# Extract just the IPv4 address without prefix
|
1196
|
-
ip_only = ip_addr.address.split("/")[0]
|
1197
|
-
|
1198
|
-
# Check if it's an IPv4 address (simple check)
|
1199
|
-
if "." in ip_only and ":" not in ip_only:
|
1200
|
-
config["NTP_SERVER"][ip_only] = {
|
1201
|
-
"maxpoll": "10",
|
1202
|
-
"minpoll": "6",
|
1203
|
-
"prefer": "false",
|
1204
|
-
}
|
1205
|
-
logger.info(
|
1206
|
-
f"Added NTP server {ip_only} from device {ntp_device.name} with role {ntp_device.role.slug}"
|
1207
|
-
)
|
1208
|
-
break
|
1209
|
-
|
1210
|
-
except Exception as e:
|
1211
|
-
logger.warning(f"Could not process NTP servers: {e}")
|
1212
|
-
|
1213
|
-
# Add VLAN configuration from NetBox
|
1214
|
-
for vid, vlan_data in vlan_info["vlans"].items():
|
1215
|
-
vlan_name = f"Vlan{vid}"
|
1216
|
-
|
1217
|
-
# Get member ports for this VLAN and convert interface names
|
1218
|
-
members = []
|
1219
|
-
if vid in vlan_info["vlan_members"]:
|
1220
|
-
for netbox_interface_name in vlan_info["vlan_members"][vid].keys():
|
1221
|
-
# Convert NetBox interface name to SONiC format
|
1222
|
-
# Try to find speed from netbox_interfaces
|
1223
|
-
speed = None
|
1224
|
-
for sonic_name, iface_info in netbox_interfaces.items():
|
1225
|
-
if iface_info["netbox_name"] == netbox_interface_name:
|
1226
|
-
speed = iface_info["speed"]
|
1227
|
-
break
|
1228
|
-
sonic_interface_name = convert_netbox_interface_to_sonic(
|
1229
|
-
netbox_interface_name, speed
|
1230
|
-
)
|
1231
|
-
members.append(sonic_interface_name)
|
1232
|
-
|
1233
|
-
config["VLAN"][vlan_name] = {
|
1234
|
-
"admin_status": "up",
|
1235
|
-
"autostate": "enable",
|
1236
|
-
"members": members,
|
1237
|
-
"vlanid": str(vid),
|
1238
|
-
}
|
1239
|
-
|
1240
|
-
# Add VLAN members
|
1241
|
-
for vid, members in vlan_info["vlan_members"].items():
|
1242
|
-
vlan_name = f"Vlan{vid}"
|
1243
|
-
for netbox_interface_name, tagging_mode in members.items():
|
1244
|
-
# Convert NetBox interface name to SONiC format
|
1245
|
-
# Try to find speed from netbox_interfaces
|
1246
|
-
speed = None
|
1247
|
-
for sonic_name, iface_info in netbox_interfaces.items():
|
1248
|
-
if iface_info["netbox_name"] == netbox_interface_name:
|
1249
|
-
speed = iface_info["speed"]
|
1250
|
-
break
|
1251
|
-
sonic_interface_name = convert_netbox_interface_to_sonic(
|
1252
|
-
netbox_interface_name, speed
|
1253
|
-
)
|
1254
|
-
# Create VLAN_MEMBER key in format "Vlan<vid>|<port_name>"
|
1255
|
-
member_key = f"{vlan_name}|{sonic_interface_name}"
|
1256
|
-
config["VLAN_MEMBER"][member_key] = {"tagging_mode": tagging_mode}
|
1257
|
-
|
1258
|
-
# Add VLAN interfaces (SVIs)
|
1259
|
-
for vid, interface_data in vlan_info["vlan_interfaces"].items():
|
1260
|
-
vlan_name = f"Vlan{vid}"
|
1261
|
-
if "addresses" in interface_data and interface_data["addresses"]:
|
1262
|
-
# Add the VLAN interface
|
1263
|
-
config["VLAN_INTERFACE"][vlan_name] = {"admin_status": "up"}
|
1264
|
-
|
1265
|
-
# Add IP configuration for each address (IPv4 and IPv6)
|
1266
|
-
for address in interface_data["addresses"]:
|
1267
|
-
ip_key = f"{vlan_name}|{address}"
|
1268
|
-
config["VLAN_INTERFACE"][ip_key] = {}
|
1269
|
-
|
1270
|
-
# Add Loopback configuration from NetBox
|
1271
|
-
for loopback_name, loopback_data in loopback_info["loopbacks"].items():
|
1272
|
-
# Add the Loopback interface
|
1273
|
-
config["LOOPBACK"][loopback_name] = {"admin_status": "up"}
|
1274
|
-
|
1275
|
-
# Add base Loopback interface entry
|
1276
|
-
config["LOOPBACK_INTERFACE"][loopback_name] = {}
|
1277
|
-
|
1278
|
-
# Add IP configuration for each address (IPv4 and IPv6)
|
1279
|
-
for address in loopback_data["addresses"]:
|
1280
|
-
ip_key = f"{loopback_name}|{address}"
|
1281
|
-
config["LOOPBACK_INTERFACE"][ip_key] = {}
|
1282
|
-
|
1283
|
-
# Add BGP_GLOBALS_AF_NETWORK configuration for Loopback0 devices
|
1284
|
-
if loopback_name == "Loopback0":
|
1285
|
-
for address in loopback_data["addresses"]:
|
1286
|
-
# Determine if this is IPv4 or IPv6 and set appropriate address family
|
1287
|
-
try:
|
1288
|
-
ip_obj = ipaddress.ip_interface(address)
|
1289
|
-
if ip_obj.version == 4:
|
1290
|
-
af_key = f"default|ipv4_unicast|{address}"
|
1291
|
-
elif ip_obj.version == 6:
|
1292
|
-
af_key = f"default|ipv6_unicast|{address}"
|
1293
|
-
else:
|
1294
|
-
continue
|
1295
|
-
|
1296
|
-
config["BGP_GLOBALS_AF_NETWORK"][af_key] = {}
|
1297
|
-
except ValueError:
|
1298
|
-
logger.warning(f"Invalid IP address format: {address}")
|
1299
|
-
continue
|
1300
|
-
|
1301
|
-
# Add management interface configuration if OOB IP is available
|
1302
|
-
if oob_ip_result:
|
1303
|
-
oob_ip, prefix_len = oob_ip_result
|
1304
|
-
|
1305
|
-
config["MGMT_INTERFACE"]["eth0"] = {"admin_status": "up"}
|
1306
|
-
# Add IP configuration to MGMT_INTERFACE with CIDR notation
|
1307
|
-
config["MGMT_INTERFACE"][f"eth0|{oob_ip}/{prefix_len}"] = {}
|
1308
|
-
|
1309
|
-
# Add breakout configuration from NetBox
|
1310
|
-
if breakout_info["breakout_cfgs"]:
|
1311
|
-
config["BREAKOUT_CFG"].update(breakout_info["breakout_cfgs"])
|
1312
|
-
|
1313
|
-
if breakout_info["breakout_ports"]:
|
1314
|
-
config["BREAKOUT_PORTS"].update(breakout_info["breakout_ports"])
|
1315
|
-
|
1316
|
-
return config
|
1317
|
-
|
1318
|
-
|
1319
|
-
def sync_sonic():
|
1320
|
-
"""Sync SONiC configurations for eligible devices.
|
1321
|
-
|
1322
|
-
Returns:
|
1323
|
-
dict: Dictionary with device names as keys and their SONiC configs as values
|
1324
|
-
"""
|
1325
|
-
logger.info("Preparing SONIC configuration files")
|
1326
|
-
|
1327
|
-
# Dictionary to store configurations for all devices
|
1328
|
-
device_configs = {}
|
1329
|
-
|
1330
|
-
# List of supported HWSKUs
|
1331
|
-
supported_hwskus = [
|
1332
|
-
"Accton-AS5835-54T",
|
1333
|
-
"Accton-AS7326-56X",
|
1334
|
-
"Accton-AS7726-32X",
|
1335
|
-
"Accton-AS9716-32D",
|
1336
|
-
]
|
1337
|
-
|
1338
|
-
logger.debug(f"Supported HWSKUs: {', '.join(supported_hwskus)}")
|
1339
|
-
|
1340
|
-
# Get device query list from NETBOX_FILTER_CONDUCTOR_SONIC
|
1341
|
-
nb_device_query_list = get_nb_device_query_list_sonic()
|
1342
|
-
|
1343
|
-
devices = []
|
1344
|
-
for nb_device_query in nb_device_query_list:
|
1345
|
-
# Query devices with the NETBOX_FILTER_CONDUCTOR_SONIC criteria
|
1346
|
-
for device in utils.nb.dcim.devices.filter(**nb_device_query):
|
1347
|
-
# Check if device role matches allowed roles
|
1348
|
-
if device.role and device.role.slug in DEFAULT_SONIC_ROLES:
|
1349
|
-
devices.append(device)
|
1350
|
-
logger.debug(
|
1351
|
-
f"Found device: {device.name} with role: {device.role.slug}"
|
1352
|
-
)
|
1353
|
-
|
1354
|
-
logger.info(f"Found {len(devices)} devices matching criteria")
|
1355
|
-
|
1356
|
-
# Generate SONIC configuration for each device
|
1357
|
-
for device in devices:
|
1358
|
-
# Get HWSKU from sonic_parameters custom field, default to None
|
1359
|
-
hwsku = None
|
1360
|
-
if (
|
1361
|
-
hasattr(device, "custom_fields")
|
1362
|
-
and "sonic_parameters" in device.custom_fields
|
1363
|
-
and device.custom_fields["sonic_parameters"]
|
1364
|
-
and "hwsku" in device.custom_fields["sonic_parameters"]
|
1365
|
-
):
|
1366
|
-
hwsku = device.custom_fields["sonic_parameters"]["hwsku"]
|
1367
|
-
|
1368
|
-
# Skip devices without HWSKU
|
1369
|
-
if not hwsku:
|
1370
|
-
logger.debug(f"Skipping device {device.name}: no HWSKU configured")
|
1371
|
-
continue
|
1372
|
-
|
1373
|
-
logger.debug(f"Processing device: {device.name} with HWSKU: {hwsku}")
|
1374
|
-
|
1375
|
-
# Validate that HWSKU is supported
|
1376
|
-
if hwsku not in supported_hwskus:
|
1377
|
-
logger.warning(
|
1378
|
-
f"Device {device.name} has unsupported HWSKU: {hwsku}. Supported HWSKUs: {', '.join(supported_hwskus)}"
|
1379
|
-
)
|
1380
|
-
continue
|
1381
|
-
|
1382
|
-
# Generate SONIC configuration based on device HWSKU
|
1383
|
-
sonic_config = generate_sonic_config(device, hwsku)
|
1384
|
-
|
1385
|
-
# Store configuration in the dictionary
|
1386
|
-
device_configs[device.name] = sonic_config
|
1387
|
-
|
1388
|
-
# Save the generated configuration to NetBox config context
|
1389
|
-
save_config_to_netbox(device, sonic_config)
|
1390
|
-
|
1391
|
-
# Export the generated configuration to local file
|
1392
|
-
export_config_to_file(device, sonic_config)
|
1393
|
-
|
1394
|
-
logger.info(
|
1395
|
-
f"Generated SONiC config for device {device.name} with {len(sonic_config['PORT'])} ports"
|
1396
|
-
)
|
1397
|
-
|
1398
|
-
logger.info(f"Generated SONiC configurations for {len(device_configs)} devices")
|
1399
|
-
|
1400
|
-
# Return the dictionary with all device configurations
|
1401
|
-
return device_configs
|