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
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,34 +141,25 @@ 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 it is available during node deploy without querying the netbox again
|
161
155
|
if "extra" not in node_attributes:
|
162
156
|
node_attributes["extra"] = {}
|
157
|
+
# NOTE: Copy instance_info into extra field. because ironic removes it on undeployment. This way it may be readded on undeploy without querying the netbox again
|
158
|
+
if "instance_info" in node_attributes and node_attributes["instance_info"]:
|
159
|
+
node_attributes["extra"].update(
|
160
|
+
{"instance_info": json.dumps(node_attributes["instance_info"])}
|
161
|
+
)
|
162
|
+
# NOTE: Write metadata used for provisioning into 'extra' field, so that it is available during node deploy without querying the netbox again
|
163
163
|
if (
|
164
164
|
"netplan_parameters" in device.custom_fields
|
165
165
|
and device.custom_fields["netplan_parameters"]
|
@@ -191,10 +191,14 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
191
191
|
)
|
192
192
|
if lock.acquire(timeout=120):
|
193
193
|
try:
|
194
|
-
|
194
|
+
osism_utils.push_task_output(
|
195
|
+
request_id, f"Processing device {device.name}\n"
|
196
|
+
)
|
195
197
|
node = openstack.baremetal_node_show(device.name, ignore_missing=True)
|
196
198
|
if not node:
|
197
|
-
|
199
|
+
osism_utils.push_task_output(
|
200
|
+
request_id, f"Creating baremetal node for {device.name}\n"
|
201
|
+
)
|
198
202
|
node = openstack.baremetal_node_create(device.name, node_attributes)
|
199
203
|
else:
|
200
204
|
# 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 +222,9 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
218
222
|
if not node_updates["driver_info"]:
|
219
223
|
node_updates.pop("driver_info", None)
|
220
224
|
if node_updates or force_update:
|
221
|
-
|
222
|
-
|
225
|
+
osism_utils.push_task_output(
|
226
|
+
request_id,
|
227
|
+
f"Updating baremetal node for {device.name} with {node_updates}\n",
|
223
228
|
)
|
224
229
|
# 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
230
|
node = openstack.baremetal_node_update(
|
@@ -240,27 +245,31 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
240
245
|
== port["address"].upper()
|
241
246
|
]
|
242
247
|
if not port:
|
243
|
-
|
244
|
-
|
248
|
+
osism_utils.push_task_output(
|
249
|
+
request_id,
|
250
|
+
f"Creating baremetal port with MAC address {port_attributes['address']} for {device.name}\n",
|
245
251
|
)
|
246
252
|
openstack.baremetal_port_create(port_attributes)
|
247
253
|
else:
|
248
254
|
node_ports.remove(port[0])
|
249
255
|
for node_port in node_ports:
|
250
|
-
# NOTE: Delete remaining ports not found in
|
251
|
-
|
252
|
-
|
256
|
+
# NOTE: Delete remaining ports not found in NetBox
|
257
|
+
osism_utils.push_task_output(
|
258
|
+
request_id,
|
259
|
+
f"Deleting baremetal port with MAC address {node_port['address']} for {device.name}\n",
|
253
260
|
)
|
254
261
|
openstack.baremetal_port_delete(node_port["id"])
|
255
262
|
|
256
263
|
node_validation = openstack.baremetal_node_validate(node["uuid"])
|
257
264
|
if node_validation["management"].result:
|
258
|
-
|
259
|
-
|
265
|
+
osism_utils.push_task_output(
|
266
|
+
request_id,
|
267
|
+
f"Validation of management interface successful for baremetal node for {device.name}\n",
|
260
268
|
)
|
261
269
|
if node["provision_state"] == "enroll":
|
262
|
-
|
263
|
-
|
270
|
+
osism_utils.push_task_output(
|
271
|
+
request_id,
|
272
|
+
f"Transitioning baremetal node to manageable state for {device.name}\n",
|
264
273
|
)
|
265
274
|
node = openstack.baremetal_node_set_provision_state(
|
266
275
|
node["uuid"], "manage"
|
@@ -268,14 +277,19 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
268
277
|
node = openstack.baremetal_node_wait_for_nodes_provision_state(
|
269
278
|
node["uuid"], "manageable"
|
270
279
|
)
|
271
|
-
|
280
|
+
osism_utils.push_task_output(
|
281
|
+
request_id,
|
282
|
+
f"Baremetal node for {device.name} is manageable\n",
|
283
|
+
)
|
272
284
|
if node_validation["boot"].result:
|
273
|
-
|
274
|
-
|
285
|
+
osism_utils.push_task_output(
|
286
|
+
request_id,
|
287
|
+
f"Validation of boot interface successful for baremetal node for {device.name}\n",
|
275
288
|
)
|
276
289
|
if node["provision_state"] == "manageable":
|
277
|
-
|
278
|
-
|
290
|
+
osism_utils.push_task_output(
|
291
|
+
request_id,
|
292
|
+
f"Transitioning baremetal node to available state for {device.name}\n",
|
279
293
|
)
|
280
294
|
node = openstack.baremetal_node_set_provision_state(
|
281
295
|
node["uuid"], "provide"
|
@@ -285,17 +299,20 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
285
299
|
node["uuid"], "available"
|
286
300
|
)
|
287
301
|
)
|
288
|
-
|
289
|
-
|
302
|
+
osism_utils.push_task_output(
|
303
|
+
request_id,
|
304
|
+
f"Baremetal node for {device.name} is available\n",
|
290
305
|
)
|
291
306
|
else:
|
292
|
-
|
293
|
-
|
307
|
+
osism_utils.push_task_output(
|
308
|
+
request_id,
|
309
|
+
f"Validation of boot interface failed for baremetal node for {device.name}\nReason: {node_validation['boot'].reason}\n",
|
294
310
|
)
|
295
311
|
if node["provision_state"] == "available":
|
296
312
|
# NOTE: Demote node to manageable
|
297
|
-
|
298
|
-
|
313
|
+
osism_utils.push_task_output(
|
314
|
+
request_id,
|
315
|
+
f"Transitioning baremetal node to manageable state for {device.name}\n",
|
299
316
|
)
|
300
317
|
node = openstack.baremetal_node_set_provision_state(
|
301
318
|
node["uuid"], "manage"
|
@@ -305,19 +322,26 @@ def sync_ironic(get_ironic_parameters, force_update=False):
|
|
305
322
|
node["uuid"], "manageable"
|
306
323
|
)
|
307
324
|
)
|
308
|
-
|
309
|
-
|
325
|
+
osism_utils.push_task_output(
|
326
|
+
request_id,
|
327
|
+
f"Baremetal node for {device.name} is manageable\n",
|
310
328
|
)
|
311
329
|
else:
|
312
|
-
|
313
|
-
|
330
|
+
osism_utils.push_task_output(
|
331
|
+
request_id,
|
332
|
+
f"Validation of management interface failed for baremetal node for {device.name}\nReason: {node_validation['management'].reason}\n",
|
314
333
|
)
|
315
334
|
except Exception as exc:
|
316
|
-
|
317
|
-
|
335
|
+
osism_utils.push_task_output(
|
336
|
+
request_id,
|
337
|
+
f"Could not fully synchronize device {device.name} with ironic: {exc}\n",
|
318
338
|
)
|
319
339
|
finally:
|
320
340
|
lock.release()
|
321
341
|
|
322
342
|
else:
|
323
|
-
|
343
|
+
osism_utils.push_task_output(
|
344
|
+
"Could not acquire lock for node {device.name}"
|
345
|
+
)
|
346
|
+
|
347
|
+
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}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
|
3
|
+
"""SONiC configuration management package."""
|
4
|
+
|
5
|
+
from .config_generator import generate_sonic_config
|
6
|
+
from .exporter import save_config_to_netbox, export_config_to_file
|
7
|
+
from .sync import sync_sonic
|
8
|
+
from .connections import (
|
9
|
+
get_connected_interfaces,
|
10
|
+
get_connected_device_for_sonic_interface,
|
11
|
+
get_connected_device_via_interface,
|
12
|
+
find_interconnected_devices,
|
13
|
+
get_device_bgp_neighbors_via_loopback,
|
14
|
+
)
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
"generate_sonic_config",
|
18
|
+
"save_config_to_netbox",
|
19
|
+
"export_config_to_file",
|
20
|
+
"sync_sonic",
|
21
|
+
"get_connected_interfaces",
|
22
|
+
"get_connected_device_for_sonic_interface",
|
23
|
+
"get_connected_device_via_interface",
|
24
|
+
"find_interconnected_devices",
|
25
|
+
"get_device_bgp_neighbors_via_loopback",
|
26
|
+
]
|