Skip to content

Commit 493f08a

Browse files
committed
feat: prevent device/address change when linked to CMDB
1 parent 17732ae commit 493f08a

File tree

8 files changed

+114
-1
lines changed

8 files changed

+114
-1
lines changed

netbox_cmdb/netbox_cmdb/models/bgp.py

+5
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
from utilities.choices import ChoiceSet
99
from utilities.querysets import RestrictedQuerySet
1010

11+
from netbox_cmdb import protect
1112
from netbox_cmdb.choices import AssetMonitoringStateChoices, AssetStateChoices
1213
from netbox_cmdb.constants import BGP_MAX_ASN, BGP_MIN_ASN
1314

1415

16+
@protect.from_device_name_change("device")
1517
class BGPGlobal(ChangeLoggedModel):
1618
"""Global BGP configuration.
1719
@@ -197,6 +199,7 @@ class Meta:
197199
abstract = True
198200

199201

202+
@protect.from_device_name_change("device")
200203
class BGPPeerGroup(BGPSessionCommon):
201204
"""A BGP Peer Group contains a set of BGP neighbors that shares common attributes."""
202205

@@ -229,6 +232,8 @@ def get_absolute_url(self):
229232
return reverse("plugins:netbox_cmdb:bgppeergroup", args=[self.pk])
230233

231234

235+
@protect.from_device_name_change("device")
236+
@protect.from_ip_address_change("local_address")
232237
class DeviceBGPSession(BGPSessionCommon):
233238
"""A Device BGP Session is a BGP session from a given device's perspective.
234239
It contains BGP local parameters for the given devices (as the local address / ASN)."""

netbox_cmdb/netbox_cmdb/models/bgp_community_list.py

+3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from django.db import models
22
from netbox.models import ChangeLoggedModel
33

4+
from netbox_cmdb import protect
45

6+
7+
@protect.from_device_name_change("device")
58
class BGPCommunityList(ChangeLoggedModel):
69
"""An object used in RoutePolicy object to filter on a list of BGP communities."""
710

netbox_cmdb/netbox_cmdb/models/interface.py

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.db import models
33
from netbox.models import ChangeLoggedModel
44

5+
from netbox_cmdb import protect
56
from netbox_cmdb.choices import AssetMonitoringStateChoices, AssetStateChoices
67

78
FEC_CHOICES = [
@@ -22,6 +23,7 @@
2223
]
2324

2425

26+
@protect.from_device_name_change("device")
2527
class DeviceInterface(ChangeLoggedModel):
2628
"""A device interface configuration."""
2729

@@ -67,6 +69,7 @@ class Meta:
6769
unique_together = ("device", "name")
6870

6971

72+
@protect.from_ip_address_change("ipv4_address", "ipv6_address")
7073
class LogicalInterface(ChangeLoggedModel):
7174
"""A logical interface configuration."""
7275

netbox_cmdb/netbox_cmdb/models/prefix_list.py

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from netbox.models import ChangeLoggedModel
77
from utilities.choices import ChoiceSet
88

9+
from netbox_cmdb import protect
10+
911

1012
class PrefixListIPVersionChoices(ChoiceSet):
1113
"""Prefix list IP versions choices."""
@@ -19,6 +21,7 @@ class PrefixListIPVersionChoices(ChoiceSet):
1921
)
2022

2123

24+
@protect.from_device_name_change("device")
2225
class PrefixList(ChangeLoggedModel):
2326
"""Prefix list main model."""
2427

netbox_cmdb/netbox_cmdb/models/route_policy.py

+2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
from netbox.models import ChangeLoggedModel
55
from utilities.querysets import RestrictedQuerySet
66

7+
from netbox_cmdb import protect
78
from netbox_cmdb.choices import DecisionChoice
89
from netbox_cmdb.fields import CustomIPAddressField
910

1011

12+
@protect.from_device_name_change("device")
1113
class RoutePolicy(ChangeLoggedModel):
1214
"""
1315
A RoutePolicy contains a name and a description and is optionally linked to a Device.

netbox_cmdb/netbox_cmdb/models/snmp.py

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.db import models
22
from netbox.models import ChangeLoggedModel
33

4+
from netbox_cmdb import protect
45
from netbox_cmdb.choices import SNMPCommunityType
56

67

@@ -23,6 +24,7 @@ class Meta:
2324
verbose_name_plural = "SNMP Communities"
2425

2526

27+
@protect.from_device_name_change("device")
2628
class SNMP(ChangeLoggedModel):
2729
"""A Snmp configuration"""
2830

netbox_cmdb/netbox_cmdb/protect.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
MODELS_LINKED_TO_DEVICE = {}
2+
MODELS_LINKED_TO_IP_ADDRESS = {}
3+
4+
5+
def from_device_name_change(*fields):
6+
def decorator(cls):
7+
if cls not in MODELS_LINKED_TO_DEVICE:
8+
MODELS_LINKED_TO_DEVICE[cls] = set()
9+
10+
if not fields:
11+
return cls
12+
13+
for field in fields:
14+
MODELS_LINKED_TO_DEVICE[cls].add(field)
15+
16+
return cls
17+
18+
return decorator
19+
20+
21+
def from_ip_address_change(*fields):
22+
def decorator(cls):
23+
if cls not in MODELS_LINKED_TO_IP_ADDRESS:
24+
MODELS_LINKED_TO_IP_ADDRESS[cls] = set()
25+
26+
if not fields:
27+
return cls
28+
29+
for field in fields:
30+
MODELS_LINKED_TO_IP_ADDRESS[cls].add(field)
31+
32+
return cls
33+
34+
return decorator

netbox_cmdb/netbox_cmdb/signals.py

+62-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
from django.db.models.signals import post_delete
1+
from dcim.models import Device
2+
from django.core.exceptions import ValidationError
3+
from django.db.models.signals import post_delete, pre_save
24
from django.dispatch import receiver
5+
from ipam.models import IPAddress
36

7+
from netbox_cmdb import protect
48
from netbox_cmdb.models.bgp import BGPSession
59

610

@@ -13,3 +17,60 @@ def clean_device_bgp_sessions(sender, instance, **kwargs):
1317
if instance.peer_b:
1418
b = instance.peer_b
1519
b.delete()
20+
21+
22+
@receiver(pre_save, sender=Device)
23+
def protect_from_device_name_change(sender, instance, **kwargs):
24+
"""Prevents any name changes for dcim.Device if there is a CMDB object linked to it.
25+
26+
Some models in the CMDB depends on NetBox Device native model.
27+
If one changes the Device name, it might affect the CMDB as a side effect, and could cause
28+
unwanted configuration changes.
29+
"""
30+
31+
if not instance.pk:
32+
return
33+
34+
current = Device.objects.get(pk=instance.pk)
35+
36+
if current.name == instance.name:
37+
return
38+
39+
for model, fields in protect.MODELS_LINKED_TO_DEVICE.items():
40+
if not fields:
41+
continue
42+
43+
for field in fields:
44+
filter = {field: instance}
45+
if model.objects.filter(**filter).exists():
46+
raise ValidationError(
47+
f"Device name cannot be changed because it is linked to: {model}."
48+
)
49+
50+
51+
@receiver(pre_save, sender=IPAddress)
52+
def protect_from_ip_address_change(sender, instance, **kwargs):
53+
"""Prevents any name changes for ipam.IPAddress if there is a CMDB object linked to it.
54+
55+
Some models in the CMDB depends on NetBox IPAddress native model.
56+
If one changes the address, it might affect the CMDB as a side effect, and could cause
57+
unwanted configuration changes.
58+
"""
59+
if not instance.pk:
60+
return
61+
62+
current = IPAddress.objects.get(pk=instance.pk)
63+
64+
if current.address.ip == instance.address.ip:
65+
return
66+
67+
for model, fields in protect.MODELS_LINKED_TO_IP_ADDRESS.items():
68+
if not fields:
69+
continue
70+
71+
for field in fields:
72+
filter = {field: instance}
73+
if model.objects.filter(**filter).exists():
74+
raise ValidationError(
75+
f"IP address cannot be changed because it is linked to: {model}."
76+
)

0 commit comments

Comments
 (0)