osism 0.20250312.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.
@@ -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 set_device_transition(device, transition):
852
- """Set the transition (device_transition) for a device in the Netbox."""
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
- if current_state and current_state not in ["0", "None"]:
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
- # transition: from-to, phase 1 (modifications in the Netbox)
935
- transition = f"from_{states[device]}-to_{state}-phase_1"
936
- set_device_transition(device, transition)
937
-
938
- manage_interfaces(device, data)
939
- manage_port_channels(device, data)
940
- remove_port_channels(device, data)
941
- manage_virtual_interfaces(device, data)
942
- remove_virtual_interfaces(device, data)
943
- manage_mlag_devices(device, data)
944
-
945
- set_device_state(device, f"{state}-phase_1")
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
- set_device_state(device, f"{state}-phase_2")
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
- for connected_device in [x for x in connected_devices if x]:
960
- deploy_configuration.for_device(connected_device)
103
+ def set_provision_state(device, state):
104
+ """Set the provision state (provision_state) for a device in the Netbox."""
961
105
 
962
- set_device_transition(device, transition)
963
- set_device_state(device, f"{state}-phase_3")
106
+ logger.info(f"Set provision state of device {device} = {state}")
964
107
 
965
- # transition: from-to, phase 4 (validate the deployed configuration)
966
- transition = f"from_{states[device]}-to_{state}-phase_4"
967
- set_device_transition(device, transition)
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
- set_device_state(device, f"{state}-phase_4")
113
+ def set_power_state(device, state):
114
+ """Set the power state (power_state) for a device in the Netbox."""
973
115
 
974
- # target state reached
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
- lock.release()
118
+ device_a = utils.nb.dcim.devices.get(name=device)
119
+ device_a.custom_fields = {"power_state": state}
120
+ device_a.save()