Skip to content

Commit 79bdd05

Browse files
authored
Merge pull request #43 from kpetremann/site_decomm
feat: site decommissioning
2 parents 9964d79 + b492af1 commit 79bdd05

File tree

10 files changed

+476
-161
lines changed

10 files changed

+476
-161
lines changed
+134-22
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,169 @@
1+
from dcim.models import Device, Site
12
from django.db import transaction
2-
from django.db.models import Q
3+
from django.http import StreamingHttpResponse
34
from drf_yasg.utils import swagger_auto_schema
45
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
56
from rest_framework import serializers, status
67
from rest_framework.response import Response
78
from rest_framework.views import APIView
89

9-
from netbox_cmdb.models.bgp import BGPPeerGroup, BGPSession, DeviceBGPSession
10-
from netbox_cmdb.models.bgp_community_list import BGPCommunityList
11-
from netbox_cmdb.models.prefix_list import PrefixList
12-
from netbox_cmdb.models.route_policy import RoutePolicy
13-
from netbox_cmdb.models.snmp import SNMP
10+
from netbox_cmdb.helpers import cleaning
1411

1512

16-
class DeleteAllCMDBObjectsRelatedToDeviceSerializer(serializers.Serializer):
13+
class DeviceDecommissioningBaseSerializer(serializers.Serializer):
1714
device_name = serializers.CharField()
1815

1916

20-
class DeleteAllCMDBObjectsRelatedToDevice(APIView):
17+
class DeviceCMDBDecommissioningAPIView(APIView):
2118

2219
permission_classes = [IsAuthenticatedOrLoginNotRequired]
2320

2421
@swagger_auto_schema(
25-
request_body=DeleteAllCMDBObjectsRelatedToDeviceSerializer,
22+
request_body=DeviceDecommissioningBaseSerializer,
2623
responses={
2724
status.HTTP_200_OK: "Objects related to device have been deleted successfully",
2825
status.HTTP_400_BAD_REQUEST: "Bad Request: Device name is required",
26+
status.HTTP_404_NOT_FOUND: "Bad Request: Device not found",
2927
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error: Something went wrong on the server",
3028
},
3129
)
32-
def post(self, request):
30+
def delete(self, request):
3331
device_name = request.data.get("device_name", None)
3432
if device_name is None:
3533
return Response(
36-
{"error": "Device name is required"}, status=status.HTTP_400_BAD_REQUEST
34+
{"error": "device_name is required"}, status=status.HTTP_400_BAD_REQUEST
35+
)
36+
37+
devices = Device.objects.filter(name=device_name)
38+
device_ids = [dev.id for dev in devices]
39+
if not device_ids:
40+
return Response(
41+
{"error": "no matching devices found"}, status=status.HTTP_404_NOT_FOUND
3742
)
3843

3944
try:
4045
with transaction.atomic():
41-
# Delete objects in reverse order of dependencies
42-
BGPSession.objects.filter(
43-
Q(peer_a__device__name=device_name) | Q(peer_b__device__name=device_name)
44-
).delete()
45-
DeviceBGPSession.objects.filter(device__name=device_name).delete()
46-
BGPPeerGroup.objects.filter(device__name=device_name).delete()
47-
RoutePolicy.objects.filter(device__name=device_name).delete()
48-
PrefixList.objects.filter(device__name=device_name).delete()
49-
BGPCommunityList.objects.filter(device_name=device_name).delete()
50-
SNMP.objects.filter(device__name=device_name).delete()
46+
deleted = cleaning.clean_cmdb_for_devices(device_ids)
5147
except Exception as e:
5248
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
5349

5450
return Response(
55-
{"message": f"Objects related to device {device_name} have been deleted successfully"},
51+
{
52+
"message": f"CMDB cleaned for {device_name}",
53+
"deleted": deleted,
54+
},
5655
status=status.HTTP_200_OK,
5756
)
57+
58+
59+
class DeviceDecommissioningAPIView(APIView):
60+
61+
permission_classes = [IsAuthenticatedOrLoginNotRequired]
62+
63+
@swagger_auto_schema(
64+
request_body=DeviceDecommissioningBaseSerializer,
65+
responses={
66+
status.HTTP_200_OK: "Objects related to device have been deleted successfully",
67+
status.HTTP_400_BAD_REQUEST: "Bad Request: Device name is required",
68+
status.HTTP_404_NOT_FOUND: "Bad Request: Device not found",
69+
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error: Something went wrong on the server",
70+
},
71+
)
72+
def delete(self, request):
73+
device_name = request.data.get("device_name", None)
74+
if device_name is None:
75+
return Response(
76+
{"error": "device_name is required"}, status=status.HTTP_400_BAD_REQUEST
77+
)
78+
79+
devices = Device.objects.filter(name=device_name)
80+
device_ids = [dev.id for dev in devices]
81+
if not device_ids:
82+
return Response(
83+
{"error": "no matching devices found"}, status=status.HTTP_404_NOT_FOUND
84+
)
85+
86+
try:
87+
with transaction.atomic():
88+
deleted = cleaning.clean_cmdb_for_devices(device_ids)
89+
for device in devices:
90+
device.delete()
91+
except Exception as e:
92+
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
93+
94+
return Response(
95+
{
96+
"message": f"{device_name} decommissionned",
97+
"deleted": deleted,
98+
},
99+
status=status.HTTP_200_OK,
100+
)
101+
102+
103+
class SiteDecommissioningSerializer(serializers.Serializer):
104+
site_name = serializers.CharField()
105+
106+
107+
class SiteDecommissioningAPIView(APIView):
108+
109+
permission_classes = [IsAuthenticatedOrLoginNotRequired]
110+
111+
@swagger_auto_schema(
112+
request_body=SiteDecommissioningSerializer,
113+
responses={
114+
status.HTTP_200_OK: "Site have been deleted successfully",
115+
status.HTTP_400_BAD_REQUEST: "Bad Request: Site name is required",
116+
status.HTTP_404_NOT_FOUND: "Bad Request: Site not found",
117+
status.HTTP_500_INTERNAL_SERVER_ERROR: "Internal Server Error: Something went wrong on the server",
118+
},
119+
)
120+
def delete(self, request):
121+
site_name = request.data.get("site_name", None)
122+
if site_name is None:
123+
return Response({"error": "site_name is required"}, status=status.HTTP_400_BAD_REQUEST)
124+
125+
try:
126+
site = Site.objects.get(name=site_name)
127+
except Site.DoesNotExist:
128+
return Response({"error": "site not found"}, status=status.HTTP_404_NOT_FOUND)
129+
except Exception:
130+
return Response(
131+
{"error": "internal server error"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
132+
)
133+
134+
devices = Device.objects.filter(site=site.id)
135+
136+
def _start():
137+
CHUNK_SIZE = 20
138+
device_ids = [dev.id for dev in devices]
139+
for i in range(0, len(device_ids), CHUNK_SIZE):
140+
chunk = device_ids[i : i + CHUNK_SIZE]
141+
try:
142+
with transaction.atomic():
143+
cleaning.clean_cmdb_for_devices(chunk)
144+
for dev in devices[i : i + CHUNK_SIZE]:
145+
dev.delete()
146+
yield f'{{"deleted": {[dev.name for dev in devices[i:i+CHUNK_SIZE]]}}}\n\n'
147+
148+
except Exception as e:
149+
StreamingHttpResponse.status_code = 500
150+
msg = {"error": str(e)}
151+
yield f"{msg}\n\n"
152+
return
153+
154+
try:
155+
with transaction.atomic():
156+
cleaning.clean_site_topology(site)
157+
yield "{{'message': 'topology cleaned'}}\n\n"
158+
except Exception as e:
159+
StreamingHttpResponse.status_code = 500
160+
msg = {"error": str(e)}
161+
yield f"{msg}\n\n"
162+
return
163+
164+
msg = {
165+
"message": f"site {site_name} has been deleted successfully",
166+
}
167+
yield f"{msg}\n\n"
168+
169+
return StreamingHttpResponse(_start(), content_type="text/plain")

netbox_cmdb/netbox_cmdb/api/urls.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
from netbox_cmdb.api.prefix_list.views import PrefixListViewSet
1414
from netbox_cmdb.api.route_policy.views import RoutePolicyViewSet
1515
from netbox_cmdb.api.snmp.views import SNMPCommunityViewSet, SNMPViewSet
16-
from netbox_cmdb.api.cmdb.views import DeleteAllCMDBObjectsRelatedToDevice
16+
from netbox_cmdb.api.cmdb.views import (
17+
DeviceCMDBDecommissioningAPIView,
18+
DeviceDecommissioningAPIView,
19+
SiteDecommissioningAPIView,
20+
)
1721

1822
router = NetBoxRouter()
1923

@@ -35,9 +39,19 @@
3539
name="asns-available-asn",
3640
),
3741
path(
38-
"cmdb/delete-all-objects/",
39-
DeleteAllCMDBObjectsRelatedToDevice.as_view(),
40-
name="asns-available-asn",
42+
"management/device-cmdb-decommissioning/",
43+
DeviceCMDBDecommissioningAPIView.as_view(),
44+
name="device-cmdb-decommissioning",
45+
),
46+
path(
47+
"management/device-decommissioning/",
48+
DeviceDecommissioningAPIView.as_view(),
49+
name="device-decommissioning",
50+
),
51+
path(
52+
"management/site-decommissioning/",
53+
SiteDecommissioningAPIView.as_view(),
54+
name="site-decommissioning",
4155
),
4256
]
4357
urlpatterns += router.urls
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from dcim.models import Location, Rack
2+
from django.db.models import Q
3+
4+
from netbox_cmdb.models.bgp import BGPPeerGroup, BGPSession, DeviceBGPSession
5+
from netbox_cmdb.models.bgp_community_list import BGPCommunityList
6+
from netbox_cmdb.models.prefix_list import PrefixList
7+
from netbox_cmdb.models.route_policy import RoutePolicy
8+
from netbox_cmdb.models.snmp import SNMP
9+
10+
11+
def clean_cmdb_for_devices(device_ids: list[int]):
12+
deleted_objects = {
13+
"bgp_sessions": [],
14+
"device_bgp_sessions": [],
15+
"bgp_peer_groups": [],
16+
"route_policies": [],
17+
"prefix_lists": [],
18+
"bgp_community_lists": [],
19+
"snmp": [],
20+
}
21+
22+
bgp_sessions = BGPSession.objects.filter(
23+
Q(peer_a__device__id__in=device_ids) | Q(peer_b__device__id__in=device_ids)
24+
)
25+
device_bgp_sessions = DeviceBGPSession.objects.filter(device__id__in=device_ids)
26+
bgp_peer_groups = BGPPeerGroup.objects.filter(device__id__in=device_ids)
27+
route_policies = RoutePolicy.objects.filter(device__id__in=device_ids)
28+
prefix_lists = PrefixList.objects.filter(device__id__in=device_ids)
29+
bgp_community_lists = BGPCommunityList.objects.filter(device__id__in=device_ids)
30+
snmp = SNMP.objects.filter(device__id__in=device_ids)
31+
32+
deleted_objects["bgp_sessions"] = [str(val) for val in list(bgp_sessions)]
33+
deleted_objects["device_bgp_sessions"] = [str(val) for val in list(device_bgp_sessions)]
34+
deleted_objects["bgp_peer_groups"] = [str(val) for val in list(bgp_peer_groups)]
35+
deleted_objects["route_policies"] = [str(val) for val in list(route_policies)]
36+
deleted_objects["prefix_lists"] = [str(val) for val in list(prefix_lists)]
37+
deleted_objects["bgp_community_lists"] = [str(val) for val in list(bgp_community_lists)]
38+
deleted_objects["snmp"] = [str(val) for val in list(snmp)]
39+
40+
bgp_sessions.delete()
41+
device_bgp_sessions.delete()
42+
bgp_peer_groups.delete()
43+
route_policies.delete()
44+
prefix_lists.delete()
45+
bgp_community_lists.delete()
46+
snmp.delete()
47+
48+
return deleted_objects
49+
50+
51+
def clean_site_topology(site):
52+
racks = Rack.objects.filter(site=site.id)
53+
racks.delete()
54+
55+
locations = Location.objects.filter(site=site.id)
56+
locations.delete()
57+
58+
site.delete()
+13-6
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
from extras.plugins import PluginTemplateExtension
22

33

4-
class Decommisioning(PluginTemplateExtension):
5-
model = "dcim.device"
6-
4+
class DecommissioningBase(PluginTemplateExtension):
75
def buttons(self):
86
return (
9-
f'<a href="#" hx-get="/plugins/cmdb/decommisioning/{self.context["object"].id}/delete" '
10-
'hx-target="#htmx-modal-content" class="btn btn-sm btn-danger" data-bs-toggle="modal" data-bs-target="#htmx-modal" '
7+
f'<a href="/plugins/cmdb/decommissioning/{self.obj}/{self.context["object"].id}/delete" '
118
'class="btn btn-sm btn-danger">Decommission</a>'
129
)
1310

1411

15-
template_extensions = [Decommisioning]
12+
class DeviceDecommissioning(DecommissioningBase):
13+
model = "dcim.device"
14+
obj = "device"
15+
16+
17+
class SiteDecommissioning(DecommissioningBase):
18+
model = "dcim.site"
19+
obj = "site"
20+
21+
22+
template_extensions = [DeviceDecommissioning, SiteDecommissioning]

netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/decommissioning.html

-43
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{% extends "base/layout.html" %}
2+
{% block title %}
3+
{{ object.name }} decommissioning
4+
{% endblock %}
5+
6+
{% block content-wrapper %}
7+
<div class="border-top">
8+
<div class="container mt-5 mb-3 border rounded">
9+
10+
<div id="_status" class="mt-3 mb-3 fw-bold"></div>
11+
<div id="_error"></div>
12+
<div id="_history" class="ps-3"></div>
13+
{% if error %}
14+
<div class="alert alert-danger">{{ error }}</div>
15+
{% else %}
16+
<div id="_content" class="mb-3">
17+
<p class="fw-bold">
18+
<span class="text-danger">Warning:</span> this action will remove both
19+
CMDB assets and DCIM of concerned asset(s)
20+
</p>
21+
<form
22+
hx-target="#_content"
23+
hx-post="/plugins/cmdb/decommissioning/{{ object_type }}/{{ object.id }}/delete"
24+
>
25+
{% csrf_token %}
26+
<button class="btn btn-sm btn-danger">Confirm</button>
27+
</form>
28+
</div>
29+
{% endif %}
30+
31+
</div>
32+
</div>
33+
{% endblock content-wrapper%}

0 commit comments

Comments
 (0)