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.
@@ -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
+ )