diff --git a/netbox_cmdb/netbox_cmdb/models/bgp.py b/netbox_cmdb/netbox_cmdb/models/bgp.py index 4aa5799..461be60 100644 --- a/netbox_cmdb/netbox_cmdb/models/bgp.py +++ b/netbox_cmdb/netbox_cmdb/models/bgp.py @@ -8,10 +8,12 @@ from utilities.choices import ChoiceSet from utilities.querysets import RestrictedQuerySet +from netbox_cmdb import protect from netbox_cmdb.choices import AssetMonitoringStateChoices, AssetStateChoices from netbox_cmdb.constants import BGP_MAX_ASN, BGP_MIN_ASN +@protect.from_device_name_change("device") class BGPGlobal(ChangeLoggedModel): """Global BGP configuration. @@ -197,6 +199,7 @@ class Meta: abstract = True +@protect.from_device_name_change("device") class BGPPeerGroup(BGPSessionCommon): """A BGP Peer Group contains a set of BGP neighbors that shares common attributes.""" @@ -229,6 +232,8 @@ def get_absolute_url(self): return reverse("plugins:netbox_cmdb:bgppeergroup", args=[self.pk]) +@protect.from_device_name_change("device") +@protect.from_ip_address_change("local_address") class DeviceBGPSession(BGPSessionCommon): """A Device BGP Session is a BGP session from a given device's perspective. It contains BGP local parameters for the given devices (as the local address / ASN).""" diff --git a/netbox_cmdb/netbox_cmdb/models/bgp_community_list.py b/netbox_cmdb/netbox_cmdb/models/bgp_community_list.py index b8513f8..fd744e0 100644 --- a/netbox_cmdb/netbox_cmdb/models/bgp_community_list.py +++ b/netbox_cmdb/netbox_cmdb/models/bgp_community_list.py @@ -1,7 +1,10 @@ from django.db import models from netbox.models import ChangeLoggedModel +from netbox_cmdb import protect + +@protect.from_device_name_change("device") class BGPCommunityList(ChangeLoggedModel): """An object used in RoutePolicy object to filter on a list of BGP communities.""" diff --git a/netbox_cmdb/netbox_cmdb/models/interface.py b/netbox_cmdb/netbox_cmdb/models/interface.py index 1f97419..10baa85 100644 --- a/netbox_cmdb/netbox_cmdb/models/interface.py +++ b/netbox_cmdb/netbox_cmdb/models/interface.py @@ -2,6 +2,7 @@ from django.db import models from netbox.models import ChangeLoggedModel +from netbox_cmdb import protect from netbox_cmdb.choices import AssetMonitoringStateChoices, AssetStateChoices FEC_CHOICES = [ @@ -22,6 +23,7 @@ ] +@protect.from_device_name_change("device") class DeviceInterface(ChangeLoggedModel): """A device interface configuration.""" @@ -67,6 +69,7 @@ class Meta: unique_together = ("device", "name") +@protect.from_ip_address_change("ipv4_address", "ipv6_address") class LogicalInterface(ChangeLoggedModel): """A logical interface configuration.""" diff --git a/netbox_cmdb/netbox_cmdb/models/prefix_list.py b/netbox_cmdb/netbox_cmdb/models/prefix_list.py index 764ead2..7e59477 100644 --- a/netbox_cmdb/netbox_cmdb/models/prefix_list.py +++ b/netbox_cmdb/netbox_cmdb/models/prefix_list.py @@ -6,6 +6,8 @@ from netbox.models import ChangeLoggedModel from utilities.choices import ChoiceSet +from netbox_cmdb import protect + class PrefixListIPVersionChoices(ChoiceSet): """Prefix list IP versions choices.""" @@ -19,6 +21,7 @@ class PrefixListIPVersionChoices(ChoiceSet): ) +@protect.from_device_name_change("device") class PrefixList(ChangeLoggedModel): """Prefix list main model.""" diff --git a/netbox_cmdb/netbox_cmdb/models/route_policy.py b/netbox_cmdb/netbox_cmdb/models/route_policy.py index 600a56c..302b4d3 100644 --- a/netbox_cmdb/netbox_cmdb/models/route_policy.py +++ b/netbox_cmdb/netbox_cmdb/models/route_policy.py @@ -4,10 +4,12 @@ from netbox.models import ChangeLoggedModel from utilities.querysets import RestrictedQuerySet +from netbox_cmdb import protect from netbox_cmdb.choices import DecisionChoice from netbox_cmdb.fields import CustomIPAddressField +@protect.from_device_name_change("device") class RoutePolicy(ChangeLoggedModel): """ A RoutePolicy contains a name and a description and is optionally linked to a Device. diff --git a/netbox_cmdb/netbox_cmdb/models/snmp.py b/netbox_cmdb/netbox_cmdb/models/snmp.py index 327bdab..5f956e4 100644 --- a/netbox_cmdb/netbox_cmdb/models/snmp.py +++ b/netbox_cmdb/netbox_cmdb/models/snmp.py @@ -1,6 +1,7 @@ from django.db import models from netbox.models import ChangeLoggedModel +from netbox_cmdb import protect from netbox_cmdb.choices import SNMPCommunityType @@ -23,6 +24,7 @@ class Meta: verbose_name_plural = "SNMP Communities" +@protect.from_device_name_change("device") class SNMP(ChangeLoggedModel): """A Snmp configuration""" diff --git a/netbox_cmdb/netbox_cmdb/protect.py b/netbox_cmdb/netbox_cmdb/protect.py new file mode 100644 index 0000000..1a9651a --- /dev/null +++ b/netbox_cmdb/netbox_cmdb/protect.py @@ -0,0 +1,34 @@ +MODELS_LINKED_TO_DEVICE = {} +MODELS_LINKED_TO_IP_ADDRESS = {} + + +def from_device_name_change(*fields): + def decorator(cls): + if cls not in MODELS_LINKED_TO_DEVICE: + MODELS_LINKED_TO_DEVICE[cls] = set() + + if not fields: + return cls + + for field in fields: + MODELS_LINKED_TO_DEVICE[cls].add(field) + + return cls + + return decorator + + +def from_ip_address_change(*fields): + def decorator(cls): + if cls not in MODELS_LINKED_TO_IP_ADDRESS: + MODELS_LINKED_TO_IP_ADDRESS[cls] = set() + + if not fields: + return cls + + for field in fields: + MODELS_LINKED_TO_IP_ADDRESS[cls].add(field) + + return cls + + return decorator diff --git a/netbox_cmdb/netbox_cmdb/signals.py b/netbox_cmdb/netbox_cmdb/signals.py index ad975c1..dd51db3 100644 --- a/netbox_cmdb/netbox_cmdb/signals.py +++ b/netbox_cmdb/netbox_cmdb/signals.py @@ -1,6 +1,10 @@ -from django.db.models.signals import post_delete +from dcim.models import Device +from django.core.exceptions import ValidationError +from django.db.models.signals import post_delete, pre_save from django.dispatch import receiver +from ipam.models import IPAddress +from netbox_cmdb import protect from netbox_cmdb.models.bgp import BGPSession @@ -13,3 +17,60 @@ def clean_device_bgp_sessions(sender, instance, **kwargs): if instance.peer_b: b = instance.peer_b b.delete() + + +@receiver(pre_save, sender=Device) +def protect_from_device_name_change(sender, instance, **kwargs): + """Prevents any name changes for dcim.Device if there is a CMDB object linked to it. + + Some models in the CMDB depends on NetBox Device native model. + If one changes the Device name, it might affect the CMDB as a side effect, and could cause + unwanted configuration changes. + """ + + if not instance.pk: + return + + current = Device.objects.get(pk=instance.pk) + + if current.name == instance.name: + return + + for model, fields in protect.MODELS_LINKED_TO_DEVICE.items(): + if not fields: + continue + + for field in fields: + fitler = {field: instance} + if model.objects.filter(**fitler).exists(): + raise ValidationError( + f"Device name cannot be changed because it is linked to: {model}." + ) + + +@receiver(pre_save, sender=IPAddress) +def protect_from_ip_address_change(sender, instance, **kwargs): + """Prevents any name changes for ipam.IPAddress if there is a CMDB object linked to it. + + Some models in the CMDB depends on NetBox IPAddress native model. + If one changes the address, it might affect the CMDB as a side effect, and could cause + unwanted configuration changes. + """ + if not instance.pk: + return + + current = IPAddress.objects.get(pk=instance.pk) + + if current.address.ip == instance.address.ip: + return + + for model, fields in protect.MODELS_LINKED_TO_IP_ADDRESS.items(): + if not fields: + continue + + for field in fields: + fitler = {field: instance} + if model.objects.filter(**fitler).exists(): + raise ValidationError( + f"IP address cannot be changed because it is linked to: {model}." + )