Skip to content

Closes #12135: Prevent the deletion of interfaces with children #14091

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

Merged
merged 8 commits into from
Nov 1, 2023
3 changes: 3 additions & 0 deletions docs/models/dcim/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ If selected, this component will be treated as if a cable has been connected.

Virtual interfaces can be bound to a physical parent interface. This is helpful for modeling virtual interfaces which employ encapsulation on a physical interface, such as an 802.1Q VLAN-tagged subinterface.

!!! note
An interface with one or more child interfaces assigned cannot be deleted until all its child interfaces have been deleted or reassigned.

### Bridged Interface

Interfaces can be bridged to other interfaces on a device in two manners: symmetric or grouped.
Expand Down
3 changes: 3 additions & 0 deletions docs/models/virtualization/vminterface.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ The interface's name. Must be unique to the assigned VM.

Identifies the parent interface of a subinterface (e.g. used to employ encapsulation).

!!! note
An interface with one or more child interfaces assigned cannot be deleted until all its child interfaces have been deleted or reassigned.

### Bridged Interface

An interface on the same VM with which this interface is bridged.
Expand Down
19 changes: 19 additions & 0 deletions netbox/dcim/migrations/0182_protect_child_interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.6 on 2023-10-20 11:48

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('dcim', '0181_rename_device_role_device_role'),
]

operations = [
migrations.AlterField(
model_name='interface',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='child_interfaces', to='dcim.interface'),
),
]
2 changes: 1 addition & 1 deletion netbox/dcim/models/device_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ class BaseInterface(models.Model):
)
parent = models.ForeignKey(
to='self',
on_delete=models.SET_NULL,
on_delete=models.RESTRICT,
related_name='child_interfaces',
null=True,
blank=True,
Expand Down
6 changes: 3 additions & 3 deletions netbox/netbox/views/generic/bulk_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import FieldDoesNotExist, ObjectDoesNotExist, ValidationError
from django.db import transaction, IntegrityError
from django.db.models import ManyToManyField, ProtectedError
from django.db.models import ManyToManyField, ProtectedError, RestrictedError
from django.db.models.fields.reverse_related import ManyToManyRel
from django.forms import HiddenInput, ModelMultipleChoiceField, MultipleHiddenInput
from django.http import HttpResponse
Expand Down Expand Up @@ -804,8 +804,8 @@ def post(self, request, **kwargs):
obj.snapshot()
obj.delete()

except ProtectedError as e:
logger.info("Caught ProtectedError while attempting to delete objects")
except (ProtectedError, RestrictedError) as e:
logger.info(f"Caught {type(e)} while attempting to delete objects")
handle_protectederror(queryset, request, e)
return redirect(self.get_return_url(request))

Expand Down
6 changes: 3 additions & 3 deletions netbox/netbox/views/generic/object_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from django.contrib import messages
from django.db import transaction
from django.db.models import ProtectedError
from django.db.models import ProtectedError, RestrictedError
from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.html import escape
Expand Down Expand Up @@ -374,8 +374,8 @@ def post(self, request, *args, **kwargs):
try:
obj.delete()

except ProtectedError as e:
logger.info("Caught ProtectedError while attempting to delete object")
except (ProtectedError, RestrictedError) as e:
logger.info(f"Caught {type(e)} while attempting to delete objects")
handle_protectederror([obj], request, e)
return redirect(obj.get_absolute_url())

Expand Down
22 changes: 17 additions & 5 deletions netbox/utilities/error_handlers.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
from django.contrib import messages
from django.db.models import ProtectedError, RestrictedError
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _


def handle_protectederror(obj_list, request, e):
"""
Generate a user-friendly error message in response to a ProtectedError exception.
Generate a user-friendly error message in response to a ProtectedError or RestrictedError exception.
"""
protected_objects = list(e.protected_objects)
protected_count = len(protected_objects) if len(protected_objects) <= 50 else 'More than 50'
err_message = f"Unable to delete <strong>{', '.join(str(obj) for obj in obj_list)}</strong>. " \
f"{protected_count} dependent objects were found: "
if type(e) is ProtectedError:
protected_objects = list(e.protected_objects)
elif type(e) is RestrictedError:
protected_objects = list(e.restricted_objects)
else:
raise e

# Formulate the error message
err_message = _(
"Unable to delete <strong>{objects}</strong>. {count} dependent objects were found: ".format(
objects=', '.join(str(obj) for obj in obj_list),
count=len(protected_objects) if len(protected_objects) <= 50 else _('More than 50')
)
)

# Append dependent objects to error message
dependent_objects = []
Expand Down
19 changes: 19 additions & 0 deletions netbox/virtualization/migrations/0037_protect_child_interfaces.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.2.6 on 2023-10-20 11:48

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('virtualization', '0036_virtualmachine_config_template'),
]

operations = [
migrations.AlterField(
model_name='vminterface',
name='parent',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='child_interfaces', to='virtualization.vminterface'),
),
]