Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prevent device/address change when linked to CMDB #46

Merged
merged 2 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions netbox_cmdb/netbox_cmdb/models/bgp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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)."""
Expand Down
3 changes: 3 additions & 0 deletions netbox_cmdb/netbox_cmdb/models/bgp_community_list.py
Original file line number Diff line number Diff line change
@@ -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."""

Expand Down
3 changes: 3 additions & 0 deletions netbox_cmdb/netbox_cmdb/models/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -22,6 +23,7 @@
]


@protect.from_device_name_change("device")
class DeviceInterface(ChangeLoggedModel):
"""A device interface configuration."""

Expand Down Expand Up @@ -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."""

Expand Down
3 changes: 3 additions & 0 deletions netbox_cmdb/netbox_cmdb/models/prefix_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -19,6 +21,7 @@ class PrefixListIPVersionChoices(ChoiceSet):
)


@protect.from_device_name_change("device")
class PrefixList(ChangeLoggedModel):
"""Prefix list main model."""

Expand Down
2 changes: 2 additions & 0 deletions netbox_cmdb/netbox_cmdb/models/route_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions netbox_cmdb/netbox_cmdb/models/snmp.py
Original file line number Diff line number Diff line change
@@ -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


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


@protect.from_device_name_change("device")
class SNMP(ChangeLoggedModel):
"""A Snmp configuration"""

Expand Down
44 changes: 44 additions & 0 deletions netbox_cmdb/netbox_cmdb/protect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
MODELS_LINKED_TO_DEVICE = {}
MODELS_LINKED_TO_IP_ADDRESS = {}


def from_device_name_change(*fields):
"""Protects from Device name changes in NetBox DCIM.

This is useful only to prevent Device name changes when CMDB is linked to it.
"""

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):
"""Protects from IP Address "address" changes in NetBox IPAM.

This is useful only to prevent IP Address "address" changes when CMDB is linked to it.
"""

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
63 changes: 62 additions & 1 deletion netbox_cmdb/netbox_cmdb/signals.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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:
filter = {field: instance}
if model.objects.filter(**filter).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:
filter = {field: instance}
if model.objects.filter(**filter).exists():
raise ValidationError(
f"IP address cannot be changed because it is linked to: {model}."
)
Loading