osism 0.20250602.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.
- osism/commands/baremetal.py +84 -37
- osism/commands/netbox.py +22 -5
- osism/commands/reconciler.py +6 -28
- osism/commands/sync.py +28 -1
- osism/commands/validate.py +7 -30
- osism/commands/vault.py +9 -1
- osism/commands/wait.py +8 -31
- osism/core/enums.py +1 -0
- osism/services/listener.py +1 -1
- osism/settings.py +12 -2
- osism/tasks/__init__.py +8 -40
- osism/tasks/conductor/__init__.py +61 -0
- osism/tasks/conductor/config.py +92 -0
- osism/tasks/conductor/ironic.py +343 -0
- osism/tasks/conductor/netbox.py +311 -0
- osism/tasks/conductor/sonic.py +1401 -0
- osism/tasks/conductor/utils.py +79 -0
- osism/tasks/conductor.py +15 -470
- osism/tasks/netbox.py +6 -4
- osism/tasks/reconciler.py +4 -5
- osism/utils/__init__.py +51 -4
- {osism-0.20250602.0.dist-info → osism-0.20250616.0.dist-info}/METADATA +4 -4
- {osism-0.20250602.0.dist-info → osism-0.20250616.0.dist-info}/RECORD +29 -23
- {osism-0.20250602.0.dist-info → osism-0.20250616.0.dist-info}/entry_points.txt +1 -0
- osism-0.20250616.0.dist-info/licenses/AUTHORS +1 -0
- osism-0.20250616.0.dist-info/pbr.json +1 -0
- osism-0.20250602.0.dist-info/licenses/AUTHORS +0 -1
- osism-0.20250602.0.dist-info/pbr.json +0 -1
- {osism-0.20250602.0.dist-info → osism-0.20250616.0.dist-info}/WHEEL +0 -0
- {osism-0.20250602.0.dist-info → osism-0.20250616.0.dist-info}/licenses/LICENSE +0 -0
- {osism-0.20250602.0.dist-info → osism-0.20250616.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,311 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
|
3
|
+
from loguru import logger
|
4
|
+
import yaml
|
5
|
+
|
6
|
+
from osism import settings, utils
|
7
|
+
from osism.tasks import netbox
|
8
|
+
|
9
|
+
|
10
|
+
def get_nb_device_query_list_ironic():
|
11
|
+
try:
|
12
|
+
supported_nb_device_filters = [
|
13
|
+
"site",
|
14
|
+
"region",
|
15
|
+
"site_group",
|
16
|
+
"location",
|
17
|
+
"rack",
|
18
|
+
"tag",
|
19
|
+
"state",
|
20
|
+
]
|
21
|
+
nb_device_query_list = yaml.safe_load(settings.NETBOX_FILTER_CONDUCTOR_IRONIC)
|
22
|
+
if type(nb_device_query_list) is not list:
|
23
|
+
raise TypeError
|
24
|
+
for nb_device_query in nb_device_query_list:
|
25
|
+
if type(nb_device_query) is not dict:
|
26
|
+
raise TypeError
|
27
|
+
for key in list(nb_device_query.keys()):
|
28
|
+
if key not in supported_nb_device_filters:
|
29
|
+
raise ValueError
|
30
|
+
# NOTE: Only "location_id" and "rack_id" are supported by NetBox
|
31
|
+
if key in ["location", "rack"]:
|
32
|
+
value_name = nb_device_query.pop(key, "")
|
33
|
+
if key == "location":
|
34
|
+
value_id = netbox.get_location_id(value_name)
|
35
|
+
elif key == "rack":
|
36
|
+
value_id = netbox.get_rack_id(value_name)
|
37
|
+
if value_id:
|
38
|
+
nb_device_query.update({key + "_id": value_id})
|
39
|
+
else:
|
40
|
+
raise ValueError(f"Invalid name {value_name} for {key}")
|
41
|
+
except (yaml.YAMLError, TypeError):
|
42
|
+
logger.error(
|
43
|
+
f"Setting NETBOX_FILTER_CONDUCTOR_IRONIC needs to be an array of mappings containing supported NetBox device filters: {supported_nb_device_filters}"
|
44
|
+
)
|
45
|
+
nb_device_query_list = []
|
46
|
+
except ValueError as exc:
|
47
|
+
logger.error(f"Unknown value in NETBOX_FILTER_CONDUCTOR_IRONIC: {exc}")
|
48
|
+
nb_device_query_list = []
|
49
|
+
|
50
|
+
return nb_device_query_list
|
51
|
+
|
52
|
+
|
53
|
+
def get_nb_device_query_list_sonic():
|
54
|
+
try:
|
55
|
+
supported_nb_device_filters = [
|
56
|
+
"site",
|
57
|
+
"region",
|
58
|
+
"site_group",
|
59
|
+
"location",
|
60
|
+
"rack",
|
61
|
+
"tag",
|
62
|
+
"state",
|
63
|
+
]
|
64
|
+
nb_device_query_list = yaml.safe_load(settings.NETBOX_FILTER_CONDUCTOR_SONIC)
|
65
|
+
if type(nb_device_query_list) is not list:
|
66
|
+
raise TypeError
|
67
|
+
for nb_device_query in nb_device_query_list:
|
68
|
+
if type(nb_device_query) is not dict:
|
69
|
+
raise TypeError
|
70
|
+
for key in list(nb_device_query.keys()):
|
71
|
+
if key not in supported_nb_device_filters:
|
72
|
+
raise ValueError
|
73
|
+
# NOTE: Only "location_id" and "rack_id" are supported by NetBox
|
74
|
+
if key in ["location", "rack"]:
|
75
|
+
value_name = nb_device_query.pop(key, "")
|
76
|
+
if key == "location":
|
77
|
+
value_id = netbox.get_location_id(value_name)
|
78
|
+
elif key == "rack":
|
79
|
+
value_id = netbox.get_rack_id(value_name)
|
80
|
+
if value_id:
|
81
|
+
nb_device_query.update({key + "_id": value_id})
|
82
|
+
else:
|
83
|
+
raise ValueError(f"Invalid name {value_name} for {key}")
|
84
|
+
except (yaml.YAMLError, TypeError):
|
85
|
+
logger.error(
|
86
|
+
f"Setting NETBOX_FILTER_CONDUCTOR_SONIC needs to be an array of mappings containing supported NetBox device filters: {supported_nb_device_filters}"
|
87
|
+
)
|
88
|
+
nb_device_query_list = []
|
89
|
+
except ValueError as exc:
|
90
|
+
logger.error(f"Unknown value in NETBOX_FILTER_CONDUCTOR_SONIC: {exc}")
|
91
|
+
nb_device_query_list = []
|
92
|
+
|
93
|
+
return nb_device_query_list
|
94
|
+
|
95
|
+
|
96
|
+
def get_device_oob_ip(device):
|
97
|
+
"""Get out-of-band IP address for device management interface.
|
98
|
+
|
99
|
+
Args:
|
100
|
+
device: NetBox device object
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
tuple: (IP address, prefix length) for management interface or None
|
104
|
+
Example: ('192.168.1.10', 24)
|
105
|
+
"""
|
106
|
+
import ipaddress
|
107
|
+
|
108
|
+
try:
|
109
|
+
oob_ip_with_prefix = None
|
110
|
+
|
111
|
+
# First check if device has oob_ip field set
|
112
|
+
if hasattr(device, "oob_ip") and device.oob_ip:
|
113
|
+
oob_ip_with_prefix = device.oob_ip
|
114
|
+
else:
|
115
|
+
# Fall back to management interfaces
|
116
|
+
interfaces = utils.nb.dcim.interfaces.filter(device_id=device.id)
|
117
|
+
|
118
|
+
for interface in interfaces:
|
119
|
+
if interface.mgmt_only:
|
120
|
+
# Get IP addresses assigned to this interface
|
121
|
+
ip_addresses = utils.nb.ipam.ip_addresses.filter(
|
122
|
+
assigned_object_id=interface.id,
|
123
|
+
)
|
124
|
+
|
125
|
+
for ip_addr in ip_addresses:
|
126
|
+
if ip_addr.address:
|
127
|
+
oob_ip_with_prefix = ip_addr.address
|
128
|
+
break
|
129
|
+
if oob_ip_with_prefix:
|
130
|
+
break
|
131
|
+
|
132
|
+
if oob_ip_with_prefix:
|
133
|
+
# Parse the IP address with prefix (e.g., "192.168.1.10/24")
|
134
|
+
ip_interface = ipaddress.ip_interface(oob_ip_with_prefix)
|
135
|
+
ip_address = str(ip_interface.ip)
|
136
|
+
prefix_length = ip_interface.network.prefixlen
|
137
|
+
|
138
|
+
logger.debug(
|
139
|
+
f"Found OOB IP for device {device.name}: {ip_address}/{prefix_length}"
|
140
|
+
)
|
141
|
+
|
142
|
+
# Return tuple of (IP address, prefix length)
|
143
|
+
return (ip_address, prefix_length)
|
144
|
+
|
145
|
+
except Exception as e:
|
146
|
+
logger.warning(f"Could not get OOB IP for device {device.name}: {e}")
|
147
|
+
|
148
|
+
return None
|
149
|
+
|
150
|
+
|
151
|
+
def get_device_vlans(device):
|
152
|
+
"""Get VLANs configured on device interfaces.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
device: NetBox device object
|
156
|
+
|
157
|
+
Returns:
|
158
|
+
dict: Dictionary with VLAN information
|
159
|
+
{
|
160
|
+
'vlans': {vid: {'name': name, 'description': desc}},
|
161
|
+
'vlan_members': {vid: {'port_name': 'tagging_mode'}},
|
162
|
+
'vlan_interfaces': {vid: {'addresses': [ip_with_prefix, ...]}}
|
163
|
+
}
|
164
|
+
"""
|
165
|
+
vlans = {}
|
166
|
+
vlan_members = {}
|
167
|
+
vlan_interfaces = {}
|
168
|
+
|
169
|
+
try:
|
170
|
+
# Get all interfaces for the device and convert to list for multiple iterations
|
171
|
+
interfaces = list(utils.nb.dcim.interfaces.filter(device_id=device.id))
|
172
|
+
|
173
|
+
for interface in interfaces:
|
174
|
+
# Skip management interfaces and virtual interfaces
|
175
|
+
if interface.mgmt_only or (
|
176
|
+
hasattr(interface, "type")
|
177
|
+
and interface.type
|
178
|
+
and interface.type.value == "virtual"
|
179
|
+
):
|
180
|
+
continue
|
181
|
+
|
182
|
+
# Process untagged VLAN
|
183
|
+
if hasattr(interface, "untagged_vlan") and interface.untagged_vlan:
|
184
|
+
vlan = interface.untagged_vlan
|
185
|
+
vid = vlan.vid
|
186
|
+
|
187
|
+
# Add VLAN info if not already present
|
188
|
+
if vid not in vlans:
|
189
|
+
vlans[vid] = {
|
190
|
+
"name": vlan.name or f"Vlan{vid}",
|
191
|
+
"description": vlan.description or "",
|
192
|
+
}
|
193
|
+
|
194
|
+
# Add interface to VLAN members as untagged
|
195
|
+
if vid not in vlan_members:
|
196
|
+
vlan_members[vid] = {}
|
197
|
+
|
198
|
+
# Use original NetBox interface name - conversion will be done in sonic.py
|
199
|
+
vlan_members[vid][interface.name] = "untagged"
|
200
|
+
|
201
|
+
# Process tagged VLANs
|
202
|
+
if hasattr(interface, "tagged_vlans") and interface.tagged_vlans:
|
203
|
+
for vlan in interface.tagged_vlans:
|
204
|
+
vid = vlan.vid
|
205
|
+
|
206
|
+
# Add VLAN info if not already present
|
207
|
+
if vid not in vlans:
|
208
|
+
vlans[vid] = {
|
209
|
+
"name": vlan.name or f"Vlan{vid}",
|
210
|
+
"description": vlan.description or "",
|
211
|
+
}
|
212
|
+
|
213
|
+
# Add interface to VLAN members as tagged
|
214
|
+
if vid not in vlan_members:
|
215
|
+
vlan_members[vid] = {}
|
216
|
+
|
217
|
+
# Use original NetBox interface name - conversion will be done in sonic.py
|
218
|
+
vlan_members[vid][interface.name] = "tagged"
|
219
|
+
|
220
|
+
# Get VLAN interfaces (SVIs) - virtual interfaces with VLAN assignments
|
221
|
+
for interface in interfaces:
|
222
|
+
# Check if interface is virtual type and has VLAN assignment
|
223
|
+
if (
|
224
|
+
hasattr(interface, "type")
|
225
|
+
and interface.type
|
226
|
+
and interface.type.value == "virtual"
|
227
|
+
and interface.name.startswith("Vlan")
|
228
|
+
):
|
229
|
+
try:
|
230
|
+
vid = int(interface.name[4:])
|
231
|
+
# Get IP addresses for this VLAN interface
|
232
|
+
ip_addresses = utils.nb.ipam.ip_addresses.filter(
|
233
|
+
assigned_object_id=interface.id,
|
234
|
+
)
|
235
|
+
|
236
|
+
addresses = []
|
237
|
+
for ip_addr in ip_addresses:
|
238
|
+
if ip_addr.address:
|
239
|
+
addresses.append(ip_addr.address)
|
240
|
+
|
241
|
+
if addresses:
|
242
|
+
if vid not in vlan_interfaces:
|
243
|
+
vlan_interfaces[vid] = {}
|
244
|
+
# Store all IP addresses for this VLAN interface
|
245
|
+
vlan_interfaces[vid]["addresses"] = addresses
|
246
|
+
except (ValueError, IndexError):
|
247
|
+
# Skip if interface name doesn't follow Vlan<number> pattern
|
248
|
+
pass
|
249
|
+
|
250
|
+
except Exception as e:
|
251
|
+
logger.warning(f"Could not get VLANs for device {device.name}: {e}")
|
252
|
+
|
253
|
+
return {
|
254
|
+
"vlans": vlans,
|
255
|
+
"vlan_members": vlan_members,
|
256
|
+
"vlan_interfaces": vlan_interfaces,
|
257
|
+
}
|
258
|
+
|
259
|
+
|
260
|
+
def get_device_loopbacks(device):
|
261
|
+
"""Get Loopback interfaces configured on device.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
device: NetBox device object
|
265
|
+
|
266
|
+
Returns:
|
267
|
+
dict: Dictionary with Loopback information
|
268
|
+
{
|
269
|
+
'loopbacks': {'Loopback0': {'addresses': [ip_with_prefix, ...]}}
|
270
|
+
}
|
271
|
+
"""
|
272
|
+
loopbacks = {}
|
273
|
+
|
274
|
+
try:
|
275
|
+
# Get all interfaces for the device
|
276
|
+
interfaces = list(utils.nb.dcim.interfaces.filter(device_id=device.id))
|
277
|
+
|
278
|
+
for interface in interfaces:
|
279
|
+
# Check if interface is virtual type and is a Loopback interface
|
280
|
+
if (
|
281
|
+
hasattr(interface, "type")
|
282
|
+
and interface.type
|
283
|
+
and interface.type.value == "virtual"
|
284
|
+
and interface.name.startswith("Loopback")
|
285
|
+
):
|
286
|
+
|
287
|
+
try:
|
288
|
+
# Get IP addresses for this Loopback interface
|
289
|
+
ip_addresses = utils.nb.ipam.ip_addresses.filter(
|
290
|
+
assigned_object_id=interface.id,
|
291
|
+
)
|
292
|
+
|
293
|
+
addresses = []
|
294
|
+
for ip_addr in ip_addresses:
|
295
|
+
if ip_addr.address:
|
296
|
+
addresses.append(ip_addr.address)
|
297
|
+
|
298
|
+
if addresses:
|
299
|
+
loopbacks[interface.name] = {"addresses": addresses}
|
300
|
+
|
301
|
+
except Exception as e:
|
302
|
+
logger.debug(
|
303
|
+
f"Error processing Loopback interface {interface.name}: {e}"
|
304
|
+
)
|
305
|
+
|
306
|
+
except Exception as e:
|
307
|
+
logger.warning(
|
308
|
+
f"Could not get Loopback interfaces for device {device.name}: {e}"
|
309
|
+
)
|
310
|
+
|
311
|
+
return {"loopbacks": loopbacks}
|