From 73d425594013347644db3e3ed20698ad9764a3cc Mon Sep 17 00:00:00 2001 From: Florian Domain Date: Tue, 26 Mar 2024 23:35:40 +0100 Subject: [PATCH 1/2] refactor: rework BGP sessions pages * reworked the BGP session list and BGP session object view * BGP session list can be now be filtered with some fields * BGP session object view is now fully showing all fields --- netbox_cmdb/netbox_cmdb/choices.py | 6 +- netbox_cmdb/netbox_cmdb/filtersets.py | 41 +++- netbox_cmdb/netbox_cmdb/forms.py | 35 ++- netbox_cmdb/netbox_cmdb/models/bgp.py | 13 +- netbox_cmdb/netbox_cmdb/navigation.py | 1 + netbox_cmdb/netbox_cmdb/tables.py | 26 +- .../templates/netbox_cmdb/bgpsession.html | 228 ++++++++++++++++++ netbox_cmdb/netbox_cmdb/urls.py | 3 + netbox_cmdb/netbox_cmdb/views.py | 46 +++- 9 files changed, 368 insertions(+), 31 deletions(-) create mode 100644 netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/bgpsession.html diff --git a/netbox_cmdb/netbox_cmdb/choices.py b/netbox_cmdb/netbox_cmdb/choices.py index 2cd9454..32ddd66 100644 --- a/netbox_cmdb/netbox_cmdb/choices.py +++ b/netbox_cmdb/netbox_cmdb/choices.py @@ -25,9 +25,9 @@ class AssetMonitoringStateChoices(ChoiceSet): DISABLED = "disabled" CHOICES = ( - (CRITICAL, "Critical"), - (WARNING, "Warning"), - (DISABLED, "Disabled"), + (CRITICAL, "Critical", "red"), + (WARNING, "Warning", "orange"), + (DISABLED, "Disabled", "gray"), ) diff --git a/netbox_cmdb/netbox_cmdb/filtersets.py b/netbox_cmdb/netbox_cmdb/filtersets.py index f404757..05d5933 100644 --- a/netbox_cmdb/netbox_cmdb/filtersets.py +++ b/netbox_cmdb/netbox_cmdb/filtersets.py @@ -1,10 +1,9 @@ import django_filters from django.db.models import Q -from netbox.filtersets import ChangeLoggedModelFilterSet from tenancy.filtersets import TenancyFilterSet -from tenancy.models import Tenant from utilities.filters import MultiValueCharFilter +from netbox.filtersets import ChangeLoggedModelFilterSet from netbox_cmdb.models.bgp import ASN, BGPPeerGroup, BGPSession device_location_filterset = [ @@ -13,6 +12,8 @@ "device__site__group__name", "device__site__region__name", "device__rack__name", + "device__site__group_id", + "device__device_type_id", ] @@ -69,11 +70,21 @@ class BGPSessionFilterSet(ChangeLoggedModelFilterSet, TenancyFilterSet): label="device__site__group__name", ) + device__site__group_id = MultiValueCharFilter( + method="filter_device_location", + label="device__site__group", + ) + device__site__region__name = MultiValueCharFilter( method="filter_device_location", label="device__site__region__name", ) + device__device_type_id = MultiValueCharFilter( + method="filter_device_type", + label="device__device_type", + ) + local_address = MultiValueCharFilter( method="filter_peer_address", label="local_address", @@ -82,7 +93,13 @@ class BGPSessionFilterSet(ChangeLoggedModelFilterSet, TenancyFilterSet): class Meta: model = BGPSession exclude = ["__all__"] - fields = ["id", "device", "local_address"] + device_location_filterset + fields = [ + "id", + "device", + "local_address", + "state", + "monitoring_state", + ] + device_location_filterset def filter_peer_address(self, queryset, name, value): if len(value) > 2: @@ -120,11 +137,27 @@ def filter_device_location(self, queryset, name, value): queryset = queryset.filter(Q(**peer_a_lookup) | Q(**peer_b_lookup)) return queryset + def filter_device_type(self, queryset, name, value): + if len(value) > 2: + # a BGP session can't have more than 2 devices + return queryset.none() + + for val in value: + # we chain the querysets to get a single BGP session when 2 values are passed + peer_a_lookup = {f"peer_a__{name}": val} + peer_b_lookup = {f"peer_b__{name}": val} + + queryset = queryset.filter(Q(**peer_a_lookup) | Q(**peer_b_lookup)).distinct() + return queryset + def search(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter( - Q(peer_a__device__name__icontains=value) | Q(peer_b__device__name__icontains=value) + Q(peer_a__device__name__icontains=value) + | Q(peer_a__description__icontains=value) + | Q(peer_b__device__name__icontains=value) + | Q(peer_b__description__icontains=value) ).distinct() diff --git a/netbox_cmdb/netbox_cmdb/forms.py b/netbox_cmdb/netbox_cmdb/forms.py index 1a1cfe0..0538cb2 100644 --- a/netbox_cmdb/netbox_cmdb/forms.py +++ b/netbox_cmdb/netbox_cmdb/forms.py @@ -1,11 +1,16 @@ """Forms.""" + from dcim.models import Device +from dcim.models.devices import DeviceType +from dcim.models.sites import SiteGroup from django import forms +from django.utils.translation import gettext as _ from extras.models import Tag -from netbox.forms import NetBoxModelForm from utilities.forms import DynamicModelMultipleChoiceField -from utilities.forms.fields import DynamicModelChoiceField +from utilities.forms.fields import DynamicModelChoiceField, MultipleChoiceField +from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelForm +from netbox_cmdb.choices import AssetMonitoringStateChoices, AssetStateChoices from netbox_cmdb.models.bgp import ASN, BGPPeerGroup, BGPSession @@ -18,18 +23,26 @@ class Meta: class BGPSessionForm(NetBoxModelForm): - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), + class Meta: + model = BGPSession + fields = ["peer_a", "peer_b", "state", "monitoring_state"] + + +class BGPSessionFilterSetForm(NetBoxModelFilterSetForm): + device__site__group_id = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + label=_("Site"), required=False, ) + device__device_type_id = DynamicModelMultipleChoiceField( + queryset=DeviceType.objects.all(), + label=_("Device type"), + required=False, + ) + state = MultipleChoiceField(choices=AssetStateChoices, required=False) + monitoring_state = MultipleChoiceField(choices=AssetMonitoringStateChoices, required=False) - class Meta: - model = BGPSession - fields = [ - "peer_a", - "peer_b", - "tags", - ] + model = BGPSession class BGPPeerGroupForm(NetBoxModelForm): diff --git a/netbox_cmdb/netbox_cmdb/models/bgp.py b/netbox_cmdb/netbox_cmdb/models/bgp.py index 6ecf624..e2e4b30 100644 --- a/netbox_cmdb/netbox_cmdb/models/bgp.py +++ b/netbox_cmdb/netbox_cmdb/models/bgp.py @@ -4,12 +4,12 @@ from django.db import models from django.db.models import Q from django.urls import reverse -from netbox.models import ChangeLoggedModel from utilities.choices import ChoiceSet from utilities.querysets import RestrictedQuerySet +from netbox.models import ChangeLoggedModel +from netbox_cmdb.choices import AssetMonitoringStateChoices, AssetStateChoices from netbox_cmdb.constants import BGP_MAX_ASN, BGP_MIN_ASN -from netbox_cmdb.choices import AssetStateChoices, AssetMonitoringStateChoices from netbox_cmdb.models.circuit import Circuit @@ -357,3 +357,12 @@ def validate_unique(self, exclude=None): ) super().validate_unique(exclude) + + def get_state_color(self): + return AssetStateChoices.colors.get(self.state) + + def get_monitoring_state_color(self): + return AssetMonitoringStateChoices.colors.get(self.monitoring_state) + + def get_absolute_url(self): + return reverse("plugins:netbox_cmdb:bgpsession", args=[self.pk]) diff --git a/netbox_cmdb/netbox_cmdb/navigation.py b/netbox_cmdb/netbox_cmdb/navigation.py index 7fbca76..8a8e589 100644 --- a/netbox_cmdb/netbox_cmdb/navigation.py +++ b/netbox_cmdb/netbox_cmdb/navigation.py @@ -1,4 +1,5 @@ """Navigation (menu).""" + from extras.plugins import PluginMenuButton, PluginMenuItem from utilities.choices import ButtonColorChoices diff --git a/netbox_cmdb/netbox_cmdb/tables.py b/netbox_cmdb/netbox_cmdb/tables.py index 83291cd..10f72eb 100644 --- a/netbox_cmdb/netbox_cmdb/tables.py +++ b/netbox_cmdb/netbox_cmdb/tables.py @@ -1,7 +1,8 @@ """Tables.""" + import django_tables2 as tables -from netbox.tables import NetBoxTable +from netbox.tables import NetBoxTable, columns from netbox_cmdb.models.bgp import ASN, BGPPeerGroup, BGPSession @@ -15,15 +16,28 @@ class Meta(NetBoxTable.Meta): class BGPSessionTable(NetBoxTable): - peer_a = tables.Column() - peer_b = tables.Column() + id = tables.Column(linkify=True) + peer_a__device = tables.Column(verbose_name="Device A") + peer_a__description = tables.Column(verbose_name="Device A description") + peer_a__local_address = tables.Column(verbose_name="Device A local address") + peer_b__device = tables.Column(verbose_name="Device B") + peer_b__description = tables.Column(verbose_name="Device B description") + peer_b__local_address = tables.Column(verbose_name="Device B local address") + state = columns.ChoiceFieldColumn() + monitoring_state = columns.ChoiceFieldColumn() class Meta(NetBoxTable.Meta): model = BGPSession fields = ( - "pk", - "peer_a", - "peer_b", + "id", + "peer_a__device", + "peer_a__description", + "peer_a__local_address", + "peer_b__device", + "peer_b__description", + "peer_b__local_address", + "state", + "monitoring_state", ) diff --git a/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/bgpsession.html b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/bgpsession.html new file mode 100644 index 0000000..961113d --- /dev/null +++ b/netbox_cmdb/netbox_cmdb/templates/netbox_cmdb/bgpsession.html @@ -0,0 +1,228 @@ +{% extends 'generic/object.html' %} {% load helpers %} +{% block title %}{{ object.peer_a.device }} <> {{ object.peer_b.device }}{% endblock %} +{% block content %} +
+
+
+
Peer A
+
+ + + + + + + + + + + + + + + + + + + + + + +
Device + {{ object.peer_a.device|linkify}}
Description + {{ object.peer_a.description }}
Local address + {{ object.peer_a.local_address|linkify}}
Local ASN + {{ object.peer_a.local_asn|linkify}}
Route Policy in + {{ object.peer_a.route_policy_in|placeholder}}
Route Policy out + {{ object.peer_a.route_policy_out|placeholder}}
Maximum prefixes + {{ object.peer_a.maximum_prefixes }}
+
AFI/SAFI
+ + + + + + + + + + {% for afisafi in object.peer_b.afi_safis.all %} + + + + + + {% endfor %} + +
NameRoute Policy inRoute Policy out
{{ afisafi.afi_safi_name }}{{ afisafi.route_policy_in.name|placeholder}}{{ afisafi.route_policy_out.name|placeholder }}
+
+
+
+
+
+
BGP Session
+
+ + + + + + + + + + + + + + + + + + + + + +
State{% badge object.get_state_display bg_color=object.get_state_color%}
Monitoring State{% badge object.get_monitoring_state_display bg_color=object.get_monitoring_state_color%}
Password{{ object.password|placeholder }}
Circuit{{ object.circuit|placeholder }}
Tenant{{ object.tenant|placeholder|linkify }}
+
+
+
+
+
+
Peer B
+
+ + + + + + + + + + + + + + + + + + + + + + +
Device + {{ object.peer_b.device|linkify}}
Description + {{ object.peer_b.description }}
Local address + {{ object.peer_b.local_address|linkify}}
Local ASN + {{ object.peer_b.local_asn|linkify}}
Route Policy in + {{ object.peer_b.route_policy_in|placeholder}}
Route Policy out + {{ object.peer_b.route_policy_out|placeholder}}
Maximum prefixes + {{ object.peer_b.maximum_prefixes }}
+
AFI/SAFI
+ + + + + + + + + + {% for afisafi in object.peer_b.afi_safis.all %} + + + + + + {% endfor %} + +
NameRoute Policy inRoute Policy out
{{ afisafi.afi_safi_name }}{{ afisafi.route_policy_in.name}}{{ afisafi.route_policy_out.name }}
+
+
+
+
+
+
+ {% if object.peer_a.peer_group %} +
+
Peer-Group A
+
+ + + + + + + + + + + + + + + + + + + + + + +
PG Name + {{ object.peer_a.peer_group.name}}
Description + {{ object.peer_a.peer_group.description }}
Local address + {{ object.peer_a.peer_group.local_address|placeholder|linkify}}
Local ASN + {{ object.peer_a.peer_group.local_asn|placeholder|linkify}}
Route Policy in + {{ object.peer_a.peer_group.route_policy_in.name|placeholder}}
Route Policy out + {{ object.peer_a.peer_group.route_policy_out.name|placeholder}}
Maximum prefixes + {{ object.peer_a.peer_group.maximum_prefixes|placeholder }}
+
+
+ {% endif %} +
+
+
+
+ {% if object.peer_b.peer_group %} +
+
Peer-Group B
+
+ + + + + + + + + + + + + + + + + + + + + + +
PG Name + {{ object.peer_b.peer_group.name}}
Description + {{ object.peer_b.peer_group.description }}
Local address + {{ object.peer_b.peer_group.local_address|placeholder|linkify}}
Local ASN + {{ object.peer_b.peer_group.local_asn|placeholder|linkify}}
Route Policy in + {{ object.peer_b.peer_group.route_policy_in.name|placeholder}}
Route Policy out + {{ object.peer_b.peer_group.route_policy_out.name|placeholder}}
Maximum prefixes + {{ object.peer_b.peer_group.maximum_prefixes|placeholder }}
+
+
+ {% endif %} +
+{% endblock %} diff --git a/netbox_cmdb/netbox_cmdb/urls.py b/netbox_cmdb/netbox_cmdb/urls.py index a825d53..82b09f3 100644 --- a/netbox_cmdb/netbox_cmdb/urls.py +++ b/netbox_cmdb/netbox_cmdb/urls.py @@ -1,4 +1,5 @@ """URLs.""" + from django.urls import path from netbox.views.generic import ObjectChangeLogView, ObjectJournalView @@ -12,6 +13,7 @@ BGPPeerGroupEditView, BGPPeerGroupListView, BGPPeerGroupView, + BGPSessionBulkDeleteView, BGPSessionDeleteView, BGPSessionEditView, BGPSessionListView, @@ -51,6 +53,7 @@ BGPSessionDeleteView.as_view(), name="bgpsession_delete", ), + path("bgp-session/delete/", BGPSessionBulkDeleteView.as_view(), name="bgpsession_bulk_delete"), path( "bgp-session//changelog/", ObjectChangeLogView.as_view(), diff --git a/netbox_cmdb/netbox_cmdb/views.py b/netbox_cmdb/netbox_cmdb/views.py index 80756b6..5be9867 100644 --- a/netbox_cmdb/netbox_cmdb/views.py +++ b/netbox_cmdb/netbox_cmdb/views.py @@ -1,18 +1,24 @@ """Views.""" + from netbox.views.generic import ( ObjectDeleteView, ObjectEditView, ObjectListView, ObjectView, ) - +from netbox.views.generic.bulk_views import BulkDeleteView from netbox_cmdb.filtersets import ( ASNFilterSet, BGPPeerGroupFilterSet, BGPSessionFilterSet, ) -from netbox_cmdb.forms import ASNForm, BGPPeerGroupForm, BGPSessionForm -from netbox_cmdb.models.bgp import ASN, BGPPeerGroup, BGPSession +from netbox_cmdb.forms import ( + ASNForm, + BGPPeerGroupForm, + BGPSessionFilterSetForm, + BGPSessionForm, +) +from netbox_cmdb.models.bgp import ASN, BGPPeerGroup, BGPSession, DeviceBGPSession from netbox_cmdb.tables import ASNTable, BGPPeerGroupTable, BGPSessionTable @@ -42,8 +48,9 @@ class ASNView(ObjectView): class BGPSessionListView(ObjectListView): - queryset = BGPSession.objects.all() + queryset = BGPSession.objects.prefetch_related("peer_a", "peer_b").all() filterset = BGPSessionFilterSet + filterset_form = BGPSessionFilterSetForm table = BGPSessionTable template_name = "netbox_cmdb/bgpsession_list.html" @@ -53,14 +60,43 @@ class BGPSessionEditView(ObjectEditView): form = BGPSessionForm +class BGPSessionBulkDeleteView(BulkDeleteView): + queryset = BGPSession.objects.all() + filterset = BGPSessionFilterSet + table = BGPSessionTable + + class BGPSessionDeleteView(ObjectDeleteView): queryset = BGPSession.objects.all() class BGPSessionView(ObjectView): - queryset = BGPSession.objects.all() + queryset = BGPSession.objects.prefetch_related( + "peer_a", "peer_b", "peer_a__afi_safis", "peer_b__afi_safis" + ).all() template_name = "netbox_cmdb/bgpsession.html" + def get_extra_context(self, request, instance): + # Get AFI/SAFIS + peer_a_afi_safis = [] + peer_b_afi_safis = [] + if instance.peer_a.afi_safis is not None: + peer_a_afi_safis = instance.peer_a.afi_safis.all() + if instance.peer_b.afi_safis is not None: + peer_b_afi_safis = instance.peer_b.afi_safis.all() + return { + "peer_a_afi_safis": peer_a_afi_safis, + "peer_b_afi_safis": peer_b_afi_safis, + } + + +## DeviceBGPSession views + + +class DeviceBGPSessionListView(ObjectListView): + queryset = DeviceBGPSession.objects.all() + filterset = None + ## Peer groups views class BGPPeerGroupListView(ObjectListView): From 8d5e60ceb1234536d52ccab068e94da1c5e0c731 Mon Sep 17 00:00:00 2001 From: Florian Domain Date: Wed, 27 Mar 2024 09:06:03 +0100 Subject: [PATCH 2/2] style: black formatting --- netbox_cmdb/netbox_cmdb/__init__.py | 1 + netbox_cmdb/netbox_cmdb/admin.py | 1 + netbox_cmdb/netbox_cmdb/api/bgp_community_list/serializers.py | 1 + netbox_cmdb/netbox_cmdb/api/bgp_community_list/views.py | 1 + netbox_cmdb/netbox_cmdb/api/prefix_list/serializers.py | 1 + netbox_cmdb/netbox_cmdb/api/prefix_list/views.py | 1 + netbox_cmdb/netbox_cmdb/api/route_policy/serializers.py | 1 + netbox_cmdb/netbox_cmdb/models/prefix_list.py | 1 + netbox_cmdb/netbox_cmdb/version.py | 1 + 9 files changed, 9 insertions(+) diff --git a/netbox_cmdb/netbox_cmdb/__init__.py b/netbox_cmdb/netbox_cmdb/__init__.py index fc67c1f..427e71f 100644 --- a/netbox_cmdb/netbox_cmdb/__init__.py +++ b/netbox_cmdb/netbox_cmdb/__init__.py @@ -1,4 +1,5 @@ """Netbox CMDB plugin.""" + from extras.plugins import PluginConfig diff --git a/netbox_cmdb/netbox_cmdb/admin.py b/netbox_cmdb/netbox_cmdb/admin.py index 4c54dbd..7c32ae0 100644 --- a/netbox_cmdb/netbox_cmdb/admin.py +++ b/netbox_cmdb/netbox_cmdb/admin.py @@ -1,4 +1,5 @@ """Admin module.""" + from dcim.models import Device from django.contrib import admin from django.contrib.admin.options import StackedInline diff --git a/netbox_cmdb/netbox_cmdb/api/bgp_community_list/serializers.py b/netbox_cmdb/netbox_cmdb/api/bgp_community_list/serializers.py index eef2bfa..4565780 100644 --- a/netbox_cmdb/netbox_cmdb/api/bgp_community_list/serializers.py +++ b/netbox_cmdb/netbox_cmdb/api/bgp_community_list/serializers.py @@ -1,4 +1,5 @@ """Route Policy serializers.""" + from netbox_cmdb.api.common_serializers import CommonDeviceSerializer from netbox_cmdb.models.bgp_community_list import BGPCommunityList, BGPCommunityListTerm from rest_framework.serializers import ModelSerializer, ValidationError diff --git a/netbox_cmdb/netbox_cmdb/api/bgp_community_list/views.py b/netbox_cmdb/netbox_cmdb/api/bgp_community_list/views.py index 358baa8..3aefc94 100644 --- a/netbox_cmdb/netbox_cmdb/api/bgp_community_list/views.py +++ b/netbox_cmdb/netbox_cmdb/api/bgp_community_list/views.py @@ -1,4 +1,5 @@ """Route Policy views.""" + from netbox_cmdb import filtersets from netbox_cmdb.api.bgp_community_list.serializers import BGPCommunityListSerializer from netbox_cmdb.api.viewsets import CustomNetBoxModelViewSet diff --git a/netbox_cmdb/netbox_cmdb/api/prefix_list/serializers.py b/netbox_cmdb/netbox_cmdb/api/prefix_list/serializers.py index 0b1d5ed..8280101 100644 --- a/netbox_cmdb/netbox_cmdb/api/prefix_list/serializers.py +++ b/netbox_cmdb/netbox_cmdb/api/prefix_list/serializers.py @@ -1,4 +1,5 @@ """Prefix list serializers.""" + from rest_framework.serializers import ModelSerializer, ValidationError from netbox_cmdb.api.common_serializers import CommonDeviceSerializer diff --git a/netbox_cmdb/netbox_cmdb/api/prefix_list/views.py b/netbox_cmdb/netbox_cmdb/api/prefix_list/views.py index 0f1d0f6..5bab498 100644 --- a/netbox_cmdb/netbox_cmdb/api/prefix_list/views.py +++ b/netbox_cmdb/netbox_cmdb/api/prefix_list/views.py @@ -1,4 +1,5 @@ """Route Policy views.""" + from netbox_cmdb import filtersets from netbox_cmdb.api.prefix_list.serializers import PrefixListSerializer from netbox_cmdb.api.viewsets import CustomNetBoxModelViewSet diff --git a/netbox_cmdb/netbox_cmdb/api/route_policy/serializers.py b/netbox_cmdb/netbox_cmdb/api/route_policy/serializers.py index 2cbb8ee..51e5119 100644 --- a/netbox_cmdb/netbox_cmdb/api/route_policy/serializers.py +++ b/netbox_cmdb/netbox_cmdb/api/route_policy/serializers.py @@ -1,4 +1,5 @@ """Route Policy serializers.""" + from django.core.exceptions import ValidationError from netbox.api.serializers import WritableNestedSerializer from rest_framework import serializers diff --git a/netbox_cmdb/netbox_cmdb/models/prefix_list.py b/netbox_cmdb/netbox_cmdb/models/prefix_list.py index 731aa65..6567f5e 100644 --- a/netbox_cmdb/netbox_cmdb/models/prefix_list.py +++ b/netbox_cmdb/netbox_cmdb/models/prefix_list.py @@ -1,4 +1,5 @@ """Prefix list models.""" + from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse diff --git a/netbox_cmdb/netbox_cmdb/version.py b/netbox_cmdb/netbox_cmdb/version.py index 6613a87..4bb901c 100644 --- a/netbox_cmdb/netbox_cmdb/version.py +++ b/netbox_cmdb/netbox_cmdb/version.py @@ -1,2 +1,3 @@ """Version.""" + __version__ = "0.1.0"