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: site decommissioning #43

Merged
merged 9 commits into from
Aug 22, 2024
156 changes: 134 additions & 22 deletions netbox_cmdb/netbox_cmdb/api/cmdb/views.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,169 @@
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
from rest_framework.response import Response
from rest_framework.views import APIView

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
from netbox_cmdb.helpers import cleaning


class DeleteAllCMDBObjectsRelatedToDeviceSerializer(serializers.Serializer):
class DeviceDecommissioningBaseSerializer(serializers.Serializer):
device_name = serializers.CharField()


class DeleteAllCMDBObjectsRelatedToDevice(APIView):
class DeviceCMDBDecommissioningAPIView(APIView):

permission_classes = [IsAuthenticatedOrLoginNotRequired]

@swagger_auto_schema(
request_body=DeleteAllCMDBObjectsRelatedToDeviceSerializer,
request_body=DeviceDecommissioningBaseSerializer,
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",
},
)
def post(self, request):
def delete(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)
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"CMDB cleaned for {device_name}",
"deleted": deleted,
},
status=status.HTTP_200_OK,
)


class DeviceDecommissioningAPIView(APIView):

permission_classes = [IsAuthenticatedOrLoginNotRequired]

@swagger_auto_schema(
request_body=DeviceDecommissioningBaseSerializer,
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",
},
)
def delete(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
)

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():
deleted = cleaning.clean_cmdb_for_devices(device_ids)
for device in devices:
device.delete()
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)

return Response(
{
"message": f"{device_name} decommissionned",
"deleted": deleted,
},
status=status.HTTP_200_OK,
)


class SiteDecommissioningSerializer(serializers.Serializer):
site_name = serializers.CharField()


class SiteDecommissioningAPIView(APIView):

permission_classes = [IsAuthenticatedOrLoginNotRequired]

@swagger_auto_schema(
request_body=SiteDecommissioningSerializer,
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 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)

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)

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():
cleaning.clean_site_topology(site)
yield "{{'message': 'topology cleaned'}}\n\n"
except Exception as 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")
22 changes: 18 additions & 4 deletions netbox_cmdb/netbox_cmdb/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
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 (
DeviceCMDBDecommissioningAPIView,
DeviceDecommissioningAPIView,
SiteDecommissioningAPIView,
)

router = NetBoxRouter()

Expand All @@ -35,9 +39,19 @@
name="asns-available-asn",
),
path(
"cmdb/delete-all-objects/",
DeleteAllCMDBObjectsRelatedToDevice.as_view(),
name="asns-available-asn",
"management/device-cmdb-decommissioning/",
DeviceCMDBDecommissioningAPIView.as_view(),
name="device-cmdb-decommissioning",
),
path(
"management/device-decommissioning/",
DeviceDecommissioningAPIView.as_view(),
name="device-decommissioning",
),
path(
"management/site-decommissioning/",
SiteDecommissioningAPIView.as_view(),
name="site-decommissioning",
),
]
urlpatterns += router.urls
58 changes: 58 additions & 0 deletions netbox_cmdb/netbox_cmdb/helpers/cleaning.py
Original file line number Diff line number Diff line change
@@ -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()
19 changes: 13 additions & 6 deletions netbox_cmdb/netbox_cmdb/template_content.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
from extras.plugins import PluginTemplateExtension


class Decommisioning(PluginTemplateExtension):
model = "dcim.device"

class DecommissioningBase(PluginTemplateExtension):
def buttons(self):
return (
f'<a href="#" hx-get="/plugins/cmdb/decommisioning/{self.context["object"].id}/delete" '
'hx-target="#htmx-modal-content" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#htmx-modal" '
f'<a href="/plugins/cmdb/decommissioning/{self.obj}/{self.context["object"].id}/delete" '
'class="btn btn-sm btn-danger">Decommission</a>'
)


template_extensions = [Decommisioning]
class DeviceDecommissioning(DecommissioningBase):
model = "dcim.device"
obj = "device"


class SiteDecommissioning(DecommissioningBase):
model = "dcim.site"
obj = "site"


template_extensions = [DeviceDecommissioning, SiteDecommissioning]
43 changes: 0 additions & 43 deletions netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning.html

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% extends "base/layout.html" %}
{% block title %}
{{ object.name }} decommissioning
{% endblock %}

{% block content-wrapper %}
<div class="border-top">
<div class="container mt-5 mb-3 border rounded">

<div id="_status" class="mt-3 mb-3 fw-bold"></div>
<div id="_error"></div>
<div id="_history" class="ps-3"></div>
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% else %}
<div id="_content" class="mb-3">
<p class="fw-bold">
<span class="text-danger">Warning:</span> this action will remove both
CMDB assets and DCIM of concerned asset(s)
</p>
<form
hx-target="#_content"
hx-post="/plugins/cmdb/decommissioning/{{ object_type }}/{{ object.id }}/delete"
>
{% csrf_token %}
<button class="btn btn-sm btn-danger">Confirm</button>
</form>
</div>
{% endif %}

</div>
</div>
{% endblock content-wrapper%}
Loading
Loading