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,908 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
|
3
|
+
"""Configuration generation logic for SONiC."""
|
4
|
+
|
5
|
+
import copy
|
6
|
+
import ipaddress
|
7
|
+
import json
|
8
|
+
import os
|
9
|
+
import re
|
10
|
+
from loguru import logger
|
11
|
+
|
12
|
+
from osism import utils
|
13
|
+
from osism.tasks.conductor.netbox import (
|
14
|
+
get_device_loopbacks,
|
15
|
+
get_device_oob_ip,
|
16
|
+
get_device_vlans,
|
17
|
+
)
|
18
|
+
from .bgp import calculate_local_asn_from_ipv4
|
19
|
+
from .device import get_device_platform, get_device_hostname, get_device_mac_address
|
20
|
+
from .interface import (
|
21
|
+
get_port_config,
|
22
|
+
get_speed_from_port_type,
|
23
|
+
convert_netbox_interface_to_sonic,
|
24
|
+
convert_sonic_interface_to_alias,
|
25
|
+
detect_breakout_ports,
|
26
|
+
detect_port_channels,
|
27
|
+
clear_port_config_cache,
|
28
|
+
)
|
29
|
+
from .connections import (
|
30
|
+
get_connected_interfaces,
|
31
|
+
get_connected_device_for_sonic_interface,
|
32
|
+
get_device_bgp_neighbors_via_loopback,
|
33
|
+
)
|
34
|
+
from .cache import get_cached_device_interfaces
|
35
|
+
|
36
|
+
# Global cache for NTP servers to avoid multiple queries
|
37
|
+
_ntp_servers_cache = None
|
38
|
+
|
39
|
+
|
40
|
+
def natural_sort_key(port_name):
|
41
|
+
"""Extract numeric part from port name for natural sorting."""
|
42
|
+
match = re.search(r"(\d+)", port_name)
|
43
|
+
return int(match.group(1)) if match else 0
|
44
|
+
|
45
|
+
|
46
|
+
def generate_sonic_config(device, hwsku, device_as_mapping=None):
|
47
|
+
"""Generate minimal SONiC config.json for a device.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
device: NetBox device object
|
51
|
+
hwsku: Hardware SKU name
|
52
|
+
device_as_mapping: Dict mapping device IDs to pre-calculated AS numbers for spine/superspine groups
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
dict: Minimal SONiC configuration dictionary
|
56
|
+
"""
|
57
|
+
# Get port configuration for the HWSKU
|
58
|
+
port_config = get_port_config(hwsku)
|
59
|
+
|
60
|
+
# Get port channel configuration from NetBox first (needed by get_connected_interfaces)
|
61
|
+
portchannel_info = detect_port_channels(device)
|
62
|
+
|
63
|
+
# Get connected interfaces to determine admin_status
|
64
|
+
connected_interfaces, connected_portchannels = get_connected_interfaces(
|
65
|
+
device, portchannel_info
|
66
|
+
)
|
67
|
+
|
68
|
+
# Get OOB IP for management interface
|
69
|
+
oob_ip_result = get_device_oob_ip(device)
|
70
|
+
|
71
|
+
# Get VLAN configuration from NetBox
|
72
|
+
vlan_info = get_device_vlans(device)
|
73
|
+
|
74
|
+
# Get Loopback configuration from NetBox
|
75
|
+
loopback_info = get_device_loopbacks(device)
|
76
|
+
|
77
|
+
# Get breakout port configuration from NetBox
|
78
|
+
breakout_info = detect_breakout_ports(device)
|
79
|
+
|
80
|
+
# Get all interfaces from NetBox with their speeds and types
|
81
|
+
netbox_interfaces = {}
|
82
|
+
try:
|
83
|
+
interfaces = get_cached_device_interfaces(device.id)
|
84
|
+
for interface in interfaces:
|
85
|
+
# Convert NetBox interface name to SONiC format for lookup
|
86
|
+
interface_speed = getattr(interface, "speed", None)
|
87
|
+
# If speed is not set, try to get it from port type
|
88
|
+
if not interface_speed and hasattr(interface, "type") and interface.type:
|
89
|
+
interface_speed = get_speed_from_port_type(interface.type.value)
|
90
|
+
sonic_name = convert_netbox_interface_to_sonic(interface, device)
|
91
|
+
netbox_interfaces[sonic_name] = {
|
92
|
+
"speed": interface_speed,
|
93
|
+
"type": (
|
94
|
+
getattr(interface.type, "value", None)
|
95
|
+
if hasattr(interface, "type") and interface.type
|
96
|
+
else None
|
97
|
+
),
|
98
|
+
"netbox_name": interface.name,
|
99
|
+
}
|
100
|
+
except Exception as e:
|
101
|
+
logger.warning(f"Could not get interface details from NetBox: {e}")
|
102
|
+
|
103
|
+
# Get device metadata using helper functions
|
104
|
+
platform = get_device_platform(device, hwsku)
|
105
|
+
hostname = get_device_hostname(device)
|
106
|
+
mac_address = get_device_mac_address(device)
|
107
|
+
|
108
|
+
# Try to load base configuration from /etc/sonic/config_db.json
|
109
|
+
# Always start with a fresh, empty configuration for each device
|
110
|
+
base_config_path = "/etc/sonic/config_db.json"
|
111
|
+
config = {}
|
112
|
+
|
113
|
+
try:
|
114
|
+
if os.path.exists(base_config_path):
|
115
|
+
with open(base_config_path, "r") as f:
|
116
|
+
base_config = json.load(f)
|
117
|
+
# Create a deep copy to ensure no cross-device contamination
|
118
|
+
config = copy.deepcopy(base_config)
|
119
|
+
logger.info(
|
120
|
+
f"Loaded fresh base configuration from {base_config_path} for device {device.name}"
|
121
|
+
)
|
122
|
+
else:
|
123
|
+
logger.debug(
|
124
|
+
f"Base config file {base_config_path} not found, starting with empty config for device {device.name}"
|
125
|
+
)
|
126
|
+
except Exception as e:
|
127
|
+
logger.warning(
|
128
|
+
f"Could not load base configuration from {base_config_path} for device {device.name}: {e}"
|
129
|
+
)
|
130
|
+
# Ensure we start fresh even on error
|
131
|
+
config = {}
|
132
|
+
|
133
|
+
# Update DEVICE_METADATA with NetBox information
|
134
|
+
if "localhost" not in config["DEVICE_METADATA"]:
|
135
|
+
config["DEVICE_METADATA"]["localhost"] = {}
|
136
|
+
|
137
|
+
config["DEVICE_METADATA"]["localhost"].update(
|
138
|
+
{
|
139
|
+
"hostname": hostname,
|
140
|
+
"hwsku": hwsku,
|
141
|
+
"platform": platform,
|
142
|
+
"mac": mac_address,
|
143
|
+
}
|
144
|
+
)
|
145
|
+
|
146
|
+
# Add BGP_GLOBALS configuration with router_id set to primary IP address
|
147
|
+
primary_ip = None
|
148
|
+
if device.primary_ip4:
|
149
|
+
primary_ip = str(device.primary_ip4.address).split("/")[0]
|
150
|
+
elif device.primary_ip6:
|
151
|
+
primary_ip = str(device.primary_ip6.address).split("/")[0]
|
152
|
+
|
153
|
+
if primary_ip:
|
154
|
+
if "default" not in config["BGP_GLOBALS"]:
|
155
|
+
config["BGP_GLOBALS"]["default"] = {}
|
156
|
+
config["BGP_GLOBALS"]["default"]["router_id"] = primary_ip
|
157
|
+
|
158
|
+
# Calculate and add local_asn from router_id (only for IPv4)
|
159
|
+
if device.primary_ip4:
|
160
|
+
try:
|
161
|
+
# Check if device is in a spine/superspine group with pre-calculated AS
|
162
|
+
if device_as_mapping and device.id in device_as_mapping:
|
163
|
+
local_asn = device_as_mapping[device.id]
|
164
|
+
logger.debug(
|
165
|
+
f"Using group-calculated AS {local_asn} for spine/superspine device {device.name}"
|
166
|
+
)
|
167
|
+
else:
|
168
|
+
# Use normal AS calculation for leaf switches and non-grouped devices
|
169
|
+
local_asn = calculate_local_asn_from_ipv4(primary_ip)
|
170
|
+
|
171
|
+
config["BGP_GLOBALS"]["default"]["local_asn"] = str(local_asn)
|
172
|
+
except ValueError as e:
|
173
|
+
logger.warning(
|
174
|
+
f"Could not calculate local ASN for device {device.name}: {e}"
|
175
|
+
)
|
176
|
+
|
177
|
+
# Add port configurations
|
178
|
+
_add_port_configurations(
|
179
|
+
config,
|
180
|
+
port_config,
|
181
|
+
connected_interfaces,
|
182
|
+
portchannel_info,
|
183
|
+
breakout_info,
|
184
|
+
netbox_interfaces,
|
185
|
+
vlan_info,
|
186
|
+
device,
|
187
|
+
)
|
188
|
+
|
189
|
+
# Add interface configurations
|
190
|
+
_add_interface_configurations(config, connected_interfaces, portchannel_info)
|
191
|
+
|
192
|
+
# Add BGP configurations
|
193
|
+
_add_bgp_configurations(
|
194
|
+
config,
|
195
|
+
connected_interfaces,
|
196
|
+
connected_portchannels,
|
197
|
+
portchannel_info,
|
198
|
+
device,
|
199
|
+
device_as_mapping,
|
200
|
+
)
|
201
|
+
|
202
|
+
# Add NTP server configuration (device-specific)
|
203
|
+
_add_ntp_configuration(config, device)
|
204
|
+
|
205
|
+
# Add VLAN configuration
|
206
|
+
_add_vlan_configuration(config, vlan_info, netbox_interfaces, device)
|
207
|
+
|
208
|
+
# Add Loopback configuration
|
209
|
+
_add_loopback_configuration(config, loopback_info)
|
210
|
+
|
211
|
+
# Add management interface configuration
|
212
|
+
if oob_ip_result:
|
213
|
+
oob_ip, prefix_len = oob_ip_result
|
214
|
+
config["MGMT_INTERFACE"]["eth0"] = {"admin_status": "up"}
|
215
|
+
config["MGMT_INTERFACE"][f"eth0|{oob_ip}/{prefix_len}"] = {}
|
216
|
+
|
217
|
+
# Add breakout configuration
|
218
|
+
if breakout_info["breakout_cfgs"]:
|
219
|
+
config["BREAKOUT_CFG"].update(breakout_info["breakout_cfgs"])
|
220
|
+
if breakout_info["breakout_ports"]:
|
221
|
+
config["BREAKOUT_PORTS"].update(breakout_info["breakout_ports"])
|
222
|
+
|
223
|
+
# Add port channel configuration
|
224
|
+
_add_portchannel_configuration(config, portchannel_info)
|
225
|
+
|
226
|
+
return config
|
227
|
+
|
228
|
+
|
229
|
+
def _add_port_configurations(
|
230
|
+
config,
|
231
|
+
port_config,
|
232
|
+
connected_interfaces,
|
233
|
+
portchannel_info,
|
234
|
+
breakout_info,
|
235
|
+
netbox_interfaces,
|
236
|
+
vlan_info,
|
237
|
+
device,
|
238
|
+
):
|
239
|
+
"""Add port configurations to config."""
|
240
|
+
# Sort ports naturally (Ethernet0, Ethernet4, Ethernet8, ...)
|
241
|
+
sorted_ports = sorted(port_config.keys(), key=natural_sort_key)
|
242
|
+
|
243
|
+
for port_name in sorted_ports:
|
244
|
+
port_info = port_config[port_name]
|
245
|
+
|
246
|
+
# Skip master ports that have breakout configurations
|
247
|
+
# These will be replaced by their individual breakout ports
|
248
|
+
if port_name in breakout_info["breakout_cfgs"]:
|
249
|
+
logger.debug(
|
250
|
+
f"Skipping master port {port_name} - has breakout configuration"
|
251
|
+
)
|
252
|
+
continue
|
253
|
+
|
254
|
+
# Set admin_status to "up" if port is connected or is a port channel member, otherwise "down"
|
255
|
+
admin_status = (
|
256
|
+
"up"
|
257
|
+
if (
|
258
|
+
port_name in connected_interfaces
|
259
|
+
or port_name in portchannel_info["member_mapping"]
|
260
|
+
)
|
261
|
+
else "down"
|
262
|
+
)
|
263
|
+
|
264
|
+
# Check if this port is a breakout port and adjust speed and lanes accordingly
|
265
|
+
port_speed = port_info["speed"]
|
266
|
+
port_lanes = port_info["lanes"]
|
267
|
+
|
268
|
+
# Override with NetBox data if available and hardware config has no speed
|
269
|
+
if port_name in netbox_interfaces:
|
270
|
+
netbox_speed = netbox_interfaces[port_name]["speed"]
|
271
|
+
if netbox_speed and (not port_speed or port_speed == "0"):
|
272
|
+
logger.info(
|
273
|
+
f"Using NetBox speed {netbox_speed} for port {port_name} (hardware config had: {port_speed})"
|
274
|
+
)
|
275
|
+
port_speed = str(netbox_speed)
|
276
|
+
|
277
|
+
if port_name in breakout_info["breakout_ports"]:
|
278
|
+
# Get the master port to determine original speed and lanes
|
279
|
+
master_port = breakout_info["breakout_ports"][port_name]["master"]
|
280
|
+
|
281
|
+
# Override with individual breakout port speed from NetBox if available
|
282
|
+
if port_name in netbox_interfaces and netbox_interfaces[port_name]["speed"]:
|
283
|
+
port_speed = str(netbox_interfaces[port_name]["speed"])
|
284
|
+
logger.debug(
|
285
|
+
f"Using NetBox speed {port_speed} for breakout port {port_name}"
|
286
|
+
)
|
287
|
+
elif master_port in breakout_info["breakout_cfgs"]:
|
288
|
+
# Fallback to extracting speed from breakout mode
|
289
|
+
brkout_mode = breakout_info["breakout_cfgs"][master_port]["brkout_mode"]
|
290
|
+
if "25G" in brkout_mode:
|
291
|
+
port_speed = "25000"
|
292
|
+
elif "50G" in brkout_mode:
|
293
|
+
port_speed = "50000"
|
294
|
+
elif "100G" in brkout_mode:
|
295
|
+
port_speed = "100000"
|
296
|
+
elif "200G" in brkout_mode:
|
297
|
+
port_speed = "200000"
|
298
|
+
|
299
|
+
# Calculate individual lane for this breakout port
|
300
|
+
port_lanes = _calculate_breakout_port_lane(
|
301
|
+
port_name, master_port, port_config
|
302
|
+
)
|
303
|
+
|
304
|
+
# Generate correct alias based on port name and speed
|
305
|
+
interface_speed = int(port_speed) if port_speed else None
|
306
|
+
is_breakout_port = port_name in breakout_info["breakout_ports"]
|
307
|
+
correct_alias = convert_sonic_interface_to_alias(
|
308
|
+
port_name, interface_speed, is_breakout_port
|
309
|
+
)
|
310
|
+
|
311
|
+
# Use master port index for breakout ports
|
312
|
+
port_index = port_info["index"]
|
313
|
+
if is_breakout_port:
|
314
|
+
master_port = breakout_info["breakout_ports"][port_name]["master"]
|
315
|
+
if master_port in port_config:
|
316
|
+
port_index = port_config[master_port]["index"]
|
317
|
+
|
318
|
+
port_data = {
|
319
|
+
"admin_status": admin_status,
|
320
|
+
"alias": correct_alias,
|
321
|
+
"index": port_index,
|
322
|
+
"lanes": port_lanes,
|
323
|
+
"speed": port_speed,
|
324
|
+
"mtu": "9100",
|
325
|
+
"adv_speeds": "all",
|
326
|
+
"autoneg": "off",
|
327
|
+
"link_training": "off",
|
328
|
+
"unreliable_los": "auto",
|
329
|
+
}
|
330
|
+
|
331
|
+
# Add valid_speeds if available in port_info
|
332
|
+
if "valid_speeds" in port_info:
|
333
|
+
port_data["valid_speeds"] = port_info["valid_speeds"]
|
334
|
+
|
335
|
+
# Override valid_speeds for breakout ports based on their individual speed
|
336
|
+
if port_name in breakout_info["breakout_ports"]:
|
337
|
+
# For breakout ports, set valid_speeds based on the port's speed
|
338
|
+
breakout_valid_speeds = _get_breakout_port_valid_speeds(port_speed)
|
339
|
+
if breakout_valid_speeds:
|
340
|
+
port_data["valid_speeds"] = breakout_valid_speeds
|
341
|
+
|
342
|
+
config["PORT"][port_name] = port_data
|
343
|
+
|
344
|
+
# Add all breakout ports (since master ports were skipped above)
|
345
|
+
_add_missing_breakout_ports(
|
346
|
+
config,
|
347
|
+
breakout_info,
|
348
|
+
port_config,
|
349
|
+
connected_interfaces,
|
350
|
+
portchannel_info,
|
351
|
+
netbox_interfaces,
|
352
|
+
)
|
353
|
+
|
354
|
+
# Add tagged VLANs to PORT configuration
|
355
|
+
_add_tagged_vlans_to_ports(config, vlan_info, netbox_interfaces, device)
|
356
|
+
|
357
|
+
|
358
|
+
def _get_breakout_port_valid_speeds(port_speed):
|
359
|
+
"""Get valid speeds for a breakout port based on its configured speed."""
|
360
|
+
if not port_speed:
|
361
|
+
return None
|
362
|
+
|
363
|
+
speed_int = int(port_speed)
|
364
|
+
|
365
|
+
if speed_int == 25000:
|
366
|
+
return "25000,10000,1000"
|
367
|
+
elif speed_int == 50000:
|
368
|
+
return "50000,25000,10000,1000"
|
369
|
+
elif speed_int == 100000:
|
370
|
+
return "100000,50000,25000,10000,1000"
|
371
|
+
elif speed_int == 200000:
|
372
|
+
return "200000,100000,50000,25000,10000,1000"
|
373
|
+
else:
|
374
|
+
# For other speeds, include common lower speeds
|
375
|
+
return f"{port_speed},10000,1000"
|
376
|
+
|
377
|
+
|
378
|
+
def _calculate_breakout_port_lane(port_name, master_port, port_config):
|
379
|
+
"""Calculate individual lane for a breakout port."""
|
380
|
+
# Get master port's lanes from port_config
|
381
|
+
if master_port in port_config:
|
382
|
+
master_lanes = port_config[master_port]["lanes"]
|
383
|
+
# Parse lane range (e.g., "1,2,3,4" or "1-4")
|
384
|
+
if "," in master_lanes:
|
385
|
+
lanes_list = [int(lane.strip()) for lane in master_lanes.split(",")]
|
386
|
+
elif "-" in master_lanes:
|
387
|
+
start, end = map(int, master_lanes.split("-"))
|
388
|
+
lanes_list = list(range(start, end + 1))
|
389
|
+
else:
|
390
|
+
# Single lane or simple number
|
391
|
+
lanes_list = [int(master_lanes)]
|
392
|
+
|
393
|
+
# Calculate which lane this breakout port should use
|
394
|
+
port_match = re.match(r"Ethernet(\d+)", port_name)
|
395
|
+
if port_match:
|
396
|
+
sonic_port_num = int(port_match.group(1))
|
397
|
+
master_port_match = re.match(r"Ethernet(\d+)", master_port)
|
398
|
+
if master_port_match:
|
399
|
+
master_port_num = int(master_port_match.group(1))
|
400
|
+
# Calculate subport index (0, 1, 2, 3 for 4x breakout)
|
401
|
+
subport_index = sonic_port_num - master_port_num
|
402
|
+
if 0 <= subport_index < len(lanes_list):
|
403
|
+
return str(lanes_list[subport_index])
|
404
|
+
else:
|
405
|
+
logger.warning(
|
406
|
+
f"Breakout port {port_name}: subport_index {subport_index} out of range for lanes_list {lanes_list}"
|
407
|
+
)
|
408
|
+
return "1" # Default fallback
|
409
|
+
|
410
|
+
|
411
|
+
def _add_missing_breakout_ports(
|
412
|
+
config,
|
413
|
+
breakout_info,
|
414
|
+
port_config,
|
415
|
+
connected_interfaces,
|
416
|
+
portchannel_info,
|
417
|
+
netbox_interfaces,
|
418
|
+
):
|
419
|
+
"""Add all breakout ports to config (master ports are skipped in main loop)."""
|
420
|
+
for port_name in breakout_info["breakout_ports"]:
|
421
|
+
if port_name not in config["PORT"]:
|
422
|
+
# Get the master port to determine configuration
|
423
|
+
master_port = breakout_info["breakout_ports"][port_name]["master"]
|
424
|
+
|
425
|
+
# Override with individual breakout port speed from NetBox if available
|
426
|
+
if port_name in netbox_interfaces and netbox_interfaces[port_name]["speed"]:
|
427
|
+
port_speed = str(netbox_interfaces[port_name]["speed"])
|
428
|
+
logger.debug(
|
429
|
+
f"Using NetBox speed {port_speed} for missing breakout port {port_name}"
|
430
|
+
)
|
431
|
+
elif master_port in breakout_info["breakout_cfgs"]:
|
432
|
+
# Fallback to extracting speed from breakout mode
|
433
|
+
brkout_mode = breakout_info["breakout_cfgs"][master_port]["brkout_mode"]
|
434
|
+
if "25G" in brkout_mode:
|
435
|
+
port_speed = "25000"
|
436
|
+
elif "50G" in brkout_mode:
|
437
|
+
port_speed = "50000"
|
438
|
+
elif "100G" in brkout_mode:
|
439
|
+
port_speed = "100000"
|
440
|
+
elif "200G" in brkout_mode:
|
441
|
+
port_speed = "200000"
|
442
|
+
else:
|
443
|
+
port_speed = "25000" # Default fallback
|
444
|
+
else:
|
445
|
+
port_speed = "25000" # Default fallback
|
446
|
+
|
447
|
+
# Set admin_status based on connection or port channel membership
|
448
|
+
admin_status = (
|
449
|
+
"up"
|
450
|
+
if (
|
451
|
+
port_name in connected_interfaces
|
452
|
+
or port_name in portchannel_info["member_mapping"]
|
453
|
+
)
|
454
|
+
else "down"
|
455
|
+
)
|
456
|
+
|
457
|
+
# Generate correct alias (breakout port always gets subport notation)
|
458
|
+
interface_speed = int(port_speed)
|
459
|
+
correct_alias = convert_sonic_interface_to_alias(
|
460
|
+
port_name, interface_speed, is_breakout=True
|
461
|
+
)
|
462
|
+
|
463
|
+
# Use master port index for breakout ports
|
464
|
+
port_index = "1" # Default fallback
|
465
|
+
if master_port in port_config:
|
466
|
+
port_index = port_config[master_port]["index"]
|
467
|
+
|
468
|
+
# Calculate individual lane for this breakout port
|
469
|
+
port_lanes = _calculate_breakout_port_lane(
|
470
|
+
port_name, master_port, port_config
|
471
|
+
)
|
472
|
+
|
473
|
+
port_data = {
|
474
|
+
"admin_status": admin_status,
|
475
|
+
"alias": correct_alias,
|
476
|
+
"index": port_index,
|
477
|
+
"lanes": port_lanes,
|
478
|
+
"speed": port_speed,
|
479
|
+
"mtu": "9100",
|
480
|
+
"adv_speeds": "all",
|
481
|
+
"autoneg": "off",
|
482
|
+
"link_training": "off",
|
483
|
+
"unreliable_los": "auto",
|
484
|
+
}
|
485
|
+
|
486
|
+
# For breakout ports, check if master port has valid_speeds
|
487
|
+
if (
|
488
|
+
master_port in port_config
|
489
|
+
and "valid_speeds" in port_config[master_port]
|
490
|
+
):
|
491
|
+
port_data["valid_speeds"] = port_config[master_port]["valid_speeds"]
|
492
|
+
|
493
|
+
# Override valid_speeds for breakout ports based on their individual speed
|
494
|
+
breakout_valid_speeds = _get_breakout_port_valid_speeds(port_speed)
|
495
|
+
if breakout_valid_speeds:
|
496
|
+
port_data["valid_speeds"] = breakout_valid_speeds
|
497
|
+
|
498
|
+
config["PORT"][port_name] = port_data
|
499
|
+
|
500
|
+
|
501
|
+
def _add_tagged_vlans_to_ports(config, vlan_info, netbox_interfaces, device):
|
502
|
+
"""Add tagged VLANs to PORT configuration."""
|
503
|
+
# Build a mapping of ports to their tagged VLANs
|
504
|
+
port_tagged_vlans = {}
|
505
|
+
for vid, members in vlan_info["vlan_members"].items():
|
506
|
+
for netbox_interface_name, tagging_mode in members.items():
|
507
|
+
# Convert NetBox interface name to SONiC format
|
508
|
+
# Try to find speed from netbox_interfaces
|
509
|
+
speed = None
|
510
|
+
for sonic_name, iface_info in netbox_interfaces.items():
|
511
|
+
if iface_info["netbox_name"] == netbox_interface_name:
|
512
|
+
speed = iface_info["speed"]
|
513
|
+
break
|
514
|
+
sonic_interface_name = convert_netbox_interface_to_sonic(
|
515
|
+
netbox_interface_name, device
|
516
|
+
)
|
517
|
+
|
518
|
+
# Only add if this is a tagged VLAN (not untagged)
|
519
|
+
if tagging_mode == "tagged":
|
520
|
+
if sonic_interface_name not in port_tagged_vlans:
|
521
|
+
port_tagged_vlans[sonic_interface_name] = []
|
522
|
+
port_tagged_vlans[sonic_interface_name].append(str(vid))
|
523
|
+
|
524
|
+
# Update PORT configuration with tagged VLANs
|
525
|
+
for port_name in config["PORT"]:
|
526
|
+
if port_name in port_tagged_vlans:
|
527
|
+
# Sort the VLAN IDs numerically for consistent ordering
|
528
|
+
tagged_vlans = sorted(port_tagged_vlans[port_name], key=int)
|
529
|
+
config["PORT"][port_name]["tagged_vlans"] = tagged_vlans
|
530
|
+
|
531
|
+
|
532
|
+
def _add_interface_configurations(config, connected_interfaces, portchannel_info):
|
533
|
+
"""Add INTERFACE configuration for connected interfaces."""
|
534
|
+
for port_name in config["PORT"]:
|
535
|
+
# Check if this port is in the connected interfaces set and not a port channel member
|
536
|
+
if (
|
537
|
+
port_name in connected_interfaces
|
538
|
+
and port_name not in portchannel_info["member_mapping"]
|
539
|
+
):
|
540
|
+
# Add interface to INTERFACE section with ipv6_use_link_local_only enabled
|
541
|
+
config["INTERFACE"][port_name] = {"ipv6_use_link_local_only": "enable"}
|
542
|
+
|
543
|
+
|
544
|
+
def _add_bgp_configurations(
|
545
|
+
config,
|
546
|
+
connected_interfaces,
|
547
|
+
connected_portchannels,
|
548
|
+
portchannel_info,
|
549
|
+
device,
|
550
|
+
device_as_mapping=None,
|
551
|
+
):
|
552
|
+
"""Add BGP configurations."""
|
553
|
+
# Add BGP_NEIGHBOR_AF configuration for connected interfaces
|
554
|
+
for port_name in config["PORT"]:
|
555
|
+
if (
|
556
|
+
port_name in connected_interfaces
|
557
|
+
and port_name not in portchannel_info["member_mapping"]
|
558
|
+
):
|
559
|
+
ipv4_key = f"default|{port_name}|ipv4_unicast"
|
560
|
+
ipv6_key = f"default|{port_name}|ipv6_unicast"
|
561
|
+
config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "true"}
|
562
|
+
config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "true"}
|
563
|
+
|
564
|
+
# Add BGP_NEIGHBOR_AF configuration for connected port channels
|
565
|
+
for pc_name in connected_portchannels:
|
566
|
+
ipv4_key = f"default|{pc_name}|ipv4_unicast"
|
567
|
+
ipv6_key = f"default|{pc_name}|ipv6_unicast"
|
568
|
+
config["BGP_NEIGHBOR_AF"][ipv4_key] = {"admin_status": "true"}
|
569
|
+
config["BGP_NEIGHBOR_AF"][ipv6_key] = {"admin_status": "true"}
|
570
|
+
|
571
|
+
# Add BGP_NEIGHBOR configuration for connected interfaces
|
572
|
+
for port_name in config["PORT"]:
|
573
|
+
if (
|
574
|
+
port_name in connected_interfaces
|
575
|
+
and port_name not in portchannel_info["member_mapping"]
|
576
|
+
):
|
577
|
+
neighbor_key = f"default|{port_name}"
|
578
|
+
|
579
|
+
# Determine peer_type based on connected device AS
|
580
|
+
peer_type = "external" # Default
|
581
|
+
connected_device = get_connected_device_for_sonic_interface(
|
582
|
+
device, port_name
|
583
|
+
)
|
584
|
+
if connected_device:
|
585
|
+
peer_type = _determine_peer_type(
|
586
|
+
device, connected_device, device_as_mapping
|
587
|
+
)
|
588
|
+
|
589
|
+
config["BGP_NEIGHBOR"][neighbor_key] = {
|
590
|
+
"peer_type": peer_type,
|
591
|
+
"v6only": "true",
|
592
|
+
}
|
593
|
+
|
594
|
+
# Add BGP_NEIGHBOR configuration for connected port channels
|
595
|
+
for pc_name in connected_portchannels:
|
596
|
+
neighbor_key = f"default|{pc_name}"
|
597
|
+
|
598
|
+
# Determine peer_type based on connected device AS
|
599
|
+
peer_type = "external" # Default
|
600
|
+
connected_device = get_connected_device_for_sonic_interface(device, pc_name)
|
601
|
+
if connected_device:
|
602
|
+
peer_type = _determine_peer_type(
|
603
|
+
device, connected_device, device_as_mapping
|
604
|
+
)
|
605
|
+
|
606
|
+
config["BGP_NEIGHBOR"][neighbor_key] = {
|
607
|
+
"peer_type": peer_type,
|
608
|
+
"v6only": "true",
|
609
|
+
}
|
610
|
+
|
611
|
+
# Add additional BGP_NEIGHBOR configuration using Loopback0 IP addresses
|
612
|
+
_add_loopback_bgp_neighbors(
|
613
|
+
config, device, portchannel_info, connected_interfaces, device_as_mapping
|
614
|
+
)
|
615
|
+
|
616
|
+
|
617
|
+
def _get_connected_device_for_interface(device, interface_name):
|
618
|
+
"""Get the connected device for a given interface name.
|
619
|
+
|
620
|
+
Args:
|
621
|
+
device: NetBox device object
|
622
|
+
interface_name: SONiC interface name (e.g., "Ethernet0")
|
623
|
+
|
624
|
+
Returns:
|
625
|
+
NetBox device object or None if not found
|
626
|
+
"""
|
627
|
+
return get_connected_device_for_sonic_interface(device, interface_name)
|
628
|
+
|
629
|
+
|
630
|
+
def _determine_peer_type(local_device, connected_device, device_as_mapping=None):
|
631
|
+
"""Determine BGP peer type (internal/external) based on AS number comparison.
|
632
|
+
|
633
|
+
Args:
|
634
|
+
local_device: Local NetBox device object
|
635
|
+
connected_device: Connected NetBox device object
|
636
|
+
device_as_mapping: Dict mapping device IDs to pre-calculated AS numbers
|
637
|
+
|
638
|
+
Returns:
|
639
|
+
str: "internal" if AS numbers match, "external" otherwise
|
640
|
+
"""
|
641
|
+
try:
|
642
|
+
# Get local AS number
|
643
|
+
local_as = None
|
644
|
+
if device_as_mapping and local_device.id in device_as_mapping:
|
645
|
+
local_as = device_as_mapping[local_device.id]
|
646
|
+
elif local_device.primary_ip4:
|
647
|
+
local_as = calculate_local_asn_from_ipv4(
|
648
|
+
str(local_device.primary_ip4.address)
|
649
|
+
)
|
650
|
+
|
651
|
+
# Get connected device AS number
|
652
|
+
connected_as = None
|
653
|
+
if device_as_mapping and connected_device.id in device_as_mapping:
|
654
|
+
connected_as = device_as_mapping[connected_device.id]
|
655
|
+
elif connected_device.primary_ip4:
|
656
|
+
connected_as = calculate_local_asn_from_ipv4(
|
657
|
+
str(connected_device.primary_ip4.address)
|
658
|
+
)
|
659
|
+
|
660
|
+
# Compare AS numbers
|
661
|
+
if local_as and connected_as and local_as == connected_as:
|
662
|
+
return "internal"
|
663
|
+
else:
|
664
|
+
return "external"
|
665
|
+
|
666
|
+
except Exception as e:
|
667
|
+
logger.debug(
|
668
|
+
f"Could not determine peer type between {local_device.name} and {connected_device.name}: {e}"
|
669
|
+
)
|
670
|
+
return "external" # Default to external on error
|
671
|
+
|
672
|
+
|
673
|
+
def _add_loopback_bgp_neighbors(
|
674
|
+
config, device, portchannel_info, connected_interfaces, device_as_mapping=None
|
675
|
+
):
|
676
|
+
"""Add BGP_NEIGHBOR configuration using Loopback0 IP addresses from connected devices."""
|
677
|
+
try:
|
678
|
+
# Get BGP neighbors via loopback using the new connections module
|
679
|
+
bgp_neighbors = get_device_bgp_neighbors_via_loopback(
|
680
|
+
device, portchannel_info, connected_interfaces, config["PORT"]
|
681
|
+
)
|
682
|
+
|
683
|
+
for neighbor_info in bgp_neighbors:
|
684
|
+
neighbor_key = f"default|{neighbor_info['ip']}"
|
685
|
+
|
686
|
+
# Determine peer_type based on AS comparison
|
687
|
+
peer_type = _determine_peer_type(
|
688
|
+
device,
|
689
|
+
neighbor_info["device"],
|
690
|
+
device_as_mapping,
|
691
|
+
)
|
692
|
+
|
693
|
+
config["BGP_NEIGHBOR"][neighbor_key] = {"peer_type": peer_type}
|
694
|
+
|
695
|
+
except Exception as e:
|
696
|
+
logger.warning(f"Could not process BGP neighbors for device {device.name}: {e}")
|
697
|
+
|
698
|
+
|
699
|
+
def _get_ntp_servers():
|
700
|
+
"""Get NTP servers from manager/metalbox devices. Uses caching to avoid repeated queries."""
|
701
|
+
global _ntp_servers_cache
|
702
|
+
|
703
|
+
if _ntp_servers_cache is not None:
|
704
|
+
logger.debug("Using cached NTP servers")
|
705
|
+
return _ntp_servers_cache
|
706
|
+
|
707
|
+
ntp_servers = {}
|
708
|
+
try:
|
709
|
+
# Get devices with manager or metalbox device roles
|
710
|
+
devices_manager = utils.nb.dcim.devices.filter(role="manager")
|
711
|
+
devices_metalbox = utils.nb.dcim.devices.filter(role="metalbox")
|
712
|
+
|
713
|
+
# Combine both device lists
|
714
|
+
ntp_devices = list(devices_manager) + list(devices_metalbox)
|
715
|
+
logger.debug(f"Found {len(ntp_devices)} potential NTP devices")
|
716
|
+
|
717
|
+
for ntp_device in ntp_devices:
|
718
|
+
# Get interfaces for this device to find Loopback0
|
719
|
+
device_interfaces = utils.nb.dcim.interfaces.filter(device_id=ntp_device.id)
|
720
|
+
|
721
|
+
for interface in device_interfaces:
|
722
|
+
# Look for Loopback0 interface
|
723
|
+
if interface.name == "Loopback0":
|
724
|
+
# Get IP addresses assigned to this Loopback0 interface
|
725
|
+
ip_addresses = utils.nb.ipam.ip_addresses.filter(
|
726
|
+
assigned_object_id=interface.id,
|
727
|
+
)
|
728
|
+
|
729
|
+
for ip_addr in ip_addresses:
|
730
|
+
if ip_addr.address:
|
731
|
+
# Extract just the IPv4 address without prefix
|
732
|
+
ip_only = ip_addr.address.split("/")[0]
|
733
|
+
|
734
|
+
# Check if it's an IPv4 address (simple check)
|
735
|
+
if "." in ip_only and ":" not in ip_only:
|
736
|
+
ntp_servers[ip_only] = {
|
737
|
+
"maxpoll": "10",
|
738
|
+
"minpoll": "6",
|
739
|
+
"prefer": "false",
|
740
|
+
}
|
741
|
+
logger.info(
|
742
|
+
f"Found NTP server {ip_only} from device {ntp_device.name} with role {ntp_device.role.slug}"
|
743
|
+
)
|
744
|
+
break
|
745
|
+
|
746
|
+
# Cache the results
|
747
|
+
_ntp_servers_cache = ntp_servers
|
748
|
+
logger.debug(f"Cached {len(ntp_servers)} NTP servers")
|
749
|
+
|
750
|
+
except Exception as e:
|
751
|
+
logger.warning(f"Could not process NTP servers: {e}")
|
752
|
+
_ntp_servers_cache = {}
|
753
|
+
|
754
|
+
return _ntp_servers_cache
|
755
|
+
|
756
|
+
|
757
|
+
def _add_ntp_configuration(config, device):
|
758
|
+
"""Add NTP_SERVER configuration to device config."""
|
759
|
+
try:
|
760
|
+
ntp_servers = _get_ntp_servers()
|
761
|
+
|
762
|
+
# Add NTP servers to this device's configuration
|
763
|
+
for ip, ntp_config in ntp_servers.items():
|
764
|
+
config["NTP_SERVER"][ip] = copy.deepcopy(ntp_config)
|
765
|
+
|
766
|
+
if ntp_servers:
|
767
|
+
logger.debug(
|
768
|
+
f"Added {len(ntp_servers)} NTP servers to device {device.name}"
|
769
|
+
)
|
770
|
+
else:
|
771
|
+
logger.debug(f"No NTP servers found for device {device.name}")
|
772
|
+
|
773
|
+
except Exception as e:
|
774
|
+
logger.warning(f"Could not add NTP configuration to device {device.name}: {e}")
|
775
|
+
|
776
|
+
|
777
|
+
def clear_ntp_cache():
|
778
|
+
"""Clear the NTP servers cache. Should be called at the start of sync_sonic."""
|
779
|
+
global _ntp_servers_cache
|
780
|
+
_ntp_servers_cache = None
|
781
|
+
logger.debug("Cleared NTP servers cache")
|
782
|
+
|
783
|
+
|
784
|
+
def clear_all_caches():
|
785
|
+
"""Clear all caches in config_generator module."""
|
786
|
+
clear_ntp_cache()
|
787
|
+
clear_port_config_cache()
|
788
|
+
logger.debug("Cleared all config_generator caches")
|
789
|
+
|
790
|
+
|
791
|
+
def _add_vlan_configuration(config, vlan_info, netbox_interfaces, device):
|
792
|
+
"""Add VLAN configuration from NetBox."""
|
793
|
+
# Add VLAN configuration
|
794
|
+
for vid, vlan_data in vlan_info["vlans"].items():
|
795
|
+
vlan_name = f"Vlan{vid}"
|
796
|
+
|
797
|
+
# Get member ports for this VLAN and convert interface names
|
798
|
+
members = []
|
799
|
+
if vid in vlan_info["vlan_members"]:
|
800
|
+
for netbox_interface_name in vlan_info["vlan_members"][vid].keys():
|
801
|
+
# Convert NetBox interface name to SONiC format
|
802
|
+
# Try to find speed from netbox_interfaces
|
803
|
+
speed = None
|
804
|
+
for sonic_name, iface_info in netbox_interfaces.items():
|
805
|
+
if iface_info["netbox_name"] == netbox_interface_name:
|
806
|
+
speed = iface_info["speed"]
|
807
|
+
break
|
808
|
+
sonic_interface_name = convert_netbox_interface_to_sonic(
|
809
|
+
netbox_interface_name, device
|
810
|
+
)
|
811
|
+
members.append(sonic_interface_name)
|
812
|
+
|
813
|
+
config["VLAN"][vlan_name] = {
|
814
|
+
"admin_status": "up",
|
815
|
+
"autostate": "enable",
|
816
|
+
"members": members,
|
817
|
+
"vlanid": str(vid),
|
818
|
+
}
|
819
|
+
|
820
|
+
# Add VLAN members
|
821
|
+
for vid, members in vlan_info["vlan_members"].items():
|
822
|
+
vlan_name = f"Vlan{vid}"
|
823
|
+
for netbox_interface_name, tagging_mode in members.items():
|
824
|
+
# Convert NetBox interface name to SONiC format
|
825
|
+
# Try to find speed from netbox_interfaces
|
826
|
+
speed = None
|
827
|
+
for sonic_name, iface_info in netbox_interfaces.items():
|
828
|
+
if iface_info["netbox_name"] == netbox_interface_name:
|
829
|
+
speed = iface_info["speed"]
|
830
|
+
break
|
831
|
+
sonic_interface_name = convert_netbox_interface_to_sonic(
|
832
|
+
netbox_interface_name, device
|
833
|
+
)
|
834
|
+
# Create VLAN_MEMBER key in format "Vlan<vid>|<port_name>"
|
835
|
+
member_key = f"{vlan_name}|{sonic_interface_name}"
|
836
|
+
config["VLAN_MEMBER"][member_key] = {"tagging_mode": tagging_mode}
|
837
|
+
|
838
|
+
# Add VLAN interfaces (SVIs)
|
839
|
+
for vid, interface_data in vlan_info["vlan_interfaces"].items():
|
840
|
+
vlan_name = f"Vlan{vid}"
|
841
|
+
if "addresses" in interface_data and interface_data["addresses"]:
|
842
|
+
# Add the VLAN interface
|
843
|
+
config["VLAN_INTERFACE"][vlan_name] = {"admin_status": "up"}
|
844
|
+
|
845
|
+
# Add IP configuration for each address (IPv4 and IPv6)
|
846
|
+
for address in interface_data["addresses"]:
|
847
|
+
ip_key = f"{vlan_name}|{address}"
|
848
|
+
config["VLAN_INTERFACE"][ip_key] = {}
|
849
|
+
|
850
|
+
|
851
|
+
def _add_loopback_configuration(config, loopback_info):
|
852
|
+
"""Add Loopback configuration from NetBox."""
|
853
|
+
for loopback_name, loopback_data in loopback_info["loopbacks"].items():
|
854
|
+
# Add the Loopback interface
|
855
|
+
config["LOOPBACK"][loopback_name] = {"admin_status": "up"}
|
856
|
+
|
857
|
+
# Add base Loopback interface entry
|
858
|
+
config["LOOPBACK_INTERFACE"][loopback_name] = {}
|
859
|
+
|
860
|
+
# Add IP configuration for each address (IPv4 and IPv6)
|
861
|
+
for address in loopback_data["addresses"]:
|
862
|
+
ip_key = f"{loopback_name}|{address}"
|
863
|
+
config["LOOPBACK_INTERFACE"][ip_key] = {}
|
864
|
+
|
865
|
+
# Add BGP_GLOBALS_AF_NETWORK configuration for Loopback0 devices
|
866
|
+
if loopback_name == "Loopback0":
|
867
|
+
for address in loopback_data["addresses"]:
|
868
|
+
# Determine if this is IPv4 or IPv6 and set appropriate address family
|
869
|
+
try:
|
870
|
+
ip_obj = ipaddress.ip_interface(address)
|
871
|
+
if ip_obj.version == 4:
|
872
|
+
af_key = f"default|ipv4_unicast|{address}"
|
873
|
+
elif ip_obj.version == 6:
|
874
|
+
af_key = f"default|ipv6_unicast|{address}"
|
875
|
+
else:
|
876
|
+
continue
|
877
|
+
|
878
|
+
config["BGP_GLOBALS_AF_NETWORK"][af_key] = {}
|
879
|
+
except ValueError:
|
880
|
+
logger.warning(f"Invalid IP address format: {address}")
|
881
|
+
continue
|
882
|
+
|
883
|
+
|
884
|
+
def _add_portchannel_configuration(config, portchannel_info):
|
885
|
+
"""Add port channel configuration from NetBox."""
|
886
|
+
if portchannel_info["portchannels"]:
|
887
|
+
for pc_name, pc_data in portchannel_info["portchannels"].items():
|
888
|
+
# Add PORTCHANNEL configuration
|
889
|
+
config["PORTCHANNEL"][pc_name] = {
|
890
|
+
"admin_status": pc_data["admin_status"],
|
891
|
+
"fast_rate": pc_data["fast_rate"],
|
892
|
+
"min_links": pc_data["min_links"],
|
893
|
+
"mtu": pc_data["mtu"],
|
894
|
+
}
|
895
|
+
|
896
|
+
# Add PORTCHANNEL_INTERFACE configuration to enable IPv6 link-local
|
897
|
+
config["PORTCHANNEL_INTERFACE"][pc_name] = {
|
898
|
+
"ipv6_use_link_local_only": "enable"
|
899
|
+
}
|
900
|
+
|
901
|
+
# Add PORTCHANNEL_MEMBER configuration for each member
|
902
|
+
for member in pc_data["members"]:
|
903
|
+
member_key = f"{pc_name}|{member}"
|
904
|
+
config["PORTCHANNEL_MEMBER"][member_key] = {}
|
905
|
+
|
906
|
+
logger.debug(
|
907
|
+
f"Added port channel {pc_name} with {len(pc_data['members'])} members"
|
908
|
+
)
|