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.
- osism/commands/baremetal.py +48 -7
- 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/wait.py +8 -31
- osism/services/listener.py +1 -1
- osism/settings.py +12 -2
- osism/tasks/__init__.py +8 -40
- osism/tasks/conductor/__init__.py +8 -1
- osism/tasks/conductor/ironic.py +86 -66
- osism/tasks/conductor/netbox.py +267 -6
- osism/tasks/conductor/sonic.py +1401 -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.20250616.0.dist-info}/METADATA +3 -3
- {osism-0.20250605.0.dist-info → osism-0.20250616.0.dist-info}/RECORD +25 -24
- {osism-0.20250605.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.20250605.0.dist-info/licenses/AUTHORS +0 -1
- osism-0.20250605.0.dist-info/pbr.json +0 -1
- {osism-0.20250605.0.dist-info → osism-0.20250616.0.dist-info}/WHEEL +0 -0
- {osism-0.20250605.0.dist-info → osism-0.20250616.0.dist-info}/licenses/LICENSE +0 -0
- {osism-0.20250605.0.dist-info → osism-0.20250616.0.dist-info}/top_level.txt +0 -0
osism/tasks/conductor/ironic.py
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
# SPDX-License-Identifier: Apache-2.0
|
2
2
|
|
3
|
-
import ipaddress
|
4
3
|
import json
|
5
4
|
|
6
5
|
import jinja2
|
7
|
-
from loguru import logger
|
8
6
|
from pottery import Redlock
|
9
7
|
|
10
8
|
from osism import utils as osism_utils
|
11
9
|
from osism.tasks import netbox, openstack
|
12
|
-
from osism.tasks.conductor.netbox import
|
10
|
+
from osism.tasks.conductor.netbox import (
|
11
|
+
get_device_oob_ip,
|
12
|
+
get_nb_device_query_list_ironic,
|
13
|
+
)
|
13
14
|
from osism.tasks.conductor.utils import (
|
14
15
|
deep_compare,
|
15
16
|
deep_decrypt,
|
@@ -33,25 +34,32 @@ driver_params = {
|
|
33
34
|
}
|
34
35
|
|
35
36
|
|
36
|
-
def sync_ironic(get_ironic_parameters, force_update=False):
|
37
|
+
def sync_ironic(request_id, get_ironic_parameters, force_update=False):
|
38
|
+
osism_utils.push_task_output(
|
39
|
+
request_id,
|
40
|
+
"Starting NetBox device synchronisation with ironic\n",
|
41
|
+
)
|
37
42
|
devices = set()
|
38
|
-
nb_device_query_list =
|
43
|
+
nb_device_query_list = get_nb_device_query_list_ironic()
|
39
44
|
for nb_device_query in nb_device_query_list:
|
40
45
|
devices |= set(netbox.get_devices(**nb_device_query))
|
41
46
|
|
42
|
-
# NOTE: Find nodes in Ironic which are no longer present in
|
47
|
+
# NOTE: Find nodes in Ironic which are no longer present in NetBox and remove them
|
43
48
|
device_names = {dev.name for dev in devices}
|
44
49
|
nodes = openstack.baremetal_node_list()
|
45
50
|
for node in nodes:
|
46
|
-
|
51
|
+
osism_utils.push_task_output(
|
52
|
+
request_id, f"Looking for {node['Name']} in NetBox\n"
|
53
|
+
)
|
47
54
|
if node["Name"] not in device_names:
|
48
55
|
if (
|
49
56
|
not node["Instance UUID"]
|
50
57
|
and node["Provisioning State"] in ["enroll", "manageable", "available"]
|
51
58
|
and node["Power State"] in ["power off", None]
|
52
59
|
):
|
53
|
-
|
54
|
-
|
60
|
+
osism_utils.push_task_output(
|
61
|
+
request_id,
|
62
|
+
f"Cleaning up baremetal node not found in NetBox: {node['Name']}\n",
|
55
63
|
)
|
56
64
|
for port in openstack.baremetal_port_list(
|
57
65
|
details=False, attributes=dict(node_uuid=node["UUID"])
|
@@ -59,14 +67,15 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
59
67
|
openstack.baremetal_port_delete(port.id)
|
60
68
|
openstack.baremetal_node_delete(node["UUID"])
|
61
69
|
else:
|
62
|
-
|
70
|
+
osism_utils.push_task_output(
|
63
71
|
f"Cannot remove baremetal node because it is still provisioned or running: {node}"
|
64
72
|
)
|
65
73
|
|
66
|
-
# NOTE: Find nodes in
|
74
|
+
# NOTE: Find nodes in NetBox which are not present in Ironic and add them
|
67
75
|
for device in devices:
|
68
|
-
|
69
|
-
|
76
|
+
osism_utils.push_task_output(
|
77
|
+
request_id, f"Looking for {device.name} in ironic\n"
|
78
|
+
)
|
70
79
|
|
71
80
|
node_interfaces = list(netbox.get_interfaces_by_device(device.name))
|
72
81
|
|
@@ -75,7 +84,7 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
75
84
|
"ironic_parameters" in device.custom_fields
|
76
85
|
and device.custom_fields["ironic_parameters"]
|
77
86
|
):
|
78
|
-
# NOTE: Update node attributes with overrides from
|
87
|
+
# NOTE: Update node attributes with overrides from NetBox device
|
79
88
|
deep_merge(node_attributes, device.custom_fields["ironic_parameters"])
|
80
89
|
|
81
90
|
# NOTE: Decrypt ansible vaulted secrets
|
@@ -132,32 +141,19 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
132
141
|
# NOTE: Render driver address field
|
133
142
|
address_key = driver_params[node_attributes["driver"]]["address"]
|
134
143
|
if address_key in node_attributes["driver_info"]:
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
for interface in node_interfaces
|
141
|
-
if interface.mgmt_only
|
142
|
-
and "address" in interface
|
143
|
-
and interface["address"]
|
144
|
-
]
|
145
|
-
if len(node_mgmt_addresses) > 0:
|
146
|
-
node_mgmt_address = node_mgmt_addresses[0]
|
147
|
-
else:
|
148
|
-
node_mgmt_address = None
|
149
|
-
if node_mgmt_address:
|
144
|
+
oob_ip_result = get_device_oob_ip(device)
|
145
|
+
if oob_ip_result:
|
146
|
+
oob_ip, _ = (
|
147
|
+
oob_ip_result # Extract IP address, ignore prefix length
|
148
|
+
)
|
150
149
|
node_attributes["driver_info"][address_key] = (
|
151
150
|
jinja2.Environment(loader=jinja2.BaseLoader())
|
152
151
|
.from_string(node_attributes["driver_info"][address_key])
|
153
|
-
.render(
|
154
|
-
remote_board_address=str(
|
155
|
-
ipaddress.ip_interface(node_mgmt_address).ip
|
156
|
-
)
|
157
|
-
)
|
152
|
+
.render(remote_board_address=oob_ip)
|
158
153
|
)
|
159
154
|
node_attributes.update({"resource_class": device.name})
|
160
|
-
# NOTE: Write metadata used for provisioning into 'extra' field, so that
|
155
|
+
# NOTE: Write metadata used for provisioning into 'extra' field, so that
|
156
|
+
# it is available during node deploy without querying the NetBox again
|
161
157
|
if "extra" not in node_attributes:
|
162
158
|
node_attributes["extra"] = {}
|
163
159
|
if (
|
@@ -191,10 +187,14 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
191
187
|
)
|
192
188
|
if lock.acquire(timeout=120):
|
193
189
|
try:
|
194
|
-
|
190
|
+
osism_utils.push_task_output(
|
191
|
+
request_id, f"Processing device {device.name}\n"
|
192
|
+
)
|
195
193
|
node = openstack.baremetal_node_show(device.name, ignore_missing=True)
|
196
194
|
if not node:
|
197
|
-
|
195
|
+
osism_utils.push_task_output(
|
196
|
+
request_id, f"Creating baremetal node for {device.name}\n"
|
197
|
+
)
|
198
198
|
node = openstack.baremetal_node_create(device.name, node_attributes)
|
199
199
|
else:
|
200
200
|
# NOTE: The listener service only reacts to changes in the baremetal node. Explicitly sync provision and power state in case updates were missed by the listener.
|
@@ -218,8 +218,9 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
218
218
|
if not node_updates["driver_info"]:
|
219
219
|
node_updates.pop("driver_info", None)
|
220
220
|
if node_updates or force_update:
|
221
|
-
|
222
|
-
|
221
|
+
osism_utils.push_task_output(
|
222
|
+
request_id,
|
223
|
+
f"Updating baremetal node for {device.name} with {node_updates}\n",
|
223
224
|
)
|
224
225
|
# NOTE: Do the actual updates with all values in node_attributes. Otherwise nested dicts like e.g. driver_info will be overwritten as a whole and contain only changed values
|
225
226
|
node = openstack.baremetal_node_update(
|
@@ -240,27 +241,31 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
240
241
|
== port["address"].upper()
|
241
242
|
]
|
242
243
|
if not port:
|
243
|
-
|
244
|
-
|
244
|
+
osism_utils.push_task_output(
|
245
|
+
request_id,
|
246
|
+
f"Creating baremetal port with MAC address {port_attributes['address']} for {device.name}\n",
|
245
247
|
)
|
246
248
|
openstack.baremetal_port_create(port_attributes)
|
247
249
|
else:
|
248
250
|
node_ports.remove(port[0])
|
249
251
|
for node_port in node_ports:
|
250
|
-
# NOTE: Delete remaining ports not found in
|
251
|
-
|
252
|
-
|
252
|
+
# NOTE: Delete remaining ports not found in NetBox
|
253
|
+
osism_utils.push_task_output(
|
254
|
+
request_id,
|
255
|
+
f"Deleting baremetal port with MAC address {node_port['address']} for {device.name}\n",
|
253
256
|
)
|
254
257
|
openstack.baremetal_port_delete(node_port["id"])
|
255
258
|
|
256
259
|
node_validation = openstack.baremetal_node_validate(node["uuid"])
|
257
260
|
if node_validation["management"].result:
|
258
|
-
|
259
|
-
|
261
|
+
osism_utils.push_task_output(
|
262
|
+
request_id,
|
263
|
+
f"Validation of management interface successful for baremetal node for {device.name}\n",
|
260
264
|
)
|
261
265
|
if node["provision_state"] == "enroll":
|
262
|
-
|
263
|
-
|
266
|
+
osism_utils.push_task_output(
|
267
|
+
request_id,
|
268
|
+
f"Transitioning baremetal node to manageable state for {device.name}\n",
|
264
269
|
)
|
265
270
|
node = openstack.baremetal_node_set_provision_state(
|
266
271
|
node["uuid"], "manage"
|
@@ -268,14 +273,19 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
268
273
|
node = openstack.baremetal_node_wait_for_nodes_provision_state(
|
269
274
|
node["uuid"], "manageable"
|
270
275
|
)
|
271
|
-
|
276
|
+
osism_utils.push_task_output(
|
277
|
+
request_id,
|
278
|
+
f"Baremetal node for {device.name} is manageable\n",
|
279
|
+
)
|
272
280
|
if node_validation["boot"].result:
|
273
|
-
|
274
|
-
|
281
|
+
osism_utils.push_task_output(
|
282
|
+
request_id,
|
283
|
+
f"Validation of boot interface successful for baremetal node for {device.name}\n",
|
275
284
|
)
|
276
285
|
if node["provision_state"] == "manageable":
|
277
|
-
|
278
|
-
|
286
|
+
osism_utils.push_task_output(
|
287
|
+
request_id,
|
288
|
+
f"Transitioning baremetal node to available state for {device.name}\n",
|
279
289
|
)
|
280
290
|
node = openstack.baremetal_node_set_provision_state(
|
281
291
|
node["uuid"], "provide"
|
@@ -285,17 +295,20 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
285
295
|
node["uuid"], "available"
|
286
296
|
)
|
287
297
|
)
|
288
|
-
|
289
|
-
|
298
|
+
osism_utils.push_task_output(
|
299
|
+
request_id,
|
300
|
+
f"Baremetal node for {device.name} is available\n",
|
290
301
|
)
|
291
302
|
else:
|
292
|
-
|
293
|
-
|
303
|
+
osism_utils.push_task_output(
|
304
|
+
request_id,
|
305
|
+
f"Validation of boot interface failed for baremetal node for {device.name}\nReason: {node_validation['boot'].reason}\n",
|
294
306
|
)
|
295
307
|
if node["provision_state"] == "available":
|
296
308
|
# NOTE: Demote node to manageable
|
297
|
-
|
298
|
-
|
309
|
+
osism_utils.push_task_output(
|
310
|
+
request_id,
|
311
|
+
f"Transitioning baremetal node to manageable state for {device.name}\n",
|
299
312
|
)
|
300
313
|
node = openstack.baremetal_node_set_provision_state(
|
301
314
|
node["uuid"], "manage"
|
@@ -305,19 +318,26 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
305
318
|
node["uuid"], "manageable"
|
306
319
|
)
|
307
320
|
)
|
308
|
-
|
309
|
-
|
321
|
+
osism_utils.push_task_output(
|
322
|
+
request_id,
|
323
|
+
f"Baremetal node for {device.name} is manageable\n",
|
310
324
|
)
|
311
325
|
else:
|
312
|
-
|
313
|
-
|
326
|
+
osism_utils.push_task_output(
|
327
|
+
request_id,
|
328
|
+
f"Validation of management interface failed for baremetal node for {device.name}\nReason: {node_validation['management'].reason}\n",
|
314
329
|
)
|
315
330
|
except Exception as exc:
|
316
|
-
|
317
|
-
|
331
|
+
osism_utils.push_task_output(
|
332
|
+
request_id,
|
333
|
+
f"Could not fully synchronize device {device.name} with ironic: {exc}\n",
|
318
334
|
)
|
319
335
|
finally:
|
320
336
|
lock.release()
|
321
337
|
|
322
338
|
else:
|
323
|
-
|
339
|
+
osism_utils.push_task_output(
|
340
|
+
"Could not acquire lock for node {device.name}"
|
341
|
+
)
|
342
|
+
|
343
|
+
osism_utils.finish_task_output(request_id, rc=0)
|
osism/tasks/conductor/netbox.py
CHANGED
@@ -3,11 +3,11 @@
|
|
3
3
|
from loguru import logger
|
4
4
|
import yaml
|
5
5
|
|
6
|
-
from osism import settings
|
6
|
+
from osism import settings, utils
|
7
7
|
from osism.tasks import netbox
|
8
8
|
|
9
9
|
|
10
|
-
def
|
10
|
+
def get_nb_device_query_list_ironic():
|
11
11
|
try:
|
12
12
|
supported_nb_device_filters = [
|
13
13
|
"site",
|
@@ -18,7 +18,7 @@ def get_nb_device_query_list():
|
|
18
18
|
"tag",
|
19
19
|
"state",
|
20
20
|
]
|
21
|
-
nb_device_query_list = yaml.safe_load(settings.
|
21
|
+
nb_device_query_list = yaml.safe_load(settings.NETBOX_FILTER_CONDUCTOR_IRONIC)
|
22
22
|
if type(nb_device_query_list) is not list:
|
23
23
|
raise TypeError
|
24
24
|
for nb_device_query in nb_device_query_list:
|
@@ -27,7 +27,7 @@ def get_nb_device_query_list():
|
|
27
27
|
for key in list(nb_device_query.keys()):
|
28
28
|
if key not in supported_nb_device_filters:
|
29
29
|
raise ValueError
|
30
|
-
# NOTE: Only "location_id" and "rack_id" are supported by
|
30
|
+
# NOTE: Only "location_id" and "rack_id" are supported by NetBox
|
31
31
|
if key in ["location", "rack"]:
|
32
32
|
value_name = nb_device_query.pop(key, "")
|
33
33
|
if key == "location":
|
@@ -40,11 +40,272 @@ def get_nb_device_query_list():
|
|
40
40
|
raise ValueError(f"Invalid name {value_name} for {key}")
|
41
41
|
except (yaml.YAMLError, TypeError):
|
42
42
|
logger.error(
|
43
|
-
f"Setting
|
43
|
+
f"Setting NETBOX_FILTER_CONDUCTOR_IRONIC needs to be an array of mappings containing supported NetBox device filters: {supported_nb_device_filters}"
|
44
44
|
)
|
45
45
|
nb_device_query_list = []
|
46
46
|
except ValueError as exc:
|
47
|
-
logger.error(f"Unknown value in
|
47
|
+
logger.error(f"Unknown value in NETBOX_FILTER_CONDUCTOR_IRONIC: {exc}")
|
48
48
|
nb_device_query_list = []
|
49
49
|
|
50
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}
|