osism 0.20250219.0__py3-none-any.whl → 0.20250314.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/actions/manage_device.py +28 -887
- osism/api.py +18 -0
- osism/commands/netbox.py +38 -327
- osism/services/listener.py +223 -133
- osism/settings.py +2 -3
- osism/tasks/__init__.py +44 -14
- osism/tasks/netbox.py +22 -98
- osism-0.20250314.0.dist-info/AUTHORS +1 -0
- {osism-0.20250219.0.dist-info → osism-0.20250314.0.dist-info}/METADATA +14 -14
- {osism-0.20250219.0.dist-info → osism-0.20250314.0.dist-info}/RECORD +15 -19
- {osism-0.20250219.0.dist-info → osism-0.20250314.0.dist-info}/WHEEL +1 -1
- {osism-0.20250219.0.dist-info → osism-0.20250314.0.dist-info}/entry_points.txt +1 -9
- osism-0.20250314.0.dist-info/pbr.json +1 -0
- osism/actions/check_configuration.py +0 -49
- osism/actions/deploy_configuration.py +0 -92
- osism/actions/diff_configuration.py +0 -59
- osism/actions/generate_configuration.py +0 -137
- osism-0.20250219.0.dist-info/AUTHORS +0 -1
- osism-0.20250219.0.dist-info/pbr.json +0 -1
- {osism-0.20250219.0.dist-info → osism-0.20250314.0.dist-info}/LICENSE +0 -0
- {osism-0.20250219.0.dist-info → osism-0.20250314.0.dist-info}/top_level.txt +0 -0
osism/actions/manage_device.py
CHANGED
@@ -1,74 +1,10 @@
|
|
1
1
|
# SPDX-License-Identifier: CC-BY-NC-4.0
|
2
2
|
# Copyright OSISM GmbH, 2022-2023
|
3
3
|
|
4
|
-
import glob
|
5
|
-
import os
|
6
|
-
|
7
4
|
from loguru import logger
|
8
5
|
from pottery import Redlock
|
9
|
-
import pynetbox
|
10
|
-
import yaml
|
11
6
|
|
12
7
|
from osism import utils
|
13
|
-
from osism.actions import generate_configuration, deploy_configuration
|
14
|
-
|
15
|
-
|
16
|
-
def load_data_from_filesystem(collection=None, device=None, state=None):
|
17
|
-
"""Loads all known data for a given device or an entire collection
|
18
|
-
from the file system (/netbox) in a given state.
|
19
|
-
"""
|
20
|
-
|
21
|
-
if not state or state in ["0", "None"]:
|
22
|
-
state = "a"
|
23
|
-
|
24
|
-
data = {}
|
25
|
-
if not device:
|
26
|
-
logger.info(f"Loading collection {collection}")
|
27
|
-
|
28
|
-
if os.path.isfile("/netbox/{CONF.collection}/{CONF.state}.yaml"):
|
29
|
-
with open(f"/netbox/{collection}/{state}.yaml") as fp:
|
30
|
-
data = yaml.load(fp, Loader=yaml.SafeLoader)
|
31
|
-
|
32
|
-
for directory in glob.glob(f"/netbox/{collection}/*/"):
|
33
|
-
with open(f"{directory}{state}.yaml") as fp:
|
34
|
-
data_a = yaml.load(fp, Loader=yaml.SafeLoader)
|
35
|
-
# data = data | data_a
|
36
|
-
data = {**data_a, **data}
|
37
|
-
|
38
|
-
elif device and collection:
|
39
|
-
if not os.path.isfile(
|
40
|
-
"/netbox/{CONF.collection}/{CONF.device}/{CONF.state}.yaml"
|
41
|
-
):
|
42
|
-
logger.error(
|
43
|
-
f"State {state} for device {device} in collection {collection} is not available"
|
44
|
-
)
|
45
|
-
return data
|
46
|
-
|
47
|
-
logger.info(f"Loading device {device} from collection {collection}")
|
48
|
-
|
49
|
-
with open(f"/netbox/{collection}/{device}/{state}.yaml") as fp:
|
50
|
-
data = yaml.load(fp, Loader=yaml.SafeLoader)
|
51
|
-
|
52
|
-
elif device:
|
53
|
-
# Try to find the collection of the specified device
|
54
|
-
# A device can be in exactly one collection
|
55
|
-
result = [x[0] for x in os.walk("/netbox") if device in x[0]]
|
56
|
-
if result:
|
57
|
-
logger.info(f"Loading device {device}")
|
58
|
-
|
59
|
-
try:
|
60
|
-
with open(f"{result[0]}/{state}.yaml") as fp:
|
61
|
-
data = yaml.load(fp, Loader=yaml.SafeLoader)
|
62
|
-
except: # noqa
|
63
|
-
logger.error(f"State {state} for device {device} is not available")
|
64
|
-
return data
|
65
|
-
else:
|
66
|
-
logger.error(f"Device {device} is not defined in any collection")
|
67
|
-
|
68
|
-
else:
|
69
|
-
logger.error("Specify at least a collection or a device")
|
70
|
-
|
71
|
-
return data
|
72
8
|
|
73
9
|
|
74
10
|
def get_state(device):
|
@@ -92,670 +28,6 @@ def get_states(devices):
|
|
92
28
|
return result
|
93
29
|
|
94
30
|
|
95
|
-
def get_transitions(devices):
|
96
|
-
"""Gets the transition (device_transition) stored in the Netbox for a list of devices."""
|
97
|
-
|
98
|
-
result = {}
|
99
|
-
for device in devices:
|
100
|
-
device_a = utils.nb.dcim.devices.get(name=device)
|
101
|
-
result[device] = device_a.custom_fields["device_transition"]
|
102
|
-
|
103
|
-
return result
|
104
|
-
|
105
|
-
|
106
|
-
def get_lag_interfaces(data):
|
107
|
-
"""Returns all defined LAGs."""
|
108
|
-
|
109
|
-
result = {}
|
110
|
-
for device in data:
|
111
|
-
result[device] = []
|
112
|
-
for interface in data[device]:
|
113
|
-
if data[device][interface]["type"] == "port-channel":
|
114
|
-
for interface in data[device][interface]["interfaces"]:
|
115
|
-
if interface not in result[device]:
|
116
|
-
result[device].append(interface)
|
117
|
-
|
118
|
-
return result
|
119
|
-
|
120
|
-
|
121
|
-
def manage_interfaces(device, data):
|
122
|
-
"""Manage interfaces."""
|
123
|
-
primary_address = None
|
124
|
-
lag_interfaces = get_lag_interfaces(data)
|
125
|
-
|
126
|
-
for interface in data[device]:
|
127
|
-
if data[device][interface]["type"] in ["virtual", "port-channel", "mlag"]:
|
128
|
-
continue
|
129
|
-
|
130
|
-
device_a = utils.nb.dcim.devices.get(name=device)
|
131
|
-
device_target = data[device][interface]["device"]
|
132
|
-
device_b = utils.nb.dcim.devices.get(name=device_target)
|
133
|
-
|
134
|
-
interface_a = utils.nb.dcim.interfaces.get(name=interface, device=device)
|
135
|
-
interface_b = utils.nb.dcim.interfaces.get(
|
136
|
-
name=data[device][interface]["interface"],
|
137
|
-
device=data[device][interface]["device"],
|
138
|
-
)
|
139
|
-
|
140
|
-
if not interface_a:
|
141
|
-
logger.error(f"{device} # {interface} --> not found")
|
142
|
-
|
143
|
-
if not interface_b:
|
144
|
-
logger.error(
|
145
|
-
f"{data[device][interface]['device']} # {data[device][interface]['interface']} --> not found"
|
146
|
-
)
|
147
|
-
|
148
|
-
# Ignore interfaces without an mac address
|
149
|
-
try:
|
150
|
-
if "mac_address" in data[device][interface]:
|
151
|
-
interface_a.mac_address = data[device][interface]["mac_address"]
|
152
|
-
interface_a.save()
|
153
|
-
elif interface_a.mac_address:
|
154
|
-
interface_a.mac_address = None
|
155
|
-
interface_a.save()
|
156
|
-
except: # noqa E722
|
157
|
-
pass
|
158
|
-
|
159
|
-
if interface_a and "data" in data[device][interface]:
|
160
|
-
interface_a.update(data[device][interface]["data"])
|
161
|
-
|
162
|
-
if "enabled" in data[device][interface]["data"] and interface_b:
|
163
|
-
interface_b.enabled = bool(data[device][interface]["data"]["enabled"])
|
164
|
-
|
165
|
-
# Add all addresses to the interface
|
166
|
-
if "addresses" in data[device][interface]:
|
167
|
-
for address in data[device][interface]["addresses"]:
|
168
|
-
address_a = utils.nb.ipam.ip_addresses.get(address=address)
|
169
|
-
if type(address) == str:
|
170
|
-
address_a = utils.nb.ipam.ip_addresses.get(address=address)
|
171
|
-
logger.info(f"Address {address} -> {interface}")
|
172
|
-
if not address_a:
|
173
|
-
utils.nb.ipam.ip_addresses.create(
|
174
|
-
address=address,
|
175
|
-
assigned_object_type="dcim.interface",
|
176
|
-
assigned_object_id=interface_a.id,
|
177
|
-
)
|
178
|
-
else:
|
179
|
-
address_a = utils.nb.ipam.ip_addresses.get(
|
180
|
-
address=address["address"]
|
181
|
-
)
|
182
|
-
logger.info(f"Address {address['address']} -> {interface}")
|
183
|
-
if not address_a:
|
184
|
-
address_a = utils.nb.ipam.ip_addresses.create(
|
185
|
-
assigned_object_type="dcim.interface",
|
186
|
-
assigned_object_id=interface_a.id,
|
187
|
-
**address,
|
188
|
-
)
|
189
|
-
if "primary" in address and bool(address["primary"]):
|
190
|
-
primary_address = address_a.id
|
191
|
-
device_a.primary_ip4 = address_a.id
|
192
|
-
device_a.save()
|
193
|
-
|
194
|
-
# Remove addresses from the interface that have been removed
|
195
|
-
for address in utils.nb.ipam.ip_addresses.filter(
|
196
|
-
device=device, interface=interface
|
197
|
-
):
|
198
|
-
delete = True
|
199
|
-
if "addresses" in data[device][interface]:
|
200
|
-
for address_a in data[device][interface]["addresses"]:
|
201
|
-
if type(address_a) == str and address_a == str(address):
|
202
|
-
delete = False
|
203
|
-
elif "address" in address_a and address_a["address"] == str(
|
204
|
-
address
|
205
|
-
):
|
206
|
-
delete = False
|
207
|
-
|
208
|
-
if delete:
|
209
|
-
address.delete()
|
210
|
-
|
211
|
-
logger.info(f"{interface_a} -> {device_target} # {interface_b}")
|
212
|
-
|
213
|
-
if interface_a.label:
|
214
|
-
port_a = interface_a.label
|
215
|
-
# EthernetXX/Y
|
216
|
-
elif "Ethernet" in interface_a.name and "/" in interface_a.name:
|
217
|
-
port_a = interface_a.name[8:].split("/")[0]
|
218
|
-
# EthernetXX
|
219
|
-
elif "Ethernet" in interface_a.name:
|
220
|
-
port_a = interface_a.name[8:]
|
221
|
-
# etherXX
|
222
|
-
elif "ether" in interface_a.name:
|
223
|
-
port_a = interface_a.name[5:]
|
224
|
-
# ethXX
|
225
|
-
elif "eth" in interface_a.name:
|
226
|
-
port_a = interface_a.name[3:]
|
227
|
-
# sfp-sfpplusXX
|
228
|
-
elif "sfp-sfpplus" in interface_a.name:
|
229
|
-
port_a = interface_a.name[11:]
|
230
|
-
# qsfp-qsfpplusXX
|
231
|
-
elif "qsfp-qsfpplus" in interface_a.name:
|
232
|
-
port_a = interface_a.name[13:]
|
233
|
-
# qsfpplusXX
|
234
|
-
elif "qsfpplus" in interface_a.name:
|
235
|
-
port_a = interface_a.name[8:]
|
236
|
-
else:
|
237
|
-
port_a = interface_a.name
|
238
|
-
|
239
|
-
if interface_b.label:
|
240
|
-
port_b = interface_b.label
|
241
|
-
# EthernetXX/Y
|
242
|
-
elif "Ethernet" in interface_b.name and "/" in interface_b.name:
|
243
|
-
port_b = interface_b.name[8:].split("/")[0]
|
244
|
-
# EthernetXX
|
245
|
-
elif "Ethernet" in interface_b.name:
|
246
|
-
port_b = interface_b.name[8:]
|
247
|
-
# etherXX
|
248
|
-
elif "ether" in interface_b.name:
|
249
|
-
port_b = interface_b.name[5:]
|
250
|
-
# ethXX
|
251
|
-
elif "eth" in interface_b.name:
|
252
|
-
port_b = interface_b.name[3:]
|
253
|
-
# sfp-sfpplusXX
|
254
|
-
elif "sfp-sfpplus" in interface_b.name:
|
255
|
-
port_b = interface_b.name[11:]
|
256
|
-
# qsfp-qsfpplusXX
|
257
|
-
elif "qsfp-qsfpplus" in interface_b.name:
|
258
|
-
port_b = interface_b.name[13:]
|
259
|
-
# qsfpplusXX
|
260
|
-
elif "qsfpplus" in interface_b.name:
|
261
|
-
port_b = interface_b.name[8:]
|
262
|
-
else:
|
263
|
-
port_b = interface_b.name
|
264
|
-
|
265
|
-
try:
|
266
|
-
position_a = int(device_a.position)
|
267
|
-
except:
|
268
|
-
# NOTE: dirty workaround so that it works for the moment also for nodes without
|
269
|
-
# a position in housings
|
270
|
-
position_a = 999
|
271
|
-
|
272
|
-
try:
|
273
|
-
position_b = int(device_b.position)
|
274
|
-
except:
|
275
|
-
# NOTE: dirty workaround so that it works for the moment also for nodes without
|
276
|
-
# a position in housings
|
277
|
-
position_b = 999
|
278
|
-
|
279
|
-
near_end_a = f"{position_a}:{port_a}"
|
280
|
-
if device_a.rack.name == device_b.rack.name:
|
281
|
-
far_end_a = f"{position_b}:{port_b}"
|
282
|
-
else:
|
283
|
-
far_end_a = f"{device_b.rack.name}-{position_b}:{port_b}"
|
284
|
-
label_a = f"{near_end_a} / {far_end_a}"
|
285
|
-
|
286
|
-
near_end_b = f"{position_b}:{port_b}"
|
287
|
-
if device_b.rack.name == device_a.rack.name:
|
288
|
-
far_end_b = f"{position_a}:{port_a}"
|
289
|
-
else:
|
290
|
-
far_end_b = f"{device_a.rack.name}-{position_a}:{port_a}"
|
291
|
-
label_b = f"{near_end_b} / {far_end_b}"
|
292
|
-
|
293
|
-
interface_a.update({"description": label_a})
|
294
|
-
interface_b.update({"description": label_b})
|
295
|
-
|
296
|
-
termination_a = {"object_type": "dcim.interface", "object_id": interface_a.id}
|
297
|
-
termination_b = {"object_type": "dcim.interface", "object_id": interface_b.id}
|
298
|
-
|
299
|
-
try:
|
300
|
-
connection = utils.nb.dcim.cables.create(
|
301
|
-
a_terminations=[termination_a],
|
302
|
-
b_terminations=[termination_b],
|
303
|
-
type=data[device][interface]["type"],
|
304
|
-
)
|
305
|
-
except pynetbox.core.query.RequestError as e:
|
306
|
-
# The "Duplicate termination found" error can be ignored
|
307
|
-
if "Duplicate termination found" not in e.error:
|
308
|
-
logger.error(f"ERROR --> {e.error}")
|
309
|
-
pass
|
310
|
-
|
311
|
-
# ensure that all interfaces are enabled that should be enabled
|
312
|
-
if not interface_a.enabled:
|
313
|
-
if "enabled" in data[device][interface]["data"]:
|
314
|
-
interface_a.enabled = bool(data[device][interface]["data"]["enabled"])
|
315
|
-
else:
|
316
|
-
interface_a.enabled = True
|
317
|
-
|
318
|
-
if interface_a.enabled:
|
319
|
-
logger.info(f"{device_a} # {interface_a} --> enabled")
|
320
|
-
else:
|
321
|
-
logger.info(f"{device_a} # {interface_a} --> disabled")
|
322
|
-
|
323
|
-
interface_a.save()
|
324
|
-
|
325
|
-
if not interface_b.enabled:
|
326
|
-
if (
|
327
|
-
"data" in data[device][interface]
|
328
|
-
and "enabled" in data[device][interface]["data"]
|
329
|
-
):
|
330
|
-
interface_b.enabled = bool(data[device][interface]["data"]["enabled"])
|
331
|
-
else:
|
332
|
-
interface_b.enabled = True
|
333
|
-
|
334
|
-
if interface_b.enabled:
|
335
|
-
logger.info(f"{device_b} # {interface_b} --> enabled")
|
336
|
-
else:
|
337
|
-
logger.info(f"{device_b} # {interface_b} --> disabled")
|
338
|
-
|
339
|
-
interface_b.save()
|
340
|
-
|
341
|
-
if "vlans" in data[device][interface]:
|
342
|
-
tagged = False
|
343
|
-
interface_a.untagged_vlan = None
|
344
|
-
interface_a.tagged_vlans = []
|
345
|
-
for vlan in data[device][interface]["vlans"]:
|
346
|
-
vlan_a = utils.nb.ipam.vlans.get(vid=vlan)
|
347
|
-
if not vlan_a:
|
348
|
-
try:
|
349
|
-
vlan_a = utils.nb.ipam.vlans.create(
|
350
|
-
name=f"VLAN {vlan}", vid=vlan
|
351
|
-
)
|
352
|
-
except pynetbox.core.query.RequestError as e:
|
353
|
-
logger.error(f"ERROR --> {e}")
|
354
|
-
pass
|
355
|
-
|
356
|
-
if data[device][interface]["vlans"][vlan] == "untagged":
|
357
|
-
logger.info(f"Untagged VLAN {vlan_a.vid} -> {interface_a.name}")
|
358
|
-
interface_a.untagged_vlan = vlan_a.id
|
359
|
-
|
360
|
-
if interface_a.name not in lag_interfaces[device]:
|
361
|
-
logger.info(f"Tagged VLAN {vlan_a.vid} -> {interface_b.name}")
|
362
|
-
interface_b.untagged_vlan = vlan_a.id
|
363
|
-
|
364
|
-
elif vlan_a.id not in interface_a.tagged_vlans:
|
365
|
-
logger.info(f"Tagged VLAN {vlan_a.vid} -> {interface_a.name}")
|
366
|
-
interface_a.tagged_vlans.append(vlan_a.id)
|
367
|
-
|
368
|
-
if interface_a.name not in lag_interfaces[device]:
|
369
|
-
logger.info(f"Tagged VLAN {vlan_a.vid} -> {interface_b.name}")
|
370
|
-
interface_b.tagged_vlans.append(vlan_a.id)
|
371
|
-
|
372
|
-
tagged = True
|
373
|
-
|
374
|
-
if tagged:
|
375
|
-
interface_a.mode = "tagged"
|
376
|
-
|
377
|
-
if interface_a.name not in lag_interfaces[device]:
|
378
|
-
interface_b.mode = "tagged"
|
379
|
-
else:
|
380
|
-
interface_a.mode = "access"
|
381
|
-
|
382
|
-
if interface_a.name not in lag_interfaces[device]:
|
383
|
-
interface_b.mode = "access"
|
384
|
-
|
385
|
-
interface_a.save()
|
386
|
-
|
387
|
-
if interface_a.name not in lag_interfaces[device]:
|
388
|
-
interface_b.save()
|
389
|
-
|
390
|
-
# Remove the primary IP address if it is no longer set
|
391
|
-
if not primary_address:
|
392
|
-
device_a.primary_ip4 = None
|
393
|
-
device_a.save()
|
394
|
-
|
395
|
-
|
396
|
-
def manage_port_channels(device, data):
|
397
|
-
"""Manage port channels (not MLAGs)."""
|
398
|
-
|
399
|
-
for interface in data[device]:
|
400
|
-
if data[device][interface]["type"] == "port-channel":
|
401
|
-
logger.info(
|
402
|
-
f"Local port channel {device} # {interface} -> {data[device][interface]['interfaces']}"
|
403
|
-
)
|
404
|
-
device_a = utils.nb.dcim.devices.get(name=device)
|
405
|
-
|
406
|
-
# Create the local port channel
|
407
|
-
port_channel_a = utils.nb.dcim.interfaces.get(name=interface, device=device)
|
408
|
-
if not port_channel_a:
|
409
|
-
try:
|
410
|
-
port_channel_a = utils.nb.dcim.interfaces.create(
|
411
|
-
name=interface, device=device_a.id, type="lag"
|
412
|
-
)
|
413
|
-
except pynetbox.core.query.RequestError as e:
|
414
|
-
logger.error(f"ERROR --> {e}")
|
415
|
-
pass
|
416
|
-
|
417
|
-
# Create the remote port channels and add the local interfaces to the local port channel
|
418
|
-
remote_port_channels = []
|
419
|
-
for interface_x in data[device][interface]["interfaces"]:
|
420
|
-
interface_a = utils.nb.dcim.interfaces.get(
|
421
|
-
name=interface_x, device=device
|
422
|
-
)
|
423
|
-
|
424
|
-
# NOTE: The VLANs on the Ethernet interfaces on the local devices are preserved for
|
425
|
-
# visibility in the Netbox.
|
426
|
-
# interface_a.untagged_vlan = None
|
427
|
-
# interface_a.tagged_vlans = []
|
428
|
-
|
429
|
-
interface_a.lag = port_channel_a
|
430
|
-
interface_a.save()
|
431
|
-
|
432
|
-
port_channel_b_name = (
|
433
|
-
f"Port-Channel{data[device][interface]['channel']}"
|
434
|
-
)
|
435
|
-
interface_b = utils.nb.dcim.interfaces.get(
|
436
|
-
name=interface_a.connected_endpoint.name,
|
437
|
-
device=interface_a.connected_endpoint.device,
|
438
|
-
)
|
439
|
-
|
440
|
-
interface_b.untagged_vlan = None
|
441
|
-
interface_b.tagged_vlans = []
|
442
|
-
|
443
|
-
logger.info(
|
444
|
-
f"Remote port channel {interface_b.device.name} # {port_channel_b_name} -> {interface_b.device.name} # {interface_b.name} ({interface_a.name})"
|
445
|
-
)
|
446
|
-
|
447
|
-
port_channel_b = utils.nb.dcim.interfaces.get(
|
448
|
-
name=port_channel_b_name, device=interface_b.device
|
449
|
-
)
|
450
|
-
if not port_channel_b:
|
451
|
-
try:
|
452
|
-
port_channel_b = utils.nb.dcim.interfaces.create(
|
453
|
-
name=port_channel_b_name,
|
454
|
-
device=interface_b.device.id,
|
455
|
-
type="lag",
|
456
|
-
)
|
457
|
-
except pynetbox.core.query.RequestError as e:
|
458
|
-
logger.error(f"ERROR --> {e}")
|
459
|
-
pass
|
460
|
-
|
461
|
-
interface_b.lag = port_channel_b
|
462
|
-
interface_b.save()
|
463
|
-
|
464
|
-
remote_port_channels.append(port_channel_b)
|
465
|
-
|
466
|
-
# Assign IP addresses to the local port channel
|
467
|
-
if "addresses" in data[device][interface]:
|
468
|
-
for address in data[device][interface]["addresses"]:
|
469
|
-
address_a = utils.nb.ipam.ip_addresses.get(address=address)
|
470
|
-
if type(address) == str:
|
471
|
-
address_a = utils.nb.ipam.ip_addresses.get(address=address)
|
472
|
-
logger.info(f"Address {address} -> {interface}")
|
473
|
-
if not address_a:
|
474
|
-
utils.nb.ipam.ip_addresses.create(
|
475
|
-
address=address,
|
476
|
-
assigned_object_type="dcim.interface",
|
477
|
-
assigned_object_id=port_channel_a.id,
|
478
|
-
)
|
479
|
-
else:
|
480
|
-
address_a = utils.nb.ipam.ip_addresses.get(
|
481
|
-
address=address["address"]
|
482
|
-
)
|
483
|
-
logger.info(f"Address {address['address']} -> {interface}")
|
484
|
-
if not address_a:
|
485
|
-
address_a = utils.nb.ipam.ip_addresses.create(
|
486
|
-
assigned_object_type="dcim.interface",
|
487
|
-
assigned_object_id=port_channel_a.id,
|
488
|
-
**address,
|
489
|
-
)
|
490
|
-
if "primary" in address and bool(address["primary"]):
|
491
|
-
device_a.primary_ip4 = address_a.id
|
492
|
-
device_a.save()
|
493
|
-
|
494
|
-
# Remove addresses from the local port channel that have been removed
|
495
|
-
for address in utils.nb.ipam.ip_addresses.filter(
|
496
|
-
device=device, interface=interface
|
497
|
-
):
|
498
|
-
delete = True
|
499
|
-
if "addresses" in data[device][interface]:
|
500
|
-
for address_a in data[device][interface]["addresses"]:
|
501
|
-
if type(address_a) == str and address_a == str(address):
|
502
|
-
delete = False
|
503
|
-
elif "address" in address_a and address_a["address"] == str(
|
504
|
-
address
|
505
|
-
):
|
506
|
-
delete = False
|
507
|
-
|
508
|
-
if delete:
|
509
|
-
address.delete()
|
510
|
-
|
511
|
-
# Assign VLANs to the local port channel as well as the remote port channels
|
512
|
-
port_channel_a.untagged_vlan = None
|
513
|
-
port_channel_a.tagged_vlans = []
|
514
|
-
port_channel_b.untagged_vlan = None
|
515
|
-
port_channel_b.tagged_vlans = []
|
516
|
-
|
517
|
-
if "vlans" in data[device][interface]:
|
518
|
-
tagged = False
|
519
|
-
for vlan in data[device][interface]["vlans"]:
|
520
|
-
vlan_a = utils.nb.ipam.vlans.get(vid=vlan)
|
521
|
-
if not vlan_a:
|
522
|
-
try:
|
523
|
-
vlan_a = utils.nb.ipam.vlans.create(
|
524
|
-
name=f"VLAN {vlan}", vid=vlan
|
525
|
-
)
|
526
|
-
except pynetbox.core.query.RequestError as e:
|
527
|
-
logger.error(f"ERROR --> {e}")
|
528
|
-
pass
|
529
|
-
|
530
|
-
if data[device][interface]["vlans"][vlan] == "untagged":
|
531
|
-
logger.info(
|
532
|
-
f"Untagged VLAN {vlan_a.vid} -> {port_channel_a.name}"
|
533
|
-
)
|
534
|
-
port_channel_a.untagged_vlan = vlan_a.id
|
535
|
-
|
536
|
-
for port_channel_b in remote_port_channels:
|
537
|
-
logger.info(
|
538
|
-
f"Untagged VLAN {vlan_a.vid} -> {port_channel_b.name}"
|
539
|
-
)
|
540
|
-
port_channel_b.untagged_vlan = vlan_a.id
|
541
|
-
elif vlan_a.id not in port_channel_a.tagged_vlans:
|
542
|
-
logger.info(
|
543
|
-
f"Tagged VLAN {vlan_a.vid} -> {port_channel_a.name}"
|
544
|
-
)
|
545
|
-
port_channel_a.tagged_vlans.append(vlan_a.id)
|
546
|
-
tagged = True
|
547
|
-
|
548
|
-
for port_channel_b in remote_port_channels:
|
549
|
-
logger.info(
|
550
|
-
f"Tagged VLAN {vlan_a.vid} -> {port_channel_b.name}"
|
551
|
-
)
|
552
|
-
port_channel_b.tagged_vlans.append(vlan_a.id)
|
553
|
-
|
554
|
-
if tagged:
|
555
|
-
port_channel_a.mode = "tagged"
|
556
|
-
|
557
|
-
for port_channel_b in remote_port_channels:
|
558
|
-
port_channel_b.mode = "tagged"
|
559
|
-
else:
|
560
|
-
port_channel_a.mode = "access"
|
561
|
-
|
562
|
-
for port_channel_b in remote_port_channels:
|
563
|
-
port_channel_b.mode = "access"
|
564
|
-
|
565
|
-
port_channel_a.save()
|
566
|
-
port_channel_b.save()
|
567
|
-
|
568
|
-
|
569
|
-
def remove_port_channels(device, data):
|
570
|
-
"""Remove local and remote port channels that no longer exist."""
|
571
|
-
|
572
|
-
for interface in utils.nb.dcim.interfaces.filter(device=device, type="lag"):
|
573
|
-
delete = True
|
574
|
-
for interface_a in data[device]:
|
575
|
-
if (
|
576
|
-
data[device][interface_a]["type"] == "port-channel"
|
577
|
-
and str(interface) == interface_a
|
578
|
-
):
|
579
|
-
delete = False
|
580
|
-
|
581
|
-
if delete and "Port-Channel" not in interface.name:
|
582
|
-
members = utils.nb.dcim.interfaces.filter(lag_id=interface.id)
|
583
|
-
for member in members:
|
584
|
-
member.connected_endpoint.lag.delete()
|
585
|
-
interface.delete()
|
586
|
-
|
587
|
-
|
588
|
-
def manage_virtual_interfaces(device, data):
|
589
|
-
"""Manage virtual interfaces."""
|
590
|
-
|
591
|
-
for interface in data[device]:
|
592
|
-
if data[device][interface]["type"] == "virtual":
|
593
|
-
logger.info(f"Virtual interface {interface} for {device}")
|
594
|
-
|
595
|
-
device_a = utils.nb.dcim.devices.get(name=device)
|
596
|
-
|
597
|
-
interface_a = utils.nb.dcim.interfaces.get(name=interface, device=device)
|
598
|
-
if not interface_a:
|
599
|
-
try:
|
600
|
-
interface_a = utils.nb.dcim.interfaces.create(
|
601
|
-
name=interface,
|
602
|
-
device=device_a.id,
|
603
|
-
type="virtual",
|
604
|
-
**data[device][interface]["data"],
|
605
|
-
)
|
606
|
-
except pynetbox.core.query.RequestError as e:
|
607
|
-
logger.error(f"ERROR --> {e}")
|
608
|
-
pass
|
609
|
-
|
610
|
-
if "addresses" in data[device][interface]:
|
611
|
-
for address in data[device][interface]["addresses"]:
|
612
|
-
address_a = utils.nb.ipam.ip_addresses.get(address=address)
|
613
|
-
if type(address) == str:
|
614
|
-
address_a = utils.nb.ipam.ip_addresses.get(address=address)
|
615
|
-
logger.info(f"Address {address} -> {interface}")
|
616
|
-
if not address_a:
|
617
|
-
utils.nb.ipam.ip_addresses.create(
|
618
|
-
address=address,
|
619
|
-
assigned_object_type="dcim.interface",
|
620
|
-
assigned_object_id=interface_a.id,
|
621
|
-
)
|
622
|
-
else:
|
623
|
-
address_a = utils.nb.ipam.ip_addresses.get(
|
624
|
-
address=address["address"]
|
625
|
-
)
|
626
|
-
logger.info(f"Address {address['address']} -> {interface}")
|
627
|
-
if not address_a:
|
628
|
-
address_a = utils.nb.ipam.ip_addresses.create(
|
629
|
-
assigned_object_type="dcim.interface",
|
630
|
-
assigned_object_id=interface_a.id,
|
631
|
-
**address,
|
632
|
-
)
|
633
|
-
if "primary" in address and bool(address["primary"]):
|
634
|
-
device_a.primary_ip4 = address_a.id
|
635
|
-
device_a.save()
|
636
|
-
|
637
|
-
# Remove addresses from the interface that have been removed
|
638
|
-
for address in utils.nb.ipam.ip_addresses.filter(
|
639
|
-
device=device, interface=interface
|
640
|
-
):
|
641
|
-
delete = True
|
642
|
-
if "addresses" in data[device][interface]:
|
643
|
-
for address_a in data[device][interface]["addresses"]:
|
644
|
-
if type(address_a) == str and address_a == str(address):
|
645
|
-
delete = False
|
646
|
-
elif "address" in address_a and address_a["address"] == str(
|
647
|
-
address
|
648
|
-
):
|
649
|
-
delete = False
|
650
|
-
|
651
|
-
if delete:
|
652
|
-
address.delete()
|
653
|
-
|
654
|
-
if "vlans" in data[device][interface]:
|
655
|
-
tagged = False
|
656
|
-
interface_a.untagged_vlan = None
|
657
|
-
interface_a.tagged_vlans = []
|
658
|
-
for vlan in data[device][interface]["vlans"]:
|
659
|
-
vlan_a = utils.nb.ipam.vlans.get(vid=vlan)
|
660
|
-
if not vlan_a:
|
661
|
-
try:
|
662
|
-
vlan_a = utils.nb.ipam.vlans.create(
|
663
|
-
name=f"VLAN {vlan}", vid=vlan
|
664
|
-
)
|
665
|
-
except pynetbox.core.query.RequestError as e:
|
666
|
-
logger.error(f"ERROR --> {e}")
|
667
|
-
pass
|
668
|
-
|
669
|
-
if data[device][interface]["vlans"][vlan] == "untagged":
|
670
|
-
interface_a.untagged_vlan = vlan_a.id
|
671
|
-
elif vlan_a.id not in interface_a.tagged_vlans:
|
672
|
-
interface_a.tagged_vlans.append(vlan_a.id)
|
673
|
-
tagged = True
|
674
|
-
|
675
|
-
if tagged:
|
676
|
-
interface_a.mode = "tagged"
|
677
|
-
else:
|
678
|
-
interface_a.mode = "access"
|
679
|
-
interface_a.save()
|
680
|
-
|
681
|
-
|
682
|
-
def remove_virtual_interfaces(device, data):
|
683
|
-
"""Remove virtual interfaces that no longer exist."""
|
684
|
-
|
685
|
-
for interface in utils.nb.dcim.interfaces.filter(device=device, type="virtual"):
|
686
|
-
delete = True
|
687
|
-
for interface_a in data[device]:
|
688
|
-
if (
|
689
|
-
data[device][interface_a]["type"] == "virtual"
|
690
|
-
and str(interface) == interface_a
|
691
|
-
):
|
692
|
-
delete = False
|
693
|
-
|
694
|
-
if delete:
|
695
|
-
interface.delete()
|
696
|
-
|
697
|
-
|
698
|
-
def manage_mlag_devices(device, data):
|
699
|
-
"""Manage MLAG devices (not port channels)."""
|
700
|
-
|
701
|
-
for interface in data[device]:
|
702
|
-
if data[device][interface]["type"] == "mlag":
|
703
|
-
data_a = data[device][interface]["data"]
|
704
|
-
device_a = utils.nb.dcim.devices.get(name=device)
|
705
|
-
|
706
|
-
logger.info(
|
707
|
-
f"Local port channel {device} # Port-Channel{data_a['channel']}"
|
708
|
-
)
|
709
|
-
|
710
|
-
port_channel_a = utils.nb.dcim.interfaces.get(
|
711
|
-
name=f"Port-Channel{data_a['channel']}", device=device
|
712
|
-
)
|
713
|
-
if not port_channel_a:
|
714
|
-
try:
|
715
|
-
port_channel_a = utils.nb.dcim.interfaces.create(
|
716
|
-
name=f"Port-Channel{data_a['channel']}",
|
717
|
-
device=device_a.id,
|
718
|
-
type="lag",
|
719
|
-
)
|
720
|
-
except pynetbox.core.query.RequestError as e:
|
721
|
-
logger.error(f"ERROR --> {e}")
|
722
|
-
pass
|
723
|
-
|
724
|
-
for interface_x in data[device][interface]["interfaces"]:
|
725
|
-
interface_a = utils.nb.dcim.interfaces.get(
|
726
|
-
name=interface_x, device=device
|
727
|
-
)
|
728
|
-
interface_a.lag = port_channel_a
|
729
|
-
interface_a.save()
|
730
|
-
|
731
|
-
logger.info(f"Virtual interface {data_a['vlan']} for {device}")
|
732
|
-
interface_a = utils.nb.dcim.interfaces.get(
|
733
|
-
name=f"Vlan{data_a['vlan']}", device=device
|
734
|
-
)
|
735
|
-
if not interface_a:
|
736
|
-
try:
|
737
|
-
interface_a = utils.nb.dcim.interfaces.create(
|
738
|
-
name=f"Vlan{data_a['vlan']}", device=device_a.id, type="virtual"
|
739
|
-
)
|
740
|
-
except pynetbox.core.query.RequestError as e:
|
741
|
-
logger.error(f"ERROR --> {e}")
|
742
|
-
pass
|
743
|
-
|
744
|
-
vlan_a = utils.nb.ipam.vlans.get(vid=data_a["vlan"])
|
745
|
-
interface_a.untagged_vlan = vlan_a
|
746
|
-
interface_a.parent = port_channel_a
|
747
|
-
interface_a.save()
|
748
|
-
|
749
|
-
# logger.info(f"Address {data_a['address']} -> {interface_a.name}")
|
750
|
-
address_a = utils.nb.ipam.ip_addresses.get(address=data_a["address"])
|
751
|
-
if not address_a:
|
752
|
-
utils.nb.ipam.ip_addresses.create(
|
753
|
-
address=data_a["address"],
|
754
|
-
assigned_object_type="dcim.interface",
|
755
|
-
assigned_object_id=interface_a.id,
|
756
|
-
)
|
757
|
-
|
758
|
-
|
759
31
|
def set_maintenance(device, state):
|
760
32
|
"""Set the maintenance state for a device in the Netbox."""
|
761
33
|
|
@@ -766,38 +38,6 @@ def set_maintenance(device, state):
|
|
766
38
|
device_a.save()
|
767
39
|
|
768
40
|
|
769
|
-
def set_state(device, state, state_type):
|
770
|
-
"""Set the state for a device in the Netbox."""
|
771
|
-
|
772
|
-
lock = Redlock(key=f"lock_state_{device}", masters={utils.redis})
|
773
|
-
lock.acquire()
|
774
|
-
|
775
|
-
if state_type == "power":
|
776
|
-
set_power_state(device, state)
|
777
|
-
elif state_type == "provision":
|
778
|
-
set_provision_state(device, state)
|
779
|
-
elif state_type == "introspection":
|
780
|
-
set_introspection_state(device, state)
|
781
|
-
elif state_type == "ironic":
|
782
|
-
set_ironic_state(device, state)
|
783
|
-
elif state_type == "deployment":
|
784
|
-
set_deployment_state(device, state)
|
785
|
-
else:
|
786
|
-
set_device_state(device, state)
|
787
|
-
|
788
|
-
lock.release()
|
789
|
-
|
790
|
-
|
791
|
-
def set_provision_state(device, state):
|
792
|
-
"""Set the provision state (provision_state) for a device in the Netbox."""
|
793
|
-
|
794
|
-
logger.info(f"Set provision state of device {device} = {state}")
|
795
|
-
|
796
|
-
device_a = utils.nb.dcim.devices.get(name=device)
|
797
|
-
device_a.custom_fields = {"provision_state": state}
|
798
|
-
device_a.save()
|
799
|
-
|
800
|
-
|
801
41
|
def set_ironic_state(device, state):
|
802
42
|
"""Set the ironic state (ironic_state) for a device in the Netbox."""
|
803
43
|
|
@@ -828,16 +68,6 @@ def set_deployment_state(device, state):
|
|
828
68
|
device_a.save()
|
829
69
|
|
830
70
|
|
831
|
-
def set_power_state(device, state):
|
832
|
-
"""Set the power state (power_state) for a device in the Netbox."""
|
833
|
-
|
834
|
-
logger.info(f"Set power state of device {device} = {state}")
|
835
|
-
|
836
|
-
device_a = utils.nb.dcim.devices.get(name=device)
|
837
|
-
device_a.custom_fields = {"power_state": state}
|
838
|
-
device_a.save()
|
839
|
-
|
840
|
-
|
841
71
|
def set_device_state(device, state):
|
842
72
|
"""Set the state (device_state) for a device in the Netbox."""
|
843
73
|
|
@@ -848,132 +78,43 @@ def set_device_state(device, state):
|
|
848
78
|
device_a.save()
|
849
79
|
|
850
80
|
|
851
|
-
def
|
852
|
-
"""Set the
|
853
|
-
|
854
|
-
logger.info(f"Set transition of device {device} = {transition}")
|
855
|
-
|
856
|
-
device_a = utils.nb.dcim.devices.get(name=device)
|
857
|
-
device_a.custom_fields = {"device_transition": transition}
|
858
|
-
device_a.save()
|
859
|
-
|
860
|
-
|
861
|
-
def get_device_state(device, states):
|
862
|
-
"""Get the state (device_state) for a device in the Netbox."""
|
863
|
-
|
864
|
-
return states[device]
|
865
|
-
|
866
|
-
|
867
|
-
def get_device_transition(device, transitions):
|
868
|
-
"""Get the transition (device_transition) for a device in the Netbox."""
|
869
|
-
|
870
|
-
return transitions[device]
|
871
|
-
|
872
|
-
|
873
|
-
def get_connected_devices(device, data):
|
874
|
-
"""Get all devices that are connected to a device in a certain state."""
|
875
|
-
|
876
|
-
result = []
|
877
|
-
|
878
|
-
if device not in data:
|
879
|
-
return result
|
880
|
-
|
881
|
-
for interface in data[device]:
|
882
|
-
if "device" in data[device][interface]:
|
883
|
-
result.append(data[device][interface]["device"])
|
884
|
-
|
885
|
-
return result
|
886
|
-
|
887
|
-
|
888
|
-
def run(device, state=None, data={}, enforce=False):
|
889
|
-
"""Transition a device to a specific state."""
|
890
|
-
|
891
|
-
# If no state is specified use the state that is stored in the Netbox
|
892
|
-
if not state or state in ["0", "None"]:
|
893
|
-
state = get_state(device)
|
894
|
-
|
895
|
-
# If the state in the Netbox is 0/None then set the state to a
|
896
|
-
if state in ["0", "None"]:
|
897
|
-
state = "a"
|
898
|
-
|
899
|
-
if not data:
|
900
|
-
data = load_data_from_filesystem(None, device, state)
|
901
|
-
|
902
|
-
states = get_states(data.keys())
|
903
|
-
current_state = get_device_state(device, states)
|
904
|
-
|
905
|
-
# Device is already in the target state, no transition necessary
|
906
|
-
if not enforce and current_state == state:
|
907
|
-
logger.info(f"Device {device} is already in state {state}")
|
908
|
-
return
|
909
|
-
|
910
|
-
transitions = get_transitions(data.keys())
|
911
|
-
current_transition = get_device_transition(device, transitions)
|
81
|
+
def set_state(device, state, state_type):
|
82
|
+
"""Set the state for a device in the Netbox."""
|
912
83
|
|
913
|
-
|
914
|
-
current_data = load_data_from_filesystem(None, device, current_state)
|
915
|
-
else:
|
916
|
-
current_data = {}
|
917
|
-
|
918
|
-
# One transition is already running, no second transition possible
|
919
|
-
if not enforce and current_transition and current_transition != "0":
|
920
|
-
logger.info(f"{device} is already in transit")
|
921
|
-
return
|
922
|
-
|
923
|
-
# Get connected devices in source and target state
|
924
|
-
connected_devices = set(
|
925
|
-
get_connected_devices(device, current_data)
|
926
|
-
+ get_connected_devices(device, data)
|
927
|
-
)
|
928
|
-
logger.info(connected_devices)
|
929
|
-
|
930
|
-
# Allow only one active transition per device
|
931
|
-
lock = Redlock(key=f"lock_{device}", masters={utils.redis}, auto_release_time=120)
|
84
|
+
lock = Redlock(key=f"lock_state_{device}", masters={utils.redis})
|
932
85
|
lock.acquire()
|
933
86
|
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
# transition: from-to, phase 2 (generate the new configuration)
|
948
|
-
transition = f"from_{states[device]}-to_{state}-phase_2"
|
949
|
-
set_device_transition(device, transition)
|
950
|
-
|
951
|
-
for connected_device in [x for x in connected_devices if x]:
|
952
|
-
generate_configuration.for_device(connected_device)
|
87
|
+
if state_type == "power":
|
88
|
+
set_power_state(device, state)
|
89
|
+
elif state_type == "provision":
|
90
|
+
set_provision_state(device, state)
|
91
|
+
elif state_type == "introspection":
|
92
|
+
set_introspection_state(device, state)
|
93
|
+
elif state_type == "ironic":
|
94
|
+
set_ironic_state(device, state)
|
95
|
+
elif state_type == "deployment":
|
96
|
+
set_deployment_state(device, state)
|
97
|
+
else:
|
98
|
+
set_device_state(device, state)
|
953
99
|
|
954
|
-
|
100
|
+
lock.release()
|
955
101
|
|
956
|
-
# transition: from-to, phase 3 (deploy the new configuration)
|
957
|
-
transition = f"from_{states[device]}-to_{state}-phase_3"
|
958
102
|
|
959
|
-
|
960
|
-
|
103
|
+
def set_provision_state(device, state):
|
104
|
+
"""Set the provision state (provision_state) for a device in the Netbox."""
|
961
105
|
|
962
|
-
|
963
|
-
set_device_state(device, f"{state}-phase_3")
|
106
|
+
logger.info(f"Set provision state of device {device} = {state}")
|
964
107
|
|
965
|
-
|
966
|
-
|
967
|
-
|
108
|
+
device_a = utils.nb.dcim.devices.get(name=device)
|
109
|
+
device_a.custom_fields = {"provision_state": state}
|
110
|
+
device_a.save()
|
968
111
|
|
969
|
-
# for connected_device in connected_devices:
|
970
|
-
# validate_configuration.for_device(connected_device)
|
971
112
|
|
972
|
-
|
113
|
+
def set_power_state(device, state):
|
114
|
+
"""Set the power state (power_state) for a device in the Netbox."""
|
973
115
|
|
974
|
-
|
975
|
-
transition = ""
|
976
|
-
set_device_transition(device, transition)
|
977
|
-
set_device_state(device, f"{state}")
|
116
|
+
logger.info(f"Set power state of device {device} = {state}")
|
978
117
|
|
979
|
-
|
118
|
+
device_a = utils.nb.dcim.devices.get(name=device)
|
119
|
+
device_a.custom_fields = {"power_state": state}
|
120
|
+
device_a.save()
|