osism 0.20250605.0__py3-none-any.whl → 0.20250616.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,1401 @@
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