Skip to content

Commit

Permalink
Merge pull request #43 from kpetremann/site_decomm
Browse files Browse the repository at this point in the history
feat: site decommissioning
  • Loading branch information
kpetremann authored Aug 22, 2024
2 parents 9964d79 + b492af1 commit 79bdd05
Show file tree
Hide file tree
Showing 10 changed files with 476 additions and 161 deletions.
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

0 comments on commit 79bdd05

Please sign in to comment.