From d92fd3a872f116b5899e47e0bc2f8268efcf7ad9 Mon Sep 17 00:00:00 2001
From: Kevin Petremann
Date: Wed, 21 Aug 2024 13:06:48 +0200
Subject: [PATCH 1/9] feat(ui): site decommisioning
---
netbox_cmdb/netbox_cmdb/helpers/cleaning.py | 58 +++++
netbox_cmdb/netbox_cmdb/template_content.py | 19 +-
.../netbox_cmdb/decommissioning.html | 43 ----
.../netbox_cmdb/decommissioning/base.html | 33 +++
.../decommissioning/device_summary.html | 35 +++
.../decommissioning/site_progressive.html | 29 +++
netbox_cmdb/netbox_cmdb/urls.py | 16 +-
netbox_cmdb/netbox_cmdb/views.py | 234 ++++++++++++------
8 files changed, 333 insertions(+), 134 deletions(-)
create mode 100644 netbox_cmdb/netbox_cmdb/helpers/cleaning.py
delete mode 100644 netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning.html
create mode 100644 netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/base.html
create mode 100644 netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/device_summary.html
create mode 100644 netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/site_progressive.html
diff --git a/netbox_cmdb/netbox_cmdb/helpers/cleaning.py b/netbox_cmdb/netbox_cmdb/helpers/cleaning.py
new file mode 100644
index 0000000..6f1e30f
--- /dev/null
+++ b/netbox_cmdb/netbox_cmdb/helpers/cleaning.py
@@ -0,0 +1,58 @@
+from dcim.models import Location, Rack
+from django.db.models import Q
+
+from netbox_cmdb.models.bgp import BGPPeerGroup, BGPSession, DeviceBGPSession
+from netbox_cmdb.models.bgp_community_list import BGPCommunityList
+from netbox_cmdb.models.prefix_list import PrefixList
+from netbox_cmdb.models.route_policy import RoutePolicy
+from netbox_cmdb.models.snmp import SNMP
+
+
+def clean_cmdb_for_devices(device_ids: list[int]):
+ deleted_objects = {
+ "bgp_sessions": [],
+ "device_bgp_sessions": [],
+ "bgp_peer_groups": [],
+ "route_policies": [],
+ "prefix_lists": [],
+ "bgp_community_lists": [],
+ "snmp": [],
+ }
+
+ bgp_sessions = BGPSession.objects.filter(
+ Q(peer_a__device__id__in=device_ids) | Q(peer_b__device__id__in=device_ids)
+ )
+ device_bgp_sessions = DeviceBGPSession.objects.filter(device__id__in=device_ids)
+ bgp_peer_groups = BGPPeerGroup.objects.filter(device__id__in=device_ids)
+ route_policies = RoutePolicy.objects.filter(device__id__in=device_ids)
+ prefix_lists = PrefixList.objects.filter(device__id__in=device_ids)
+ bgp_community_lists = BGPCommunityList.objects.filter(device__id__in=device_ids)
+ snmp = SNMP.objects.filter(device__id__in=device_ids)
+
+ deleted_objects["bgp_sessions"] = [str(val) for val in list(bgp_sessions)]
+ deleted_objects["device_bgp_sessions"] = [str(val) for val in list(device_bgp_sessions)]
+ deleted_objects["bgp_peer_groups"] = [str(val) for val in list(bgp_peer_groups)]
+ deleted_objects["route_policies"] = [str(val) for val in list(route_policies)]
+ deleted_objects["prefix_lists"] = [str(val) for val in list(prefix_lists)]
+ deleted_objects["bgp_community_lists"] = [str(val) for val in list(bgp_community_lists)]
+ deleted_objects["snmp"] = [str(val) for val in list(snmp)]
+
+ bgp_sessions.delete()
+ device_bgp_sessions.delete()
+ bgp_peer_groups.delete()
+ route_policies.delete()
+ prefix_lists.delete()
+ bgp_community_lists.delete()
+ snmp.delete()
+
+ return deleted_objects
+
+
+def clean_site_topology(site):
+ racks = Rack.objects.filter(site=site.id)
+ racks.delete()
+
+ locations = Location.objects.filter(site=site.id)
+ locations.delete()
+
+ site.delete()
diff --git a/netbox_cmdb/netbox_cmdb/template_content.py b/netbox_cmdb/netbox_cmdb/template_content.py
index e573140..b988644 100644
--- a/netbox_cmdb/netbox_cmdb/template_content.py
+++ b/netbox_cmdb/netbox_cmdb/template_content.py
@@ -1,15 +1,22 @@
from extras.plugins import PluginTemplateExtension
-class Decommisioning(PluginTemplateExtension):
- model = "dcim.device"
-
+class DecommisioningBase(PluginTemplateExtension):
def buttons(self):
return (
- f'Decommission'
)
-template_extensions = [Decommisioning]
+class DeviceDecommisioning(DecommisioningBase):
+ model = "dcim.device"
+ obj = "device"
+
+
+class SiteDecommisioning(DecommisioningBase):
+ model = "dcim.site"
+ obj = "site"
+
+
+template_extensions = [DeviceDecommisioning, SiteDecommisioning]
diff --git a/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning.html b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning.html
deleted file mode 100644
index c456889..0000000
--- a/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning.html
+++ /dev/null
@@ -1,43 +0,0 @@
-{% extends "base/layout.html" %}
-
-{% block content-wrapper %}
-
- {% if error %}
-
{{ error }}
- {% else %}
-
-
Deleted objects
-
-
-
- {% for key, objects in deleted_objects.items %}
- {% if objects %}
-
- {% endif %}
- {% endfor %}
-
-
-
- {% endif %}
-
-{% endblock content-wrapper%}
diff --git a/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/base.html b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/base.html
new file mode 100644
index 0000000..3e5310d
--- /dev/null
+++ b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/base.html
@@ -0,0 +1,33 @@
+{% extends "base/layout.html" %}
+{% block title %}
+ {{ object.name }} decommissioning
+{% endblock %}
+
+{% block content-wrapper %}
+
+
+
+
+
+
+ {% if error %}
+
{{ error }}
+ {% else %}
+
+
+ Warning: this action will remove both
+ CMDB assets and DCIM of concerned asset(s)
+
+
+
+ {% endif %}
+
+
+
+{% endblock content-wrapper%}
diff --git a/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/device_summary.html b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/device_summary.html
new file mode 100644
index 0000000..415e94a
--- /dev/null
+++ b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/device_summary.html
@@ -0,0 +1,35 @@
+
+ {% if error %}
+
{{ error }}
+ {% endif %}
+
+
Deleted objects
+
+
+
+ {% for key, objects in deleted_objects.items %}
+ {% if objects %}
+
+ {% endif %}
+ {% endfor %}
+
diff --git a/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/site_progressive.html b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/site_progressive.html
new file mode 100644
index 0000000..77d25ee
--- /dev/null
+++ b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/site_progressive.html
@@ -0,0 +1,29 @@
+{% if not stop %}
+
+{% endif %}
+
+{# --- #}
+{# OOB components #}
+{# -- #}
+
+
+ {{ status | safe }}
+
+
+{% if error %}
+
+ {{ error | safe }}
+
+{% endif %}
+
+
+ {{ message | safe }}
+
diff --git a/netbox_cmdb/netbox_cmdb/urls.py b/netbox_cmdb/netbox_cmdb/urls.py
index eac2fe6..923584d 100644
--- a/netbox_cmdb/netbox_cmdb/urls.py
+++ b/netbox_cmdb/netbox_cmdb/urls.py
@@ -20,15 +20,16 @@
BGPSessionEditView,
BGPSessionListView,
BGPSessionView,
- DecommissioningView,
DeviceBGPSessionEditView,
DeviceBGPSessionListView,
DeviceBGPSessionView,
- DeviceBGPSessionDeleteView,
+ DeviceDecommissioningView,
+ DeviecBGPSessionDeleteView,
RoutePolicyDeleteView,
RoutePolicyEditView,
RoutePolicyListView,
RoutePolicyView,
+ SiteDecommissioningView,
SNMPCommunityDeleteView,
SNMPCommunityEditView,
SNMPCommunityListView,
@@ -39,9 +40,14 @@
urlpatterns = [
path(
- "decommisioning//delete",
- DecommissioningView.as_view(),
- name="decommisioning_delete",
+ "decommisioning/device//delete",
+ DeviceDecommissioningView.as_view(),
+ name="device_decommisioning_delete",
+ ),
+ path(
+ "decommisioning/site//delete",
+ SiteDecommissioningView.as_view(),
+ name="site_decommisioning_delete",
),
# ASN
path("asn/", ASNListView.as_view(), name="asn_list"),
diff --git a/netbox_cmdb/netbox_cmdb/views.py b/netbox_cmdb/netbox_cmdb/views.py
index 47ee2c5..b4e508e 100644
--- a/netbox_cmdb/netbox_cmdb/views.py
+++ b/netbox_cmdb/netbox_cmdb/views.py
@@ -1,8 +1,10 @@
"""Views."""
-from dcim.models import Device
+import math
+from datetime import datetime
+
+from dcim.models import Device, Site
from django.db import transaction
-from django.db.models import Q
from django.shortcuts import render
from netbox.views.generic import (
ObjectDeleteView,
@@ -12,7 +14,6 @@
)
from netbox.views.generic.bulk_views import BulkDeleteView
from utilities.forms import ConfirmationForm
-from utilities.htmx import is_htmx
from utilities.utils import count_related
from netbox_cmdb.filtersets import (
@@ -34,6 +35,7 @@
SNMPCommunityGroupForm,
SNMPGroupForm,
)
+from netbox_cmdb.helpers import cleaning
from netbox_cmdb.models.bgp import (
ASN,
AfiSafi,
@@ -41,8 +43,6 @@
BGPSession,
DeviceBGPSession,
)
-from netbox_cmdb.models.bgp_community_list import BGPCommunityList
-from netbox_cmdb.models.prefix_list import PrefixList
from netbox_cmdb.models.route_policy import RoutePolicy
from netbox_cmdb.models.snmp import SNMP, SNMPCommunity
from netbox_cmdb.tables import (
@@ -56,10 +56,11 @@
)
-## Decommission a device
-class DecommissioningView(ObjectDeleteView):
- queryset = Device.objects.all()
- template_name = "netbox_cmdb/decommissioning.html"
+class DecommissioningBaseView(ObjectDeleteView):
+ template_name = "netbox_cmdb/decommissioning/base.html"
+ site_template_name = "netbox_cmdb/decommissioning/site_progressive.html"
+ device_template_name = "netbox_cmdb/decommissioning/device_summary.html"
+ base_form_url = ""
def get(self, request, *args, **kwargs):
"""
@@ -68,98 +69,171 @@ def get(self, request, *args, **kwargs):
Args:
request: The current request
"""
- obj = self.get_object(**kwargs)
+ object = self.get_object(**kwargs)
form = ConfirmationForm(initial=request.GET)
-
- # If this is an HTMX request, return only the rendered deletion form as modal content
- if is_htmx(request):
- # form_url = reverse("decommisioning_delete", kwargs={'pk': obj.pk})
- form_url = f"/plugins/cmdb/decommisioning/{kwargs['pk']}/delete"
- return render(
- request,
- "htmx/delete_form.html",
- {
- "object": obj,
- "object_type": self.queryset.model._meta.verbose_name,
- "form": form,
- "form_url": form_url,
- **self.get_extra_context(request, obj),
- },
- )
+ devices = []
+
+ if isinstance(object, Device):
+ devices.append(object)
+ object_type = "device"
+ elif isinstance(object, Site):
+ devices = Device.objects.filter(site=object.id)
+ object_type = "site"
+ else:
+ error = f"{type(object)} decommissioning is not supported"
+ return render(request, self.template_name, context={"error": error})
return render(
request,
self.template_name,
{
- "object": obj,
+ "object": object,
+ "object_type": object_type,
"form": form,
- "return_url": self.get_return_url(request, obj),
- **self.get_extra_context(request, obj),
+ "return_url": self.get_return_url(request, object),
+ **self.get_extra_context(request, object),
},
)
def post(self, request, *args, **kwargs):
# Fetch the device to delete
- device = self.get_object(**kwargs)
- deleted_objects = {
- "bgp_sessions": [],
- "device_bgp_sessions": [],
- "bgp_peer_groups": [],
- "route_policies": [],
- "prefix_lists": [],
- "bgp_community_lists": [],
- "snmp": [],
- }
-
- device_name = device.name
+ object = self.get_object(**kwargs)
+
+ if isinstance(object, Device):
+ return self._device_cleaning(request, object, *args, **kwargs)
+
+ elif isinstance(object, Site):
+ return self._site_cleaning(request, object, *args, **kwargs)
+ else:
+ error = f"{type(object)} decommissioning is not supported"
+ return render(request, self.template_name, context={"error": error})
+
+ def _device_cleaning(self, request, device, *args, **kwargs):
try:
with transaction.atomic():
- bgp_sessions = BGPSession.objects.filter(
- Q(peer_a__device__id=device.id) | Q(peer_b__device__id=device.id)
+ deleted = cleaning.clean_cmdb_for_devices([device.id])
+ device.delete()
+ except Exception as error:
+ return render(
+ request,
+ self.device_template_name,
+ context={"error": f"Failed to clean device: {error}"},
+ )
+
+ return render(
+ request,
+ self.device_template_name,
+ context={"deleted_device": device.name, "deleted_objects": deleted},
+ )
+
+ def _site_cleaning(self, request, site, *args, **kwargs):
+ devices = Device.objects.filter(site=site.id)
+
+ # Get list of devices to delete
+ CHUNK_SIZE = 20
+ device_ids = [dev.id for dev in devices]
+ remaining_chunks = math.ceil(len(device_ids) / CHUNK_SIZE)
+
+ # We avoid an infinite loop if we fail to delete devices
+ if request.POST.get("chunks"):
+ previously_remaining_chunks = int(request.POST["chunks"])
+ if remaining_chunks >= previously_remaining_chunks:
+ return render(
+ request,
+ self.site_template_name,
+ context={
+ "object": site,
+ "object_type": "site",
+ "status": 'Failed',
+ "error": "devices are not being deleted, stopping here",
+ "chunks": remaining_chunks,
+ "stop": True,
+ },
+ )
+
+ if not device_ids:
+ try:
+ with transaction.atomic():
+ cleaning.clean_site_topology(site)
+ except Exception as error:
+ return render(
+ request,
+ self.site_template_name,
+ context={
+ "object": site,
+ "object_type": "site",
+ "status": 'Failed',
+ "error": f"Topology cleaning failure: {error}",
+ "chunks": remaining_chunks,
+ "stop": True,
+ },
)
- device_bgp_sessions = DeviceBGPSession.objects.filter(device__id=device.id)
- bgp_peer_groups = BGPPeerGroup.objects.filter(device__id=device.id)
- route_policies = RoutePolicy.objects.filter(device__id=device.id)
- prefix_lists = PrefixList.objects.filter(device__id=device.id)
- bgp_community_lists = BGPCommunityList.objects.filter(device__id=device.id)
- snmp = SNMP.objects.filter(device__id=device.id)
-
- deleted_objects["bgp_sessions"] = [str(val) for val in list(bgp_sessions)]
- deleted_objects["device_bgp_sessions"] = [
- str(val) for val in list(device_bgp_sessions)
- ]
- deleted_objects["bgp_peer_groups"] = [str(val) for val in list(bgp_peer_groups)]
- deleted_objects["route_policies"] = [str(val) for val in list(route_policies)]
- deleted_objects["prefix_lists"] = [str(val) for val in list(prefix_lists)]
- deleted_objects["bgp_community_lists"] = [
- str(val) for val in list(bgp_community_lists)
- ]
- deleted_objects["snmp"] = [str(val) for val in list(snmp)]
-
- bgp_sessions.delete()
- device_bgp_sessions.delete()
- bgp_peer_groups.delete()
- route_policies.delete()
- prefix_lists.delete()
- bgp_community_lists.delete()
- snmp.delete()
-
- except Exception as e:
- # Render the template with an error message
- return render(request, self.template_name, context={"error": str(e)})
-
- # Call the parent class's post method to delete the device
- super().post(request, *args, **kwargs)
-
- # Return the HTML response with the list of deleted objects
+
+ chunk = device_ids[0:CHUNK_SIZE]
+
+ try:
+ with transaction.atomic():
+ cleaning.clean_cmdb_for_devices(chunk)
+ device_names = [dev.name for dev in devices[0:CHUNK_SIZE]]
+ for dev in devices[0:CHUNK_SIZE]:
+ dev.delete()
+
+ except Exception as error:
+ return render(
+ request,
+ self.site_template_name,
+ context={
+ "object": site,
+ "object_type": "site",
+ "status": 'Failed',
+ "error": f"Device cleaning failure: {error}",
+ "chunks": remaining_chunks,
+ "stop": True,
+ },
+ )
+
+ # Tell the client to continue requesting deletion
+ if remaining_chunks:
+ status = f"Number of devices to delete: {len(device_ids) - len(chunk)}"
+ message = f'{datetime.now().strftime("%H:%M:%S")} - deleted: {device_names}
'
+
+ return render(
+ request,
+ self.site_template_name,
+ context={
+ "object": site,
+ "object_type": "site",
+ "status": status,
+ "message": message,
+ "chunks": remaining_chunks,
+ },
+ )
+
return render(
request,
- self.template_name,
- context={"deleted_device": device_name, "deleted_objects": deleted_objects},
+ self.site_template_name,
+ context={
+ "object": site,
+ "object_type": "site",
+ "status": 'Success',
+ "message": f'{site.name}: site, racks, locations, devices and CMDB deleted
',
+ "chunks": remaining_chunks,
+ "stop": True,
+ },
)
+class DeviceDecommissioningView(DecommissioningBaseView):
+ base_form_url = "/plugins/cmdb/decommisioning/device"
+ queryset = Device.objects.all()
+
+
+class SiteDecommissioningView(DecommissioningBaseView):
+ base_form_url = "/plugins/cmdb/decommisioning/site"
+ queryset = Site.objects.all()
+
+
## ASN views
class ASNListView(ObjectListView):
queryset = ASN.objects.all()
From 3e1a81de5e10b05500cb6a11843c4ed72f3129a1 Mon Sep 17 00:00:00 2001
From: Kevin Petremann
Date: Wed, 21 Aug 2024 14:37:49 +0200
Subject: [PATCH 2/9] refactor(decomm): factorize for webAPI and webUI
---
netbox_cmdb/netbox_cmdb/api/cmdb/views.py | 23 ++++++++++++-----------
1 file changed, 12 insertions(+), 11 deletions(-)
diff --git a/netbox_cmdb/netbox_cmdb/api/cmdb/views.py b/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
index 3d2ea01..dfa4dde 100644
--- a/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
+++ b/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
@@ -1,3 +1,4 @@
+from dcim.models import Device
from django.db import transaction
from django.db.models import Q
from drf_yasg.utils import swagger_auto_schema
@@ -6,6 +7,7 @@
from rest_framework.response import Response
from rest_framework.views import APIView
+from netbox_cmdb.helpers import cleaning
from netbox_cmdb.models.bgp import BGPPeerGroup, BGPSession, DeviceBGPSession
from netbox_cmdb.models.bgp_community_list import BGPCommunityList
from netbox_cmdb.models.prefix_list import PrefixList
@@ -26,6 +28,7 @@ class DeleteAllCMDBObjectsRelatedToDevice(APIView):
responses={
status.HTTP_200_OK: "Objects related to device have been deleted successfully",
status.HTTP_400_BAD_REQUEST: "Bad Request: Device name is required",
+ status.HTTP_404_NOT_FOUND: "Bad Request: Device not found",
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error: Something went wrong on the server",
},
)
@@ -36,22 +39,20 @@ def post(self, request):
{"error": "Device name is required"}, status=status.HTTP_400_BAD_REQUEST
)
+ devices = Device.objects.filter(name=device_name)
+ device_ids = [dev.id for dev in devices]
+ if not device_ids:
+ return Response(
+ {"error": "no matching devices found"}, status=status.HTTP_404_NOT_FOUND
+ )
+
try:
with transaction.atomic():
- # Delete objects in reverse order of dependencies
- BGPSession.objects.filter(
- Q(peer_a__device__name=device_name) | Q(peer_b__device__name=device_name)
- ).delete()
- DeviceBGPSession.objects.filter(device__name=device_name).delete()
- BGPPeerGroup.objects.filter(device__name=device_name).delete()
- RoutePolicy.objects.filter(device__name=device_name).delete()
- PrefixList.objects.filter(device__name=device_name).delete()
- BGPCommunityList.objects.filter(device_name=device_name).delete()
- SNMP.objects.filter(device__name=device_name).delete()
+ deleted = cleaning.clean_cmdb_for_devices(device_ids)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(
- {"message": f"Objects related to device {device_name} have been deleted successfully"},
+ {"message": f"Objects related to device {device_name} have been deleted successfully: {deleted}"},
status=status.HTTP_200_OK,
)
From a598c52bfdd9304bc104a92ec3dbbefe57df9e09 Mon Sep 17 00:00:00 2001
From: Kevin Petremann
Date: Wed, 21 Aug 2024 16:03:31 +0200
Subject: [PATCH 3/9] feat(api): add decommission-site endpoint
---
netbox_cmdb/netbox_cmdb/api/cmdb/views.py | 89 ++++++++++++++++++++---
netbox_cmdb/netbox_cmdb/api/urls.py | 9 ++-
2 files changed, 87 insertions(+), 11 deletions(-)
diff --git a/netbox_cmdb/netbox_cmdb/api/cmdb/views.py b/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
index dfa4dde..ce0b5b9 100644
--- a/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
+++ b/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
@@ -1,6 +1,6 @@
-from dcim.models import Device
+from dcim.models import Device, Site
from django.db import transaction
-from django.db.models import Q
+from django.http import StreamingHttpResponse
from drf_yasg.utils import swagger_auto_schema
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from rest_framework import serializers, status
@@ -8,11 +8,6 @@
from rest_framework.views import APIView
from netbox_cmdb.helpers import cleaning
-from netbox_cmdb.models.bgp import BGPPeerGroup, BGPSession, DeviceBGPSession
-from netbox_cmdb.models.bgp_community_list import BGPCommunityList
-from netbox_cmdb.models.prefix_list import PrefixList
-from netbox_cmdb.models.route_policy import RoutePolicy
-from netbox_cmdb.models.snmp import SNMP
class DeleteAllCMDBObjectsRelatedToDeviceSerializer(serializers.Serializer):
@@ -36,7 +31,7 @@ def post(self, request):
device_name = request.data.get("device_name", None)
if device_name is None:
return Response(
- {"error": "Device name is required"}, status=status.HTTP_400_BAD_REQUEST
+ {"error": "device name is required"}, status=status.HTTP_400_BAD_REQUEST
)
devices = Device.objects.filter(name=device_name)
@@ -53,6 +48,82 @@ def post(self, request):
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response(
- {"message": f"Objects related to device {device_name} have been deleted successfully: {deleted}"},
+ {
+ "message": f"objects related to device {device_name} have been deleted successfully",
+ "deleted": deleted,
+ },
status=status.HTTP_200_OK,
)
+
+
+class DecommissionSiteSerializer(serializers.Serializer):
+ site_name = serializers.CharField()
+
+
+class DecommissionSite(APIView):
+
+ permission_classes = [IsAuthenticatedOrLoginNotRequired]
+
+ @swagger_auto_schema(
+ request_body=DecommissionSiteSerializer,
+ responses={
+ status.HTTP_200_OK: "Site have been deleted successfully",
+ status.HTTP_400_BAD_REQUEST: "Bad Request: Site name is required",
+ status.HTTP_404_NOT_FOUND: "Bad Request: Site not found",
+ status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error: Something went wrong on the server",
+ },
+ )
+ def post(self, request):
+ site_name = request.data.get("site_name", None)
+ if site_name is None:
+ return Response({"error": "site name is required"}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ site = Site.objects.get(name=site_name)
+ except Site.DoesNotExist:
+ return Response({"error": "site not found"}, status=status.HTTP_404_NOT_FOUND)
+ except Exception:
+ return Response(
+ {"error": "internal server error"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+
+ devices = Device.objects.filter(site=site.id)
+
+ import logging
+
+ def _start():
+ CHUNK_SIZE = 20
+ device_ids = [dev.id for dev in devices]
+ for i in range(0, len(device_ids), CHUNK_SIZE):
+ chunk = device_ids[i : i + CHUNK_SIZE]
+ try:
+ with transaction.atomic():
+ cleaning.clean_cmdb_for_devices(chunk)
+ for dev in devices[i : i + CHUNK_SIZE]:
+ dev.delete()
+ yield f'{{"deleted": {[dev.name for dev in devices[i:i+CHUNK_SIZE]]}}}\n\n'
+
+ except Exception as e:
+ StreamingHttpResponse.status_code = 500
+ msg = {"error": str(e)}
+ yield f"{msg}\n\n"
+ return
+
+ try:
+ with transaction.atomic():
+ logging.warning("cleaning site")
+ cleaning.clean_site_topology(site)
+ yield "{{'message': 'topology cleaned'}}\n\n"
+ except Exception as e:
+ logging.warning("error: %s", e)
+ StreamingHttpResponse.status_code = 500
+ msg = {"error": str(e)}
+ yield f"{msg}\n\n"
+ return
+
+ msg = {
+ "message": f"site {site_name} has been deleted successfully",
+ }
+ yield f"{msg}\n\n"
+
+ return StreamingHttpResponse(_start(), content_type="text/plain")
diff --git a/netbox_cmdb/netbox_cmdb/api/urls.py b/netbox_cmdb/netbox_cmdb/api/urls.py
index d1930d4..d7cdaec 100644
--- a/netbox_cmdb/netbox_cmdb/api/urls.py
+++ b/netbox_cmdb/netbox_cmdb/api/urls.py
@@ -13,7 +13,7 @@
from netbox_cmdb.api.prefix_list.views import PrefixListViewSet
from netbox_cmdb.api.route_policy.views import RoutePolicyViewSet
from netbox_cmdb.api.snmp.views import SNMPCommunityViewSet, SNMPViewSet
-from netbox_cmdb.api.cmdb.views import DeleteAllCMDBObjectsRelatedToDevice
+from netbox_cmdb.api.cmdb.views import DeleteAllCMDBObjectsRelatedToDevice, DecommissionSite
router = NetBoxRouter()
@@ -37,7 +37,12 @@
path(
"cmdb/delete-all-objects/",
DeleteAllCMDBObjectsRelatedToDevice.as_view(),
- name="asns-available-asn",
+ name="delete-all-objects",
+ ),
+ path(
+ "cmdb/decommission-site/",
+ DecommissionSite.as_view(),
+ name="decommission-site",
),
]
urlpatterns += router.urls
From 02861eeb9fc9058c9fa9bb6e6239869a1cbf96ab Mon Sep 17 00:00:00 2001
From: Kevin Petremann
Date: Wed, 21 Aug 2024 16:03:51 +0200
Subject: [PATCH 4/9] refactor(api): decom and delete now in management
---
netbox_cmdb/netbox_cmdb/api/urls.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/netbox_cmdb/netbox_cmdb/api/urls.py b/netbox_cmdb/netbox_cmdb/api/urls.py
index d7cdaec..e98f5d9 100644
--- a/netbox_cmdb/netbox_cmdb/api/urls.py
+++ b/netbox_cmdb/netbox_cmdb/api/urls.py
@@ -35,12 +35,12 @@
name="asns-available-asn",
),
path(
- "cmdb/delete-all-objects/",
+ "management/delete-all-objects/",
DeleteAllCMDBObjectsRelatedToDevice.as_view(),
name="delete-all-objects",
),
path(
- "cmdb/decommission-site/",
+ "management/decommission-site/",
DecommissionSite.as_view(),
name="decommission-site",
),
From e7a95e8895f26d560a7944722ecf400ddc0f638b Mon Sep 17 00:00:00 2001
From: Kevin Petremann
Date: Wed, 21 Aug 2024 16:05:54 +0200
Subject: [PATCH 5/9] chore: remove extra debug logging
---
netbox_cmdb/netbox_cmdb/api/cmdb/views.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/netbox_cmdb/netbox_cmdb/api/cmdb/views.py b/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
index ce0b5b9..4b898d0 100644
--- a/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
+++ b/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
@@ -89,8 +89,6 @@ def post(self, request):
devices = Device.objects.filter(site=site.id)
- import logging
-
def _start():
CHUNK_SIZE = 20
device_ids = [dev.id for dev in devices]
@@ -111,11 +109,9 @@ def _start():
try:
with transaction.atomic():
- logging.warning("cleaning site")
cleaning.clean_site_topology(site)
yield "{{'message': 'topology cleaned'}}\n\n"
except Exception as e:
- logging.warning("error: %s", e)
StreamingHttpResponse.status_code = 500
msg = {"error": str(e)}
yield f"{msg}\n\n"
From 7afedcbf730c709d17d2601f7f972af2ab74726e Mon Sep 17 00:00:00 2001
From: Kevin Petremann
Date: Wed, 21 Aug 2024 16:14:55 +0200
Subject: [PATCH 6/9] fix: import renamed
---
netbox_cmdb/netbox_cmdb/urls.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/netbox_cmdb/netbox_cmdb/urls.py b/netbox_cmdb/netbox_cmdb/urls.py
index 923584d..748980d 100644
--- a/netbox_cmdb/netbox_cmdb/urls.py
+++ b/netbox_cmdb/netbox_cmdb/urls.py
@@ -24,7 +24,7 @@
DeviceBGPSessionListView,
DeviceBGPSessionView,
DeviceDecommissioningView,
- DeviecBGPSessionDeleteView,
+ DeviceBGPSessionDeleteView,
RoutePolicyDeleteView,
RoutePolicyEditView,
RoutePolicyListView,
From af271b25f758da9eae7a04eb7c34161ba5846dc6 Mon Sep 17 00:00:00 2001
From: Kevin Petremann
Date: Wed, 21 Aug 2024 16:18:38 +0200
Subject: [PATCH 7/9] refactor(decommission): delete instead of post
---
netbox_cmdb/netbox_cmdb/api/cmdb/views.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/netbox_cmdb/netbox_cmdb/api/cmdb/views.py b/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
index 4b898d0..6e5ab14 100644
--- a/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
+++ b/netbox_cmdb/netbox_cmdb/api/cmdb/views.py
@@ -27,7 +27,7 @@ class DeleteAllCMDBObjectsRelatedToDevice(APIView):
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error: Something went wrong on the server",
},
)
- def post(self, request):
+ def delete(self, request):
device_name = request.data.get("device_name", None)
if device_name is None:
return Response(
@@ -73,7 +73,7 @@ class DecommissionSite(APIView):
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error: Something went wrong on the server",
},
)
- def post(self, request):
+ def delete(self, request):
site_name = request.data.get("site_name", None)
if site_name is None:
return Response({"error": "site name is required"}, status=status.HTTP_400_BAD_REQUEST)
From a1267ef262e59758399fe608f42b05d9fecccada Mon Sep 17 00:00:00 2001
From: Kevin Petremann
Date: Thu, 22 Aug 2024 10:42:22 +0200
Subject: [PATCH 8/9] refactor: better View class use and fix typo
---
netbox_cmdb/netbox_cmdb/template_content.py | 10 +--
.../netbox_cmdb/decommissioning/base.html | 2 +-
.../decommissioning/site_progressive.html | 2 +-
netbox_cmdb/netbox_cmdb/urls.py | 8 +-
netbox_cmdb/netbox_cmdb/views.py | 80 ++++++++-----------
5 files changed, 46 insertions(+), 56 deletions(-)
diff --git a/netbox_cmdb/netbox_cmdb/template_content.py b/netbox_cmdb/netbox_cmdb/template_content.py
index b988644..b869993 100644
--- a/netbox_cmdb/netbox_cmdb/template_content.py
+++ b/netbox_cmdb/netbox_cmdb/template_content.py
@@ -1,22 +1,22 @@
from extras.plugins import PluginTemplateExtension
-class DecommisioningBase(PluginTemplateExtension):
+class DecommissioningBase(PluginTemplateExtension):
def buttons(self):
return (
- f'Decommission'
)
-class DeviceDecommisioning(DecommisioningBase):
+class DeviceDecommissioning(DecommissioningBase):
model = "dcim.device"
obj = "device"
-class SiteDecommisioning(DecommisioningBase):
+class SiteDecommissioning(DecommissioningBase):
model = "dcim.site"
obj = "site"
-template_extensions = [DeviceDecommisioning, SiteDecommisioning]
+template_extensions = [DeviceDecommissioning, SiteDecommissioning]
diff --git a/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/base.html b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/base.html
index 3e5310d..55093e1 100644
--- a/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/base.html
+++ b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning/base.html
@@ -20,7 +20,7 @@