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,389 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Centralized connection detection functions for SONiC configuration.
4
+
5
+ This module provides unified helper functions for detecting connected devices
6
+ using the NetBox connected_endpoints API, replacing legacy cable-based detection.
7
+ """
8
+
9
+ from loguru import logger
10
+ from typing import Optional, Set, Tuple, List, Any, DefaultDict
11
+ from collections import defaultdict
12
+
13
+ from osism import utils
14
+ from .interface import (
15
+ convert_netbox_interface_to_sonic,
16
+ )
17
+ from .cache import get_cached_device_interfaces
18
+
19
+
20
+ def get_connected_device_via_interface(
21
+ interface: Any, source_device_id: int
22
+ ) -> Optional[Any]:
23
+ """Get the connected device for a given interface using connected_endpoints API.
24
+
25
+ Args:
26
+ interface: NetBox interface object
27
+ source_device_id: ID of the source device to exclude from results
28
+
29
+ Returns:
30
+ Connected NetBox device object or None if not found/reachable
31
+ """
32
+ # Skip management-only interfaces
33
+ if hasattr(interface, "mgmt_only") and interface.mgmt_only:
34
+ return None
35
+
36
+ # Check if interface has connected_endpoints
37
+ if not (
38
+ hasattr(interface, "connected_endpoints") and interface.connected_endpoints
39
+ ):
40
+ return None
41
+
42
+ # Ensure connected_endpoints_reachable is True
43
+ if not getattr(interface, "connected_endpoints_reachable", False):
44
+ return None
45
+
46
+ try:
47
+ # Process each connected endpoint
48
+ for endpoint in interface.connected_endpoints:
49
+ # Get the connected device from the endpoint
50
+ if hasattr(endpoint, "device") and endpoint.device.id != source_device_id:
51
+ return endpoint.device
52
+ except Exception as e:
53
+ logger.debug(
54
+ f"Error processing connected_endpoints for interface {interface.name}: {e}"
55
+ )
56
+
57
+ return None
58
+
59
+
60
+ def get_connected_interfaces(
61
+ device: Any, portchannel_info: Optional[dict] = None
62
+ ) -> Tuple[Set[str], Set[str]]:
63
+ """Get list of interface names that are connected to other devices.
64
+
65
+ Uses the modern connected_endpoints API as the primary method for detecting
66
+ connections, with reachability checks.
67
+
68
+ Args:
69
+ device: NetBox device object
70
+ portchannel_info: Optional port channel info dict from detect_port_channels
71
+
72
+ Returns:
73
+ tuple: (set of connected interfaces, set of connected port channels)
74
+ """
75
+ connected_interfaces = set()
76
+ connected_portchannels = set()
77
+
78
+ try:
79
+ # Get all interfaces for the device (using cache)
80
+ interfaces = get_cached_device_interfaces(device.id)
81
+
82
+ for interface in interfaces:
83
+ # Skip management-only interfaces
84
+ if hasattr(interface, "mgmt_only") and interface.mgmt_only:
85
+ continue
86
+
87
+ # Check if interface is connected using connected_endpoints API
88
+ connected_device = get_connected_device_via_interface(interface, device.id)
89
+
90
+ if connected_device:
91
+ # Convert NetBox interface name to SONiC format
92
+ sonic_interface_name = convert_netbox_interface_to_sonic(
93
+ interface, device
94
+ )
95
+ connected_interfaces.add(sonic_interface_name)
96
+
97
+ # If this interface is part of a port channel, mark the port channel as connected
98
+ if (
99
+ portchannel_info
100
+ and sonic_interface_name in portchannel_info["member_mapping"]
101
+ ):
102
+ pc_name = portchannel_info["member_mapping"][sonic_interface_name]
103
+ connected_portchannels.add(pc_name)
104
+
105
+ except Exception as e:
106
+ logger.warning(
107
+ f"Could not get interface connections for device {device.name}: {e}"
108
+ )
109
+
110
+ return connected_interfaces, connected_portchannels
111
+
112
+
113
+ def get_connected_device_for_sonic_interface(
114
+ device: Any, sonic_interface_name: str
115
+ ) -> Optional[Any]:
116
+ """Get the connected device for a given SONiC interface name.
117
+
118
+ For Port Channels, uses the member ports to detect connected devices.
119
+
120
+ Args:
121
+ device: NetBox device object
122
+ sonic_interface_name: SONiC interface name (e.g., "Ethernet0" or "PortChannel1")
123
+
124
+ Returns:
125
+ NetBox device object or None if not found
126
+ """
127
+ try:
128
+ # Check if this is a Port Channel
129
+ if sonic_interface_name.startswith("PortChannel"):
130
+ return get_connected_device_for_port_channel(device, sonic_interface_name)
131
+
132
+ # Handle regular interfaces
133
+ interfaces = get_cached_device_interfaces(device.id)
134
+
135
+ for interface in interfaces:
136
+ # Convert NetBox interface name to SONiC format
137
+ sonic_name = convert_netbox_interface_to_sonic(interface, device)
138
+
139
+ if sonic_name == sonic_interface_name:
140
+ return get_connected_device_via_interface(interface, device.id)
141
+
142
+ except Exception as e:
143
+ logger.debug(
144
+ f"Could not find connected device for interface {sonic_interface_name}: {e}"
145
+ )
146
+
147
+ return None
148
+
149
+
150
+ def get_connected_device_for_port_channel(
151
+ device: Any, portchannel_name: str
152
+ ) -> Optional[Any]:
153
+ """Get the connected device for a Port Channel by checking its member ports.
154
+
155
+ Args:
156
+ device: NetBox device object
157
+ portchannel_name: Port Channel name (e.g., "PortChannel1")
158
+
159
+ Returns:
160
+ NetBox device object or None if not found
161
+ """
162
+ try:
163
+ # Import here to avoid circular imports
164
+ from .interface import detect_port_channels
165
+
166
+ # Get port channel information to find member ports
167
+ portchannel_info = detect_port_channels(device)
168
+
169
+ if portchannel_name not in portchannel_info["portchannels"]:
170
+ logger.debug(
171
+ f"Port Channel {portchannel_name} not found on device {device.name}"
172
+ )
173
+ return None
174
+
175
+ member_ports = portchannel_info["portchannels"][portchannel_name]["members"]
176
+
177
+ if not member_ports:
178
+ logger.debug(f"No member ports found for Port Channel {portchannel_name}")
179
+ return None
180
+
181
+ # Check each member port to find a connected device
182
+ # All member ports in a Port Channel should connect to the same remote device
183
+ interfaces = get_cached_device_interfaces(device.id)
184
+
185
+ for member_port in member_ports:
186
+ # Find the NetBox interface corresponding to this member port
187
+ for interface in interfaces:
188
+ sonic_name = convert_netbox_interface_to_sonic(interface, device)
189
+ if sonic_name == member_port:
190
+ connected_device = get_connected_device_via_interface(
191
+ interface, device.id
192
+ )
193
+ if connected_device:
194
+ logger.debug(
195
+ f"Found connected device {connected_device.name} for Port Channel {portchannel_name} "
196
+ f"via member port {member_port}"
197
+ )
198
+ return connected_device
199
+ break
200
+
201
+ logger.debug(
202
+ f"No connected device found for any member ports of {portchannel_name}"
203
+ )
204
+ return None
205
+
206
+ except Exception as e:
207
+ logger.debug(
208
+ f"Could not find connected device for Port Channel {portchannel_name}: {e}"
209
+ )
210
+ return None
211
+
212
+
213
+ def find_interconnected_devices(
214
+ devices: List[Any], target_roles: List[str] = ["spine", "superspine"]
215
+ ) -> List[List[Any]]:
216
+ """Find groups of interconnected devices with specific roles.
217
+
218
+ Uses connected_endpoints API to build a graph of device connections.
219
+
220
+ Args:
221
+ devices: List of NetBox device objects
222
+ target_roles: List of device roles to consider
223
+
224
+ Returns:
225
+ List of groups, where each group is a list of interconnected devices of the same role
226
+ """
227
+ from collections import deque
228
+
229
+ # Filter devices by target roles
230
+ target_devices = {}
231
+ for device in devices:
232
+ if hasattr(device, "role") and device.role and device.role.slug in target_roles:
233
+ target_devices[device.id] = device
234
+
235
+ if not target_devices:
236
+ return []
237
+
238
+ # Build connection graph for each role separately
239
+ role_graphs: DefaultDict[str, DefaultDict[int, Set[int]]] = defaultdict(
240
+ lambda: defaultdict(set)
241
+ )
242
+
243
+ for device in target_devices.values():
244
+ device_role = device.role.slug
245
+
246
+ try:
247
+ # Get all interfaces for this device
248
+ interfaces = get_cached_device_interfaces(device.id)
249
+
250
+ for interface in interfaces:
251
+ # Get connected device using our helper
252
+ connected_device = get_connected_device_via_interface(
253
+ interface, device.id
254
+ )
255
+
256
+ if (
257
+ connected_device
258
+ and connected_device.id in target_devices
259
+ and connected_device.role.slug == device_role
260
+ ):
261
+ # Add bidirectional connection to the graph
262
+ role_graphs[device_role][device.id].add(connected_device.id)
263
+ role_graphs[device_role][connected_device.id].add(device.id)
264
+
265
+ except Exception as e:
266
+ logger.warning(f"Error processing device {device.name} for grouping: {e}")
267
+
268
+ # Find connected components for each role using BFS
269
+ all_groups = []
270
+
271
+ for role, graph in role_graphs.items():
272
+ visited = set()
273
+
274
+ for device_id in graph:
275
+ if device_id not in visited:
276
+ # BFS to find all connected devices
277
+ group = []
278
+ queue = deque([device_id])
279
+ visited.add(device_id)
280
+
281
+ while queue:
282
+ current_id = queue.popleft()
283
+ group.append(target_devices[current_id])
284
+
285
+ for neighbor_id in graph[current_id]:
286
+ if neighbor_id not in visited:
287
+ visited.add(neighbor_id)
288
+ queue.append(neighbor_id)
289
+
290
+ if len(group) > 1: # Only include groups with multiple devices
291
+ all_groups.append(group)
292
+
293
+ return all_groups
294
+
295
+
296
+ def get_device_bgp_neighbors_via_loopback(
297
+ device: Any,
298
+ portchannel_info: dict,
299
+ connected_interfaces: Set[str],
300
+ port_config: dict,
301
+ ) -> List[dict]:
302
+ """Get BGP neighbors for a device based on Loopback0 addresses of connected devices.
303
+
304
+ Args:
305
+ device: NetBox device object
306
+ portchannel_info: Port channel information
307
+ connected_interfaces: Set of connected interface names
308
+ port_config: Port configuration dict
309
+
310
+ Returns:
311
+ List of BGP neighbor dictionaries with IP and device info
312
+ """
313
+ bgp_neighbors = []
314
+
315
+ try:
316
+ # Get all interfaces for the device
317
+ interfaces = get_cached_device_interfaces(device.id)
318
+
319
+ for interface in interfaces:
320
+ # Skip management-only interfaces
321
+ if hasattr(interface, "mgmt_only") and interface.mgmt_only:
322
+ continue
323
+
324
+ # Get connected device
325
+ connected_device = get_connected_device_via_interface(interface, device.id)
326
+ if not connected_device:
327
+ continue
328
+
329
+ # Convert to SONiC interface name to check if it's in our PORT config
330
+ sonic_interface_name = convert_netbox_interface_to_sonic(interface, device)
331
+
332
+ # Only process if this interface is in PORT configuration and connected
333
+ if (
334
+ sonic_interface_name in port_config
335
+ and sonic_interface_name in connected_interfaces
336
+ and sonic_interface_name not in portchannel_info["member_mapping"]
337
+ ):
338
+ # Check if connected device has the required tag
339
+ has_osism_tag = False
340
+ if connected_device.tags:
341
+ has_osism_tag = any(
342
+ tag.slug == "managed-by-osism" for tag in connected_device.tags
343
+ )
344
+
345
+ if has_osism_tag:
346
+ # Get Loopback0 IP addresses from the connected device
347
+ try:
348
+ # Get all interfaces for connected device (using cache)
349
+ all_connected_interfaces = get_cached_device_interfaces(
350
+ connected_device.id
351
+ )
352
+ loopback_interfaces: List[Any] = [
353
+ iface
354
+ for iface in all_connected_interfaces
355
+ if iface.name == "Loopback0"
356
+ ]
357
+
358
+ for loopback_iface in loopback_interfaces:
359
+ # Get IP addresses assigned to Loopback0
360
+ ip_addresses = utils.nb.ipam.ip_addresses.filter(
361
+ assigned_object_id=loopback_iface.id,
362
+ )
363
+
364
+ for ip_addr in ip_addresses:
365
+ if ip_addr.address:
366
+ # Extract just the IP address without prefix
367
+ ip_only = ip_addr.address.split("/")[0]
368
+ bgp_neighbors.append(
369
+ {
370
+ "ip": ip_only,
371
+ "device": connected_device,
372
+ "interface": sonic_interface_name,
373
+ }
374
+ )
375
+
376
+ except Exception as e:
377
+ logger.debug(
378
+ f"Could not get Loopback0 for device {connected_device.name}: {e}"
379
+ )
380
+ else:
381
+ logger.debug(
382
+ f"Skipping BGP neighbor for device {connected_device.name}: "
383
+ f"missing 'managed-by-osism' tag"
384
+ )
385
+
386
+ except Exception as e:
387
+ logger.warning(f"Could not process BGP neighbors for device {device.name}: {e}")
388
+
389
+ return bgp_neighbors
@@ -0,0 +1,79 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Constants and mappings for SONiC configuration."""
4
+
5
+ # Default AS prefix for local ASN calculation
6
+ DEFAULT_LOCAL_AS_PREFIX = 4200
7
+
8
+ # Default SONiC device roles
9
+ DEFAULT_SONIC_ROLES = [
10
+ "accessleaf",
11
+ "borderleaf",
12
+ "computeleaf",
13
+ "dataleaf",
14
+ "leaf",
15
+ "serviceleaf",
16
+ "spine",
17
+ "storageleaf",
18
+ "superspine",
19
+ "switch",
20
+ "transferleaf",
21
+ ]
22
+
23
+ # Default SONiC version
24
+ DEFAULT_SONIC_VERSION = "4.5.0"
25
+
26
+ # Port type to speed mapping (in Mbps)
27
+ PORT_TYPE_TO_SPEED_MAP = {
28
+ # RJ45/BASE-T Types
29
+ "100base-tx": 100, # 100Mbps RJ45
30
+ "1000base-t": 1000, # 1G RJ45
31
+ "2.5gbase-t": 2500, # 2.5G RJ45
32
+ "5gbase-t": 5000, # 5G RJ45
33
+ "10gbase-t": 10000, # 10G RJ45
34
+ # CX4
35
+ "10gbase-cx4": 10000, # 10G CX4
36
+ # 1G Optical
37
+ "1000base-x-gbic": 1000, # 1G GBIC
38
+ "1000base-x-sfp": 1000, # 1G SFP
39
+ # 10G Optical
40
+ "10gbase-x-sfpp": 10000, # 10G SFP+
41
+ "10gbase-x-xfp": 10000, # 10G XFP
42
+ "10gbase-x-xenpak": 10000, # 10G XENPAK
43
+ "10gbase-x-x2": 10000, # 10G X2
44
+ # 25G Optical
45
+ "25gbase-x-sfp28": 25000, # 25G SFP28
46
+ # 40G Optical
47
+ "40gbase-x-qsfpp": 40000, # 40G QSFP+
48
+ # 50G Optical
49
+ "50gbase-x-sfp28": 50000, # 50G SFP28
50
+ # 100G Optical
51
+ "100gbase-x-cfp": 100000, # 100G CFP
52
+ "100gbase-x-cfp2": 100000, # 100G CFP2
53
+ "100gbase-x-cfp4": 100000, # 100G CFP4
54
+ "100gbase-x-cpak": 100000, # 100G CPAK
55
+ "100gbase-x-qsfp28": 100000, # 100G QSFP28
56
+ # 200G Optical
57
+ "200gbase-x-cfp2": 200000, # 200G CFP2
58
+ "200gbase-x-qsfp56": 200000, # 200G QSFP56
59
+ # 400G Optical
60
+ "400gbase-x-qsfpdd": 400000, # 400G QSFP-DD
61
+ "400gbase-x-osfp": 400000, # 400G OSFP
62
+ # Virtual interface
63
+ "virtual": 0, # Virtual interface (no physical speed)
64
+ }
65
+
66
+ # High speed ports that use 4x multiplier (lanes)
67
+ HIGH_SPEED_PORTS = {100000, 200000, 400000, 800000} # 100G, 200G, 400G, 800G in Mbps
68
+
69
+ # Path to SONiC port configuration files
70
+ PORT_CONFIG_PATH = "/etc/sonic/port_config"
71
+
72
+ # List of supported HWSKUs
73
+ SUPPORTED_HWSKUS = [
74
+ "Accton-AS5835-54T",
75
+ "Accton-AS5835-54X",
76
+ "Accton-AS7326-56X",
77
+ "Accton-AS7726-32X",
78
+ "Accton-AS9716-32D",
79
+ ]
@@ -0,0 +1,82 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """Device-related helper functions for SONiC configuration."""
4
+
5
+ from loguru import logger
6
+
7
+ from osism import utils
8
+
9
+
10
+ def get_device_platform(device, hwsku):
11
+ """Get platform for device from sonic_parameters or generate from HWSKU.
12
+
13
+ Args:
14
+ device: NetBox device object
15
+ hwsku: Hardware SKU name
16
+
17
+ Returns:
18
+ str: Platform string (e.g., 'x86_64-accton_as7326_56x-r0')
19
+ """
20
+ platform = None
21
+ if (
22
+ hasattr(device, "custom_fields")
23
+ and "sonic_parameters" in device.custom_fields
24
+ and device.custom_fields["sonic_parameters"]
25
+ and "platform" in device.custom_fields["sonic_parameters"]
26
+ ):
27
+ platform = device.custom_fields["sonic_parameters"]["platform"]
28
+
29
+ if not platform:
30
+ # Generate platform from hwsku: x86_64-{hwsku_lower_with_underscores}-r0
31
+ hwsku_formatted = hwsku.lower().replace("-", "_")
32
+ platform = f"x86_64-{hwsku_formatted}-r0"
33
+
34
+ return platform
35
+
36
+
37
+ def get_device_hostname(device):
38
+ """Get hostname for device from inventory_hostname custom field or device name.
39
+
40
+ Args:
41
+ device: NetBox device object
42
+
43
+ Returns:
44
+ str: Hostname for the device
45
+ """
46
+ hostname = device.name
47
+ if (
48
+ hasattr(device, "custom_fields")
49
+ and "inventory_hostname" in device.custom_fields
50
+ and device.custom_fields["inventory_hostname"]
51
+ ):
52
+ hostname = device.custom_fields["inventory_hostname"]
53
+
54
+ return hostname
55
+
56
+
57
+ def get_device_mac_address(device):
58
+ """Get MAC address from device's management interface.
59
+
60
+ Args:
61
+ device: NetBox device object
62
+
63
+ Returns:
64
+ str: MAC address or default '00:00:00:00:00:00'
65
+ """
66
+ mac_address = "00:00:00:00:00:00" # Default MAC
67
+ try:
68
+ # Get all interfaces for the device
69
+ interfaces = utils.nb.dcim.interfaces.filter(device_id=device.id)
70
+ for interface in interfaces:
71
+ # Check if interface is marked as management only
72
+ if interface.mgmt_only:
73
+ if interface.mac_address:
74
+ mac_address = interface.mac_address
75
+ logger.debug(
76
+ f"Using MAC address {mac_address} from management interface {interface.name}"
77
+ )
78
+ break
79
+ except Exception as e:
80
+ logger.warning(f"Could not get MAC address for device {device.name}: {e}")
81
+
82
+ return mac_address