osism 0.20250605.0__py3-none-any.whl → 0.20250621.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- osism/commands/baremetal.py +71 -10
- osism/commands/manage.py +25 -1
- osism/commands/netbox.py +22 -5
- osism/commands/reconciler.py +6 -28
- osism/commands/sync.py +48 -1
- osism/commands/validate.py +7 -30
- osism/commands/wait.py +8 -31
- osism/services/listener.py +1 -1
- osism/settings.py +13 -2
- osism/tasks/__init__.py +8 -40
- osism/tasks/conductor/__init__.py +8 -1
- osism/tasks/conductor/ironic.py +90 -66
- osism/tasks/conductor/netbox.py +267 -6
- osism/tasks/conductor/sonic/__init__.py +26 -0
- osism/tasks/conductor/sonic/bgp.py +87 -0
- osism/tasks/conductor/sonic/cache.py +114 -0
- osism/tasks/conductor/sonic/config_generator.py +908 -0
- osism/tasks/conductor/sonic/connections.py +389 -0
- osism/tasks/conductor/sonic/constants.py +79 -0
- osism/tasks/conductor/sonic/device.py +82 -0
- osism/tasks/conductor/sonic/exporter.py +226 -0
- osism/tasks/conductor/sonic/interface.py +789 -0
- osism/tasks/conductor/sonic/sync.py +190 -0
- osism/tasks/conductor.py +2 -0
- osism/tasks/netbox.py +6 -4
- osism/tasks/reconciler.py +4 -5
- osism/utils/__init__.py +51 -4
- {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/METADATA +4 -4
- {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/RECORD +35 -25
- {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/entry_points.txt +5 -0
- osism-0.20250621.0.dist-info/pbr.json +1 -0
- osism-0.20250605.0.dist-info/pbr.json +0 -1
- {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/WHEEL +0 -0
- {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/licenses/AUTHORS +0 -0
- {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/licenses/LICENSE +0 -0
- {osism-0.20250605.0.dist-info → osism-0.20250621.0.dist-info}/top_level.txt +0 -0
@@ -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
|