diff --git a/netbox_bgp/api/serializers.py b/netbox_bgp/api/serializers.py index 0652754..595fd23 100644 --- a/netbox_bgp/api/serializers.py +++ b/netbox_bgp/api/serializers.py @@ -1,11 +1,22 @@ -from rest_framework.serializers import HyperlinkedIdentityField, ValidationError +from django.contrib.contenttypes.models import ContentType +from drf_spectacular.utils import extend_schema_field +from ipam.constants import VLANGROUP_SCOPE_TYPES +from rest_framework.serializers import ( + CharField, + JSONField, + HyperlinkedIdentityField, + IntegerField, + SerializerMethodField, + ValidationError +) from rest_framework.relations import PrimaryKeyRelatedField -from netbox.api.fields import ChoiceField, SerializedPKRelatedField +from netbox.api.fields import ChoiceField, ContentTypeField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer -from ipam.api.serializers import IPAddressSerializer, ASNSerializer, PrefixSerializer +from ipam.api.serializers import IPAddressSerializer, ASNSerializer, PrefixSerializer, VRFSerializer from tenancy.api.serializers import TenantSerializer from dcim.api.serializers import SiteSerializer, DeviceSerializer from ipam.api.field_serializers import IPNetworkField +from utilities.api import get_serializer_for_model from virtualization.api.serializers import VirtualMachineSerializer from netbox_bgp.models import ( @@ -19,15 +30,27 @@ CommunityList, CommunityListRule, ASPathList, - ASPathListRule + ASPathListRule, + Redistributing ) -from netbox_bgp.choices import CommunityStatusChoices, SessionStatusChoices +from netbox_bgp.choices import CommunityStatusChoices, SessionStatusChoices, RedistributeSourceChoices class ASPathListSerializer(NetBoxModelSerializer): url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:aspathlist-detail") + scope_type = ContentTypeField( + queryset=ContentType.objects.filter( + model__in=VLANGROUP_SCOPE_TYPES + ), + allow_null=True, + required=False, + default=None + ) + scope_id = IntegerField(allow_null=True, required=False, default=None) + scope = SerializerMethodField(read_only=True) + class Meta: model = ASPathList fields = [ @@ -36,12 +59,22 @@ class Meta: "name", "display", "description", + "scope_type", + "scope_id", + "scope", "tags", "custom_fields", "comments", ] brief_fields = ("id", "url", "display", "name", "description") + @extend_schema_field(JSONField(allow_null=True)) + def get_scope(self, obj): + if obj.scope_id is None: + return None + serializer = get_serializer_for_model(obj.scope) + context = {"request": self.context["request"]} + return serializer(obj.scope, nested=True, context=context).data class ASPathListRuleSerializer(NetBoxModelSerializer): aspath_list = ASPathListSerializer(nested=True) @@ -68,6 +101,17 @@ class Meta: class RoutingPolicySerializer(NetBoxModelSerializer): url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:routingpolicy-detail") + scope_type = ContentTypeField( + queryset=ContentType.objects.filter( + model__in=VLANGROUP_SCOPE_TYPES + ), + allow_null=True, + required=False, + default=None + ) + scope_id = IntegerField(allow_null=True, required=False, default=None) + scope = SerializerMethodField(read_only=True) + class Meta: model = RoutingPolicy fields = ( @@ -77,16 +121,38 @@ class Meta: "name", "description", "weight", + "scope_type", + "scope_id", + "scope", + "redistributing", "tags", "custom_fields", "comments", ) brief_fields = ("id", "url", "display", "name", "description") + @extend_schema_field(JSONField(allow_null=True)) + def get_scope(self, obj): + if obj.scope_id is None: + return None + serializer = get_serializer_for_model(obj.scope) + context = {"request": self.context["request"]} + return serializer(obj.scope, nested=True, context=context).data class PrefixListSerializer(NetBoxModelSerializer): url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:prefixlist-detail") + scope_type = ContentTypeField( + queryset=ContentType.objects.filter( + model__in=VLANGROUP_SCOPE_TYPES + ), + allow_null=True, + required=False, + default=None + ) + scope_id = IntegerField(allow_null=True, required=False, default=None) + scope = SerializerMethodField(read_only=True) + class Meta: model = PrefixList fields = ( @@ -95,6 +161,9 @@ class Meta: "name", "display", "description", + "scope_type", + "scope_id", + "scope", "family", "tags", "custom_fields", @@ -102,10 +171,27 @@ class Meta: ) brief_fields = ("id", "url", "display", "name", "description") + @extend_schema_field(JSONField(allow_null=True)) + def get_scope(self, obj): + if obj.scope_id is None: + return None + serializer = get_serializer_for_model(obj.scope) + context = {"request": self.context["request"]} + return serializer(obj.scope, nested=True, context=context).data class BGPPeerGroupSerializer(NetBoxModelSerializer): url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:bgppeergroup-detail") + scope_type = ContentTypeField( + queryset=ContentType.objects.filter( + model__in=VLANGROUP_SCOPE_TYPES + ), + allow_null=True, + required=False, + default=None + ) + scope_id = IntegerField(allow_null=True, required=False, default=None) + scope = SerializerMethodField(read_only=True) import_policies = SerializedPKRelatedField( queryset=RoutingPolicy.objects.all(), serializer=RoutingPolicySerializer, @@ -131,6 +217,9 @@ class Meta: "display", "name", "description", + "scope_type", + "scope_id", + "scope", "import_policies", "export_policies", "comments", @@ -138,6 +227,13 @@ class Meta: ) brief_fields = ("id", "url", "display", "name", "description") + @extend_schema_field(JSONField(allow_null=True)) + def get_scope(self, obj): + if obj.scope_id is None: + return None + serializer = get_serializer_for_model(obj.scope) + context = {"request": self.context["request"]} + return serializer(obj.scope, nested=True, context=context).data class BGPSessionSerializer(NetBoxModelSerializer): url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:bgpsession-detail") @@ -257,6 +353,17 @@ class Meta: class CommunityListSerializer(NetBoxModelSerializer): url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:communitylist-detail") + scope_type = ContentTypeField( + queryset=ContentType.objects.filter( + model__in=VLANGROUP_SCOPE_TYPES + ), + allow_null=True, + required=False, + default=None + ) + scope_id = IntegerField(allow_null=True, required=False, default=None) + scope = SerializerMethodField(read_only=True) + class Meta: model = CommunityList fields = ( @@ -265,12 +372,22 @@ class Meta: "name", "display", "description", + "scope_type", + "scope_id", + "scope", "tags", "custom_fields", "comments", ) brief_fields = ("id", "url", "display", "name", "description") + @extend_schema_field(JSONField(allow_null=True)) + def get_scope(self, obj): + if obj.scope_id is None: + return None + serializer = get_serializer_for_model(obj.scope) + context = {"request": self.context["request"]} + return serializer(obj.scope, nested=True, context=context).data class CommunityListRuleSerializer(NetBoxModelSerializer): community_list = CommunityListSerializer(nested=True) @@ -388,3 +505,56 @@ class Meta: ) brief_fields = ("id", "display", "description") + +class RedistributingSerializer(NetBoxModelSerializer): + url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:redistributing-detail") + name = CharField(required=True, allow_null=False) + scope_type = ContentTypeField( + queryset=ContentType.objects.filter( + model__in=VLANGROUP_SCOPE_TYPES + ), + allow_null=True, + required=False, + default=None + ) + scope_id = IntegerField(allow_null=True, required=False, default=None) + scope = SerializerMethodField(read_only=True) + vrf = VRFSerializer(nested=True, required=False, allow_null=True) + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + device = DeviceSerializer(nested=True, required=False, allow_null=True) + virtualmachine = VirtualMachineSerializer(nested=True, required=False, allow_null=True) + redistribute_source = ChoiceField(choices=RedistributeSourceChoices, required=True, allow_null=False) + redistribute_policy = RoutingPolicySerializer(required=False, allow_null=True) + + class Meta: + model = Redistributing + fields = ( + "id", + "url", + "tags", + "custom_fields", + "display", + "scope_type", + "scope_id", + "scope", + "vrf", + "tenant", + "device", + "virtualmachine", + "redistribute_source", + "redistribute_policy", + "created", + "last_updated", + "name", + "description", + "comments", + ) + brief_fields = ("id", "url", "name", "display", "device", "virtualmachine", "redistribute_source", "redistribute_policy") + + @extend_schema_field(JSONField(allow_null=True)) + def get_scope(self, obj): + if obj.scope_id is None: + return None + serializer = get_serializer_for_model(obj.scope) + context = {"request": self.context["request"]} + return serializer(obj.scope, nested=True, context=context).data diff --git a/netbox_bgp/api/urls.py b/netbox_bgp/api/urls.py index c8b8ddf..f48e4c4 100644 --- a/netbox_bgp/api/urls.py +++ b/netbox_bgp/api/urls.py @@ -4,7 +4,7 @@ BGPSessionViewSet, RoutingPolicyViewSet, BGPPeerGroupViewSet, CommunityViewSet, PrefixListViewSet, PrefixListRuleViewSet, RoutingPolicyRuleViewSet, CommunityListViewSet, CommunityListRuleViewSet, RootView, - ASPathListViewSet, ASPathListRuleViewSet + ASPathListViewSet, ASPathListRuleViewSet, RedistributingViewSet, ) @@ -23,5 +23,6 @@ router.register('community-list-rule', CommunityListRuleViewSet) router.register('aspath-list', ASPathListViewSet) router.register('aspath-list-rule', ASPathListRuleViewSet) +router.register('redistributing', RedistributingViewSet, 'redistributing') urlpatterns = router.urls diff --git a/netbox_bgp/api/views.py b/netbox_bgp/api/views.py index c23219b..8997099 100644 --- a/netbox_bgp/api/views.py +++ b/netbox_bgp/api/views.py @@ -5,19 +5,19 @@ BGPSessionSerializer, RoutingPolicySerializer, BGPPeerGroupSerializer, CommunitySerializer, PrefixListSerializer, PrefixListRuleSerializer, RoutingPolicyRuleSerializer, CommunityListSerializer, CommunityListRuleSerializer, - ASPathListSerializer, ASPathListRuleSerializer + ASPathListSerializer, ASPathListRuleSerializer, RedistributingSerializer, ) from netbox_bgp.models import ( BGPSession, RoutingPolicy, BGPPeerGroup, Community, PrefixList, PrefixListRule, RoutingPolicyRule, CommunityList, CommunityListRule, - ASPathList, ASPathListRule + ASPathList, ASPathListRule, Redistributing, ) from netbox_bgp.filtersets import ( BGPSessionFilterSet, RoutingPolicyFilterSet, BGPPeerGroupFilterSet, CommunityFilterSet, PrefixListFilterSet, PrefixListRuleFilterSet, RoutingPolicyRuleFilterSet, CommunityListFilterSet, CommunityListRuleFilterSet, - ASPathListFilterSet, ASPathListRuleFilterSet + ASPathListFilterSet, ASPathListRuleFilterSet, RedistributingFilterSet, ) class RootView(APIRootView): @@ -89,3 +89,9 @@ class ASPathListRuleViewSet(NetBoxModelViewSet): queryset = ASPathListRule.objects.all() serializer_class = ASPathListRuleSerializer filterset_class = ASPathListRuleFilterSet + + +class RedistributingViewSet(NetBoxModelViewSet): + queryset = Redistributing.objects.all() + serializer_class = RedistributingSerializer + filterset_class = RedistributingFilterSet diff --git a/netbox_bgp/choices.py b/netbox_bgp/choices.py index 03d849e..7580dea 100644 --- a/netbox_bgp/choices.py +++ b/netbox_bgp/choices.py @@ -31,6 +31,28 @@ class SessionStatusChoices(ChoiceSet): ] +class RedistributeSourceChoices(ChoiceSet): + key = "Redistributing.redistribute_source" + + REDISTRIBUTE_DIRECT = 'Direct' + REDISTRIBUTE_STATIC = 'Static' + REDISTRIBUTE_OSPF = 'OSPF' + REDISTRIBUTE_ISIS = 'IS-IS' + REDISTRIBUTE_EIGRP = 'EIGRP' + REDISTRIBUTE_RIP = 'RIP' + REDISTRIBUTE_BGP = 'BGP' + + CHOICES = [ + (REDISTRIBUTE_DIRECT, 'Direct', 'cyan'), + (REDISTRIBUTE_STATIC, 'Static', 'orange'), + (REDISTRIBUTE_OSPF, 'OSPF', 'green'), + (REDISTRIBUTE_ISIS, 'IS-IS', 'yellow'), + (REDISTRIBUTE_EIGRP, 'EIGRP', 'red'), + (REDISTRIBUTE_RIP, 'RIP', 'black'), + (REDISTRIBUTE_BGP, 'BGP', 'blue'), + ] + + class ActionChoices(ChoiceSet): key = "Action.status" diff --git a/netbox_bgp/filtersets.py b/netbox_bgp/filtersets.py index 3c33598..36c23d4 100644 --- a/netbox_bgp/filtersets.py +++ b/netbox_bgp/filtersets.py @@ -1,25 +1,57 @@ import django_filters import netaddr +from django.contrib.contenttypes.models import ContentType from django.db.models import Q from netaddr.core import AddrFormatError from netbox.filtersets import NetBoxModelFilterSet from tenancy.filtersets import TenancyFilterSet +from .choices import RedistributeSourceChoices from .models import ( Community, BGPSession, RoutingPolicy, RoutingPolicyRule, BGPPeerGroup, PrefixList, PrefixListRule, CommunityList, - CommunityListRule, ASPathList, ASPathListRule + CommunityListRule, ASPathList, ASPathListRule, Redistributing ) -from ipam.models import IPAddress, ASN + +from ipam.models import IPAddress, ASN, VRF from dcim.models import Device, Site from virtualization.models import VirtualMachine - +from utilities.filters import ContentTypeFilter class ASPathListFilterSet(NetBoxModelFilterSet): + scope_type = ContentTypeFilter() + region = django_filters.NumberFilter( + method='filter_scope' + ) + site_group = django_filters.NumberFilter( + method='filter_scope' + ) + site = django_filters.NumberFilter( + method='filter_scope' + ) + location = django_filters.NumberFilter( + method='filter_scope' + ) + rack = django_filters.NumberFilter( + method='filter_scope' + ) + cluster_group = django_filters.NumberFilter( + method='filter_scope' + ) + cluster = django_filters.NumberFilter( + method='filter_scope' + ) class Meta: model = ASPathList - fields = ['id', 'name', 'description'] + fields = ['id', 'name', 'description', 'scope_id'] + + def filter_scope(self, queryset, name, value): + model_name = name.replace('_', '') + return queryset.filter( + scope_type=ContentType.objects.get(model=model_name), + scope_id=value + ) def search(self, queryset, name, value): """Perform the filtered search.""" @@ -54,7 +86,7 @@ class CommunityFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = Community - fields = ('id', 'value', 'description', 'status', 'tenant',) + fields = ('id', 'value', 'description', 'status', 'tenant') def search(self, queryset, name, value): """Perform the filtered search.""" @@ -68,10 +100,39 @@ def search(self, queryset, name, value): class CommunityListFilterSet(NetBoxModelFilterSet): + scope_type = ContentTypeFilter() + region = django_filters.NumberFilter( + method='filter_scope' + ) + site_group = django_filters.NumberFilter( + method='filter_scope' + ) + site = django_filters.NumberFilter( + method='filter_scope' + ) + location = django_filters.NumberFilter( + method='filter_scope' + ) + rack = django_filters.NumberFilter( + method='filter_scope' + ) + cluster_group = django_filters.NumberFilter( + method='filter_scope' + ) + cluster = django_filters.NumberFilter( + method='filter_scope' + ) class Meta: model = CommunityList - fields = ('id', 'name', 'description',) + fields = ('id', 'name', 'description', 'scope_id') + + def filter_scope(self, queryset, name, value): + model_name = name.replace('_', '') + return queryset.filter( + scope_type=ContentType.objects.get(model=model_name), + scope_id=value + ) def search(self, queryset, name, value): """Perform the filtered search.""" @@ -88,7 +149,7 @@ class CommunityListRuleFilterSet(NetBoxModelFilterSet): class Meta: model = CommunityListRule - fields = ('id', 'action', 'community_list', 'community_list_id',) + fields = ('id', 'action', 'community_list', 'community_list_id') def search(self, queryset, name, value): """Perform the filtered search.""" @@ -195,7 +256,7 @@ class BGPSessionFilterSet(NetBoxModelFilterSet, TenancyFilterSet): field_name='site__name', queryset=Site.objects.all(), to_field_name='name', - label='DSite (name)', + label='Site (name)', ) by_remote_address = django_filters.CharFilter( method='search_by_remote_ip', @@ -208,7 +269,7 @@ class BGPSessionFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: model = BGPSession - fields = ('id', 'name', 'description', 'status', 'tenant',) + fields = ('id', 'name', 'description', 'status', 'tenant') def search(self, queryset, name, value): """Perform the filtered search.""" @@ -242,10 +303,39 @@ def search_by_local_ip(self, queryset, name, value): class RoutingPolicyFilterSet(NetBoxModelFilterSet): + scope_type = ContentTypeFilter() + region = django_filters.NumberFilter( + method='filter_scope' + ) + site_group = django_filters.NumberFilter( + method='filter_scope' + ) + site = django_filters.NumberFilter( + method='filter_scope' + ) + location = django_filters.NumberFilter( + method='filter_scope' + ) + rack = django_filters.NumberFilter( + method='filter_scope' + ) + cluster_group = django_filters.NumberFilter( + method='filter_scope' + ) + cluster = django_filters.NumberFilter( + method='filter_scope' + ) class Meta: model = RoutingPolicy - fields = ('id', 'name', 'description',) + fields = ('id', 'name', 'description', 'scope_id') + + def filter_scope(self, queryset, name, value): + model_name = name.replace('_', '') + return queryset.filter( + scope_type=ContentType.objects.get(model=model_name), + scope_id=value + ) def search(self, queryset, name, value): """Perform the filtered search.""" @@ -262,7 +352,7 @@ class RoutingPolicyRuleFilterSet(NetBoxModelFilterSet): class Meta: model = RoutingPolicyRule - fields = ('id', 'index', 'action', 'description', 'routing_policy_id', 'continue_entry',) + fields = ('id', 'index', 'action', 'description', 'routing_policy_id', 'continue_entry') def search(self, queryset, name, value): """Perform the filtered search.""" @@ -279,10 +369,39 @@ def search(self, queryset, name, value): class BGPPeerGroupFilterSet(NetBoxModelFilterSet): + scope_type = ContentTypeFilter() + region = django_filters.NumberFilter( + method='filter_scope' + ) + site_group = django_filters.NumberFilter( + method='filter_scope' + ) + site = django_filters.NumberFilter( + method='filter_scope' + ) + location = django_filters.NumberFilter( + method='filter_scope' + ) + rack = django_filters.NumberFilter( + method='filter_scope' + ) + cluster_group = django_filters.NumberFilter( + method='filter_scope' + ) + cluster = django_filters.NumberFilter( + method='filter_scope' + ) class Meta: model = BGPPeerGroup - fields = ('id', 'name', 'description',) + fields = ('id', 'name', 'description', 'scope_id') + + def filter_scope(self, queryset, name, value): + model_name = name.replace('_', '') + return queryset.filter( + scope_type=ContentType.objects.get(model=model_name), + scope_id=value + ) def search(self, queryset, name, value): """Perform the filtered search.""" @@ -296,10 +415,39 @@ def search(self, queryset, name, value): class PrefixListFilterSet(NetBoxModelFilterSet): + scope_type = ContentTypeFilter() + region = django_filters.NumberFilter( + method='filter_scope' + ) + site_group = django_filters.NumberFilter( + method='filter_scope' + ) + site = django_filters.NumberFilter( + method='filter_scope' + ) + location = django_filters.NumberFilter( + method='filter_scope' + ) + rack = django_filters.NumberFilter( + method='filter_scope' + ) + cluster_group = django_filters.NumberFilter( + method='filter_scope' + ) + cluster = django_filters.NumberFilter( + method='filter_scope' + ) class Meta: model = PrefixList - fields = ('id', 'name', 'description', 'family',) + fields = ('id', 'name', 'description', 'family', 'scope_id') + + def filter_scope(self, queryset, name, value): + model_name = name.replace('_', '') + return queryset.filter( + scope_type=ContentType.objects.get(model=model_name), + scope_id=value + ) def search(self, queryset, name, value): """Perform the filtered search.""" @@ -316,7 +464,7 @@ class PrefixListRuleFilterSet(NetBoxModelFilterSet): class Meta: model = PrefixListRule #fields = ['index', 'action', 'prefix_custom', 'ge', 'le', 'prefix_list', 'prefix_list_id'] - fields = ('id', 'index', 'action', 'ge', 'le', 'prefix_list', 'prefix_list_id',) + fields = ('id', 'index', 'action', 'ge', 'le', 'prefix_list', 'prefix_list_id') def search(self, queryset, name, value): """Perform the filtered search.""" @@ -332,3 +480,92 @@ def search(self, queryset, name, value): | Q(prefix_list_id__icontains=value) ) return queryset.filter(qs_filter) + + +class RedistributingFilterSet(NetBoxModelFilterSet, TenancyFilterSet): + scope_type = ContentTypeFilter() + region = django_filters.NumberFilter( + method='filter_scope' + ) + site_group = django_filters.NumberFilter( + method='filter_scope' + ) + site = django_filters.NumberFilter( + method='filter_scope' + ) + location = django_filters.NumberFilter( + method='filter_scope' + ) + rack = django_filters.NumberFilter( + method='filter_scope' + ) + cluster_group = django_filters.NumberFilter( + method='filter_scope' + ) + cluster = django_filters.NumberFilter( + method='filter_scope' + ) + redistribute_source = django_filters.ChoiceFilter( + choices=RedistributeSourceChoices, + ) + redistribute_policy = django_filters.ModelChoiceFilter( + queryset=RoutingPolicy.objects.all(), + ) + device_id = django_filters.ModelMultipleChoiceFilter( + field_name='device__id', + queryset=Device.objects.all(), + to_field_name='id', + label='Device (ID)', + ) + device = django_filters.ModelMultipleChoiceFilter( + field_name='device__name', + queryset=Device.objects.all(), + to_field_name='name', + label='Device (name)', + ) + virtualmachine_id = django_filters.ModelMultipleChoiceFilter( + field_name='virtualmachine__id', + queryset=VirtualMachine.objects.all(), + to_field_name='id', + label='VirtualMachine (ID)', + ) + virtualmachine = django_filters.ModelMultipleChoiceFilter( + field_name='virtualmachine__name', + queryset=VirtualMachine.objects.all(), + to_field_name='name', + label='VirtualMachine (name)', + ) + vrf = django_filters.ModelMultipleChoiceFilter( + field_name='vrf__name', + queryset=VRF.objects.all(), + to_field_name='name', + label='VRF (name)', + ) + vrf_id = django_filters.ModelMultipleChoiceFilter( + field_name='vrf__id', + queryset=VRF.objects.all(), + to_field_name='id', + label='VRF (ID)', + ) + + class Meta: + model = Redistributing + fields = ('id', 'name', 'description', 'tenant') + + def filter_scope(self, queryset, name, value): + model_name = name.replace('_', '') + return queryset.filter( + scope_type=ContentType.objects.get(model=model_name), + scope_id=value + ) + + def search(self, queryset, name, value): + """Perform the filtered search.""" + if not value.strip(): + return queryset + qs_filter = ( + Q(redistribute_source__icontains=value) + | Q(name__icontains=value) + | Q(description__icontains=value) + ) + return queryset.filter(qs_filter) diff --git a/netbox_bgp/forms.py b/netbox_bgp/forms.py index b376880..12675b2 100644 --- a/netbox_bgp/forms.py +++ b/netbox_bgp/forms.py @@ -1,5 +1,6 @@ from django import forms from utilities.forms.rendering import FieldSet +from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ( MultipleObjectsReturned, ObjectDoesNotExist, @@ -8,9 +9,10 @@ from django.utils.translation import gettext as _ from tenancy.models import Tenant -from dcim.models import Device, Site -from ipam.models import IPAddress, Prefix, ASN +from dcim.models import Device, Location, Rack, Region, Site, SiteGroup +from ipam.models import IPAddress, Prefix, ASN, VRF from ipam.formfields import IPNetworkFormField +from ipam.constants import VLANGROUP_SCOPE_TYPES from utilities.forms.fields import ( DynamicModelChoiceField, CSVModelChoiceField, @@ -19,9 +21,13 @@ TagFilterField, CSVChoiceField, CommentField, + ContentTypeChoiceField, + CSVContentTypeField, ) from utilities.forms import add_blank_choice -from utilities.forms.widgets import APISelect, APISelectMultiple +from utilities.forms.widgets import APISelect, APISelectMultiple, HTMXSelect +from utilities.forms.utils import get_field_value +from utilities.templatetags.builtins.filters import bettertitle from netbox.forms import ( NetBoxModelForm, NetBoxModelBulkEditForm, @@ -42,22 +48,65 @@ CommunityListRule, ASPathList, ASPathListRule, + Redistributing, ) from .choices import ( SessionStatusChoices, CommunityStatusChoices, IPAddressFamilyChoices, + RedistributeSourceChoices, ) -from virtualization.models import VirtualMachine +from virtualization.models import Cluster, ClusterGroup, VirtualMachine class ASPathListFilterForm(NetBoxModelFilterSetForm): model = ASPathList q = forms.CharField(required=False, label="Search") + region = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_("Region") + ) + site_group = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_("Site group") + ) + site = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_("Site") + ) + location = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + label=_("Location") + ) + rack = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label=_("Rack") + ) + cluster = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_("Cluster") + ) + cluster_group = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_("Cluster group") + ) tag = TagFilterField(model) + fieldsets = ( + FieldSet("q", "filter_id", "tag"), + FieldSet("region", "site_group", "site", "location", "rack", name=_("Location")), + FieldSet("cluster_group", "cluster", name=_("Cluster")), + ) + class ASPathListRuleFilterForm(NetBoxModelFilterSetForm): model = ASPathListRule q = forms.CharField(required=False, label="Search") @@ -66,28 +115,115 @@ class ASPathListRuleFilterForm(NetBoxModelFilterSetForm): class ASPathListForm(NetBoxModelForm): - + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) comments = CommentField() + fieldsets = ( + FieldSet("name", "description", "tags"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) + class Meta: model = ASPathList fields = ["name", "description", "tags", "comments"] + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + initial = kwargs.get("initial", {}) + + if instance is not None and instance.scope: + initial["scope"] = instance.scope + kwargs["initial"] = initial + + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass + + if self.instance and scope_type_id != self.instance.scope_type_id: + self.initial["scope"] = None + + def clean(self): + super().clean() + + # Assign the selected scope (if any) + self.instance.scope = self.cleaned_data.get("scope") + + class ASPathListBulkEditForm(NetBoxModelBulkEditForm): description = forms.CharField(max_length=200, required=False) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(method="post", attrs={"hx-select": "#form_fields"}), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) model = ASPathList + fieldsets = ( + FieldSet("description", "tag"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) nullable_fields = [ "description", + "scope", ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass class ASPathListImportForm(NetBoxModelImportForm): + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) class Meta: model = ASPathList - fields = ["name", "description", "tags"] + fields = ["name", "description", "scope_type", "scope_id", "tags"] + labels = { + "scope_id": "Scope ID", + } class ASPathListRuleImportForm(NetBoxModelImportForm): @@ -177,33 +313,157 @@ class CommunityListFilterForm(NetBoxModelFilterSetForm): model = CommunityList q = forms.CharField(required=False, label="Search") + region = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_("Region") + ) + site_group = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_("Site group") + ) + site = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_("Site") + ) + location = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + label=_("Location") + ) + rack = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label=_("Rack") + ) + cluster = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_("Cluster") + ) + cluster_group = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_("Cluster group") + ) tag = TagFilterField(model) + fieldsets = ( + FieldSet("q", "filter_id", "tag"), + FieldSet("region", "site_group", "site", "location", "rack", name=_("Location")), + FieldSet("cluster_group", "cluster", name=_("Cluster")), + ) class CommunityListForm(NetBoxModelForm): - + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) comments = CommentField() + fieldsets = ( + FieldSet("name", "description", "tags"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) + class Meta: model = CommunityList fields = ["name", "description", "tags", "comments"] + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + initial = kwargs.get("initial", {}) + + if instance is not None and instance.scope: + initial["scope"] = instance.scope + kwargs["initial"] = initial + + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass + + if self.instance and scope_type_id != self.instance.scope_type_id: + self.initial["scope"] = None + + def clean(self): + super().clean() + + # Assign the selected scope (if any) + self.instance.scope = self.cleaned_data.get("scope") class CommunityListBulkEditForm(NetBoxModelBulkEditForm): description = forms.CharField(max_length=200, required=False) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(method="post", attrs={"hx-select": "#form_fields"}), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) model = CommunityList + fieldsets = ( + FieldSet("description", "tag"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) nullable_fields = [ "description", + "scope", ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass class CommunityListImportForm(NetBoxModelImportForm): + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) class Meta: model = CommunityList - fields = ("name", "description", "tags") - + fields = ("name", "description", "scope_type", "scope_id", "tags") + labels = { + "scope_id": "Scope ID", + } class CommunityListRuleForm(NetBoxModelForm): community = DynamicModelChoiceField( @@ -514,7 +774,6 @@ class BGPSessionBulkEditForm(NetBoxModelBulkEditForm): site = DynamicModelChoiceField( label=_("Site"), queryset=Site.objects.all(), required=False ) - status = forms.ChoiceField( label=_('Status'), choices=add_blank_choice(SessionStatusChoices), @@ -575,6 +834,7 @@ class BGPSessionBulkEditForm(NetBoxModelBulkEditForm): "export_policies", "prefix_list_in", "prefix_list_out", + "site", ] @@ -582,42 +842,219 @@ class RoutingPolicyFilterForm(NetBoxModelFilterSetForm): model = RoutingPolicy q = forms.CharField(required=False, label="Search") + region = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_("Region") + ) + site_group = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_("Site group") + ) + site = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_("Site") + ) + location = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + label=_("Location") + ) + rack = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label=_("Rack") + ) + cluster = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_("Cluster") + ) + cluster_group = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_("Cluster group") + ) tag = TagFilterField(model) + fieldsets = ( + FieldSet("q", "filter_id", "tag"), + FieldSet("region", "site_group", "site", "location", "rack", name=_("Location")), + FieldSet("cluster_group", "cluster", name=_("Cluster")), + ) class RoutingPolicyForm(NetBoxModelForm): - + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) comments = CommentField() + fieldsets = ( + FieldSet("name", "description", "weight", "tags"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) + class Meta: model = RoutingPolicy fields = ["name", "description", "weight", "tags", "comments"] + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + initial = kwargs.get("initial", {}) + + if instance is not None and instance.scope: + initial["scope"] = instance.scope + kwargs["initial"] = initial + + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass + + if self.instance and scope_type_id != self.instance.scope_type_id: + self.initial["scope"] = None + + def clean(self): + super().clean() + + # Assign the selected scope (if any) + self.instance.scope = self.cleaned_data.get("scope") class RoutingPolicyImportForm(NetBoxModelImportForm): + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) class Meta: model = RoutingPolicy - fields = ("name", "description", "weight", "tags") - + fields = ("name", "description", "scope_type", "scope_id", "weight", "tags") + labels = { + "scope_id": "Scope ID", + } class RoutingPolicyBulkEditForm(NetBoxModelBulkEditForm): description = forms.CharField(max_length=200, required=False) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(method="post", attrs={"hx-select": "#form_fields"}), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) model = RoutingPolicy + fieldsets = ( + FieldSet("description", "tag"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) nullable_fields = [ "description", + "scope", ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass class BGPPeerGroupFilterForm(NetBoxModelFilterSetForm): model = BGPPeerGroup q = forms.CharField(required=False, label="Search") + region = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_("Region") + ) + site_group = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_("Site group") + ) + site = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_("Site") + ) + location = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + label=_("Location") + ) + rack = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label=_("Rack") + ) + cluster = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_("Cluster") + ) + cluster_group = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_("Cluster group") + ) tag = TagFilterField(model) + fieldsets = ( + FieldSet("q", "filter_id", "tag"), + FieldSet("region", "site_group", "site", "location", "rack", name=_("Location")), + FieldSet("cluster_group", "cluster", name=_("Cluster")), + ) class BGPPeerGroupForm(NetBoxModelForm): + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) import_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, @@ -630,6 +1067,11 @@ class BGPPeerGroupForm(NetBoxModelForm): ) comments = CommentField() + fieldsets = ( + FieldSet("name", "description", "import_policies", "export_policies", "tags"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) + class Meta: model = BGPPeerGroup fields = [ @@ -641,9 +1083,42 @@ class Meta: "comments", ] + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + initial = kwargs.get("initial", {}) + + if instance is not None and instance.scope: + initial["scope"] = instance.scope + kwargs["initial"] = initial -class BGPPeerGroupImportForm(NetBoxModelImportForm): + super().__init__(*args, **kwargs) + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass + + if self.instance and scope_type_id != self.instance.scope_type_id: + self.initial["scope"] = None + + def clean(self): + super().clean() + + # Assign the selected scope (if any) + self.instance.scope = self.cleaned_data.get("scope") + +class BGPPeerGroupImportForm(NetBoxModelImportForm): + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) import_policies = CSVModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), to_field_name="name", @@ -659,11 +1134,26 @@ class BGPPeerGroupImportForm(NetBoxModelImportForm): class Meta: model = BGPPeerGroup - fields = ("name", "description", "import_policies", "export_policies", "tags") - + fields = ("name", "description", "scope_type", "scope_id", "import_policies", "export_policies", "tags") + labels = { + "scope_id": "Scope ID", + } class BGPPeerGroupBulkEditForm(NetBoxModelBulkEditForm): description = forms.CharField(max_length=200, required=False) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(method="post", attrs={"hx-select": "#form_fields"}), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) import_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), @@ -677,10 +1167,27 @@ class BGPPeerGroupBulkEditForm(NetBoxModelBulkEditForm): ) model = BGPPeerGroup + fieldsets = ( + FieldSet("description", "import_policies", "export_policies", "tag"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) nullable_fields = [ - "description", "import_policies", "export_policies" + "description", "scope", "import_policies", "export_policies" ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass class RoutingPolicyRuleForm(NetBoxModelForm): continue_entry = forms.IntegerField( @@ -822,27 +1329,120 @@ class PrefixListFilterForm(NetBoxModelFilterSetForm): model = PrefixList q = forms.CharField(required=False, label="Search") + region = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_("Region") + ) + site_group = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_("Site group") + ) + site = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_("Site") + ) + location = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + label=_("Location") + ) + rack = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label=_("Rack") + ) + cluster = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_("Cluster") + ) + cluster_group = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_("Cluster group") + ) tag = TagFilterField(model) + fieldsets = ( + FieldSet("q", "filter_id", "tag"), + FieldSet("region", "site_group", "site", "location", "rack", name=_("Location")), + FieldSet("cluster_group", "cluster", name=_("Cluster")), + ) class PrefixListForm(NetBoxModelForm): - + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) comments = CommentField() + fieldsets = ( + FieldSet("name", "description", "family", "tags"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) + class Meta: model = PrefixList fields = ["name", "description", "family", "tags", "comments"] + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + initial = kwargs.get("initial", {}) + + if instance is not None and instance.scope: + initial["scope"] = instance.scope + kwargs["initial"] = initial + + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass + + if self.instance and scope_type_id != self.instance.scope_type_id: + self.initial["scope"] = None + + def clean(self): + super().clean() + + # Assign the selected scope (if any) + self.instance.scope = self.cleaned_data.get("scope") class PrefixListImportForm(NetBoxModelImportForm): family = CSVChoiceField( choices=IPAddressFamilyChoices, required=True, help_text=_("Family address") ) + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) class Meta: model = PrefixList - fields = ("name", "description", "family", "tags") - + fields = ("name", "description", "family", "scope_type", "scope_id", "tags") + labels = { + "scope_id": "Scope ID", + } class PrefixListBulkEditForm(NetBoxModelBulkEditForm): description = forms.CharField(max_length=200, required=False) @@ -853,11 +1453,44 @@ class PrefixListBulkEditForm(NetBoxModelBulkEditForm): choices=IPAddressFamilyChoices, ) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(method="post", attrs={"hx-select": "#form_fields"}), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) + model = PrefixList + fieldsets = ( + FieldSet("description", "family", "tag"), + FieldSet("scope_type", "scope", name=_("Scope")), + ) nullable_fields = [ "description", + "scope", ] + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass + class PrefixListRuleImportForm(NetBoxModelImportForm): prefix_list = CSVModelChoiceField( label=_('Prefix List'), @@ -921,3 +1554,309 @@ class Meta: "tags", "comments", ] + + +class RedistributingForm(NetBoxModelForm): + name = forms.CharField(max_length=256, required=True) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) + vrf = DynamicModelChoiceField(label="VRF", queryset=VRF.objects.all(), required=False) + device = DynamicModelChoiceField( + queryset=Device.objects.all(), required=False, query_params={"site_id": "$site"} + ) + virtualmachine = DynamicModelChoiceField( + queryset=VirtualMachine.objects.all(), required=False, query_params={"site_id": "$site"} + ) + redistribute_source = forms.ChoiceField( + required=True, + choices=RedistributeSourceChoices, + ) + redistribute_policy = DynamicModelChoiceField( + queryset=RoutingPolicy.objects.all(), + required=False, + query_params={"site_id": "$site"}, + widget=APISelect(api_url="/api/plugins/bgp/routing-policy/"), + ) + tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False) + + comments = CommentField() + + fieldsets = ( + FieldSet( + "name", + "description", + "vrf", + "device", + "virtualmachine", + "redistribute_source", + "redistribute_policy", + "tags" + ), + FieldSet("scope_type", "scope", name=_("Scope")), + FieldSet("tenant", name=_("Tenancy")), + ) + + class Meta: + model = Redistributing + fields = [ + "name", + "description", + "vrf", + "device", + "virtualmachine", + "redistribute_source", + "redistribute_policy", + "tenant", + "tags", + "comments", + ] + + def __init__(self, *args, **kwargs): + instance = kwargs.get("instance") + initial = kwargs.get("initial", {}) + + if instance is not None and instance.scope: + initial["scope"] = instance.scope + kwargs["initial"] = initial + + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass + + if self.instance and scope_type_id != self.instance.scope_type_id: + self.initial["scope"] = None + + def clean(self): + super().clean() + + # Assign the selected scope (if any) + self.instance.scope = self.cleaned_data.get("scope") + +class RedistributingImportForm(NetBoxModelImportForm): + scope_type = CSVContentTypeField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + required=False, + label=_('Scope type (app & model)') + ) + vrf = CSVModelChoiceField( + label=_("VRF"), + required=False, + queryset=VRF.objects.all(), + to_field_name="name", + help_text=_("Assigned VRF"), + ) + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name="name", + help_text=_("Assigned tenant"), + ) + device = CSVModelChoiceField( + queryset=Device.objects.all(), + required=False, + to_field_name="name", + help_text=_("Assigned device"), + ) + virtualmachine = CSVModelChoiceField( + queryset=VirtualMachine.objects.all(), + required=False, + to_field_name="name", + help_text=_("Assigned virtual machine"), + ) + redistribute_source = CSVChoiceField( + choices=RedistributeSourceChoices, required=True, help_text=_("Redistribute source") + ) + redistribute_policy = CSVModelChoiceField( + queryset=RoutingPolicy.objects.all(), + to_field_name="name", + required=False, + help_text=_("Routing policy name"), + ) + + class Meta: + model = Redistributing + fields = [ + "name", + "description", + "scope_type", + "scope_id", + "vrf", + "device", + "virtualmachine", + "redistribute_source", + "redistribute_policy", + "tenant", + "tags", + "comments", + ] + labels = { + "scope_id": "Scope ID", + } + + +class RedistributingFilterForm(NetBoxModelFilterSetForm): + model = Redistributing + q = forms.CharField(required=False, label="Search") + vrf_id = DynamicModelMultipleChoiceField( + queryset=VRF.objects.all(), required=False, label=_("VRF") + ) + device_id = DynamicModelMultipleChoiceField( + queryset=Device.objects.all(), required=False, label=_("Device") + ) + virtualmachine_id = DynamicModelMultipleChoiceField( + queryset=VirtualMachine.objects.all(), required=False, label=_("VirtualMachine") + ) + region = DynamicModelMultipleChoiceField( + queryset=Region.objects.all(), + required=False, + label=_("Region") + ) + site_group = DynamicModelMultipleChoiceField( + queryset=SiteGroup.objects.all(), + required=False, + label=_("Site group") + ) + site = DynamicModelMultipleChoiceField( + queryset=Site.objects.all(), + required=False, + label=_("Site") + ) + location = DynamicModelMultipleChoiceField( + queryset=Location.objects.all(), + required=False, + label=_("Location") + ) + rack = DynamicModelMultipleChoiceField( + queryset=Rack.objects.all(), + required=False, + label=_("Rack") + ) + cluster = DynamicModelMultipleChoiceField( + queryset=Cluster.objects.all(), + required=False, + label=_("Cluster") + ) + cluster_group = DynamicModelMultipleChoiceField( + queryset=ClusterGroup.objects.all(), + required=False, + label=_("Cluster group") + ) + redistribute_source = forms.MultipleChoiceField( + choices=RedistributeSourceChoices, + required=False, + ) + redistribute_policy = DynamicModelChoiceField( + queryset=RoutingPolicy.objects.all(), + required=False, + widget=APISelect(api_url="/api/plugins/bgp/routing-policy/"), + ) + tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False) + + tag = TagFilterField(model) + + fieldsets = ( + FieldSet( + "q", + "filter_id", + "vrf_id", + "device_id", + "virtualmachine_id", + "redistribute_source", + "redistribute_policy", + "tag" + ), + FieldSet("region", "site_group", "site", "location", "rack", name=_("Location")), + FieldSet("cluster_group", "cluster", name=_("Cluster")), + FieldSet("tenant", name=_("Tenancy")), + ) + +class RedistributingBulkEditForm(NetBoxModelBulkEditForm): + device = DynamicModelChoiceField( + label=_("Device"), + queryset=Device.objects.all(), + required=False, + ) + virtualmachine = DynamicModelChoiceField( + label=_("Virtual Machine"), + queryset=VirtualMachine.objects.all(), + required=False, + ) + vrf = DynamicModelChoiceField( + label=_("VRF"), queryset=VRF.objects.all(), required=False + ) + scope_type = ContentTypeChoiceField( + queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), + widget=HTMXSelect(method="post", attrs={"hx-select": "#form_fields"}), + required=False, + label=_("Scope type") + ) + scope = DynamicModelChoiceField( + label=_("Scope"), + queryset=Site.objects.none(), # Initial queryset + required=False, + disabled=True, + selector=True + ) + redistribute_source = forms.ChoiceField( + label=_('Redistribute source'), + choices=add_blank_choice(RedistributeSourceChoices), + required=True + ) + redistribute_policy = DynamicModelChoiceField( + queryset=RoutingPolicy.objects.all(), + required=False, + widget=APISelect(api_url="/api/plugins/bgp/routing-policy/"), + ) + description = forms.CharField( + label=_("Description"), max_length=200, required=False + ) + tenant = DynamicModelChoiceField( + label=_("Tenant"), queryset=Tenant.objects.all(), required=False + ) + + model = Redistributing + fieldsets = ( + FieldSet("description", "vrf", "device", "virtualmachine", "redistribute_source", "redistribute_policy", "tag"), + FieldSet("scope_type", "scope", name=_("Scope")), + FieldSet("tenant", name=_("Tenancy")), + ) + nullable_fields = [ + "tenant", + "description", + "scope", + "vrf", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if scope_type_id := get_field_value(self, "scope_type"): + try: + scope_type = ContentType.objects.get(pk=scope_type_id) + model = scope_type.model_class() + self.fields["scope"].queryset = model.objects.all() + self.fields["scope"].widget.attrs["selector"] = model._meta.label_lower + self.fields["scope"].disabled = False + self.fields["scope"].label = _(bettertitle(model._meta.verbose_name)) + except ObjectDoesNotExist: + pass \ No newline at end of file diff --git a/netbox_bgp/graphql/enums.py b/netbox_bgp/graphql/enums.py index 3f837ff..a5337d7 100644 --- a/netbox_bgp/graphql/enums.py +++ b/netbox_bgp/graphql/enums.py @@ -5,6 +5,7 @@ SessionStatusChoices, ActionChoices, IPAddressFamilyChoices, + RedistributeSourceChoices, ) __all__ = ( @@ -12,10 +13,14 @@ "NetBoxBGPSessionStatusEnum", "NetBoxBGPActionEnum", "NetBoxBGPIPAddressFamilyEnum", + "NetBoxBGPRedistributingRedistributeSourceEnum", ) -NetBoxBGPCommunityStatusEnum = strawberry.enum(CommunityStatusChoices.as_enum(), name="NetBoxBGPCommunityStatusEnum" ) +NetBoxBGPCommunityStatusEnum = strawberry.enum(CommunityStatusChoices.as_enum(), name="NetBoxBGPCommunityStatusEnum") NetBoxBGPSessionStatusEnum = strawberry.enum(SessionStatusChoices.as_enum(), name="NetBoxBGPSessionStatusEnum") NetBoxBGPActionEnum = strawberry.enum(ActionChoices.as_enum(), name="NetBoxBGPActionEnum") NetBoxBGPIPAddressFamilyEnum = strawberry.enum(IPAddressFamilyChoices.as_enum(), name="NetBoxBGPIPAddressFamilyEnum") - +NetBoxBGPRedistributingRedistributeSourceEnum = strawberry.enum( + RedistributeSourceChoices.as_enum(), + name="NetBoxBGPRedistributingRedistributeSourceEnum" +) diff --git a/netbox_bgp/graphql/filters.py b/netbox_bgp/graphql/filters.py index 430a86c..8f920cc 100644 --- a/netbox_bgp/graphql/filters.py +++ b/netbox_bgp/graphql/filters.py @@ -7,7 +7,8 @@ from netbox.graphql.filter_mixins import NetBoxModelFilterMixin from tenancy.graphql.filter_mixins import TenancyFilterMixin from ipam.graphql.filters import IPAddressFilter, ASNFilter -from dcim.graphql.filters import DeviceFilter +from dcim.graphql.filters import DeviceFilter, SiteFilter +from virtualization.graphql.filters import VirtualMachineFilter from netbox_bgp.models import ( Community, @@ -20,7 +21,8 @@ CommunityList, CommunityListRule, ASPathList, - ASPathListRule + ASPathListRule, + Redistributing, ) from netbox_bgp.filtersets import ( @@ -41,10 +43,10 @@ NetBoxBGPCommunityStatusEnum, NetBoxBGPSessionStatusEnum, NetBoxBGPIPAddressFamilyEnum, - NetBoxBGPActionEnum + NetBoxBGPActionEnum, + NetBoxBGPRedistributingRedistributeSourceEnum, ) - __all__ = ( "NetBoxBGPCommunityFilter", "NetBoxBGPSessionFilter", @@ -64,6 +66,7 @@ class NetBoxBGPASPathListFilter(NetBoxModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() + @strawberry_django.filter_type(ASPathListRule, lookups=True) class NetBoxBGPASPathListRuleFilter(NetBoxModelFilterMixin): value: FilterLookup[str] | None = strawberry_django.filter_field() @@ -81,6 +84,7 @@ class NetBoxBGPASPathListRuleFilter(NetBoxModelFilterMixin): | None ) = strawberry_django.filter_field() + @strawberry_django.filter_type(Community, lookups=True) class NetBoxBGPCommunityFilter(TenancyFilterMixin, NetBoxModelFilterMixin): value: FilterLookup[str] | None = strawberry_django.filter_field() @@ -97,6 +101,9 @@ class NetBoxBGPCommunityFilter(TenancyFilterMixin, NetBoxModelFilterMixin): class NetBoxBGPSessionFilter(TenancyFilterMixin, NetBoxModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() + site: ( + Annotated['SiteFilter', strawberry.lazy('dcim.graphql.filters')] | None + ) = strawberry_django.filter_field() status: ( Annotated[ "NetBoxBGPSessionStatusEnum", strawberry.lazy("netbox_bgp.graphql.enums") @@ -151,18 +158,18 @@ class NetBoxBGPSessionFilter(TenancyFilterMixin, NetBoxModelFilterMixin): ) = strawberry_django.filter_field() - - @strawberry_django.filter_type(BGPPeerGroup, lookups=True) class NetBoxBGPBGPPeerGroupFilter(NetBoxModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() + @strawberry_django.filter_type(RoutingPolicy, lookups=True) class NetBoxBGPRoutingPolicyFilter(NetBoxModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() description: FilterLookup[str] | None = strawberry_django.filter_field() + @strawberry_django.filter_type(RoutingPolicyRule, lookups=True) class NetBoxBGPRoutingPolicyRuleFilter(NetBoxModelFilterMixin): description: FilterLookup[str] | None = strawberry_django.filter_field() @@ -217,7 +224,6 @@ class NetBoxBGPPrefixListRuleFilter(NetBoxModelFilterMixin): prefix_list_id: ID | None = strawberry_django.filter_field() - @strawberry_django.filter_type(CommunityList, lookups=True) class NetBoxBGPCommunityListFilter(NetBoxModelFilterMixin): name: FilterLookup[str] | None = strawberry_django.filter_field() @@ -242,4 +248,30 @@ class NetBoxBGPCommunityListRuleFilter(NetBoxModelFilterMixin): community_list_id: ID | None = strawberry_django.filter_field() +@strawberry_django.filter_type(Redistributing, lookups=True) +class NetBoxBGPRedistributingFilter(TenancyFilterMixin, NetBoxModelFilterMixin): + name: FilterLookup[str] | None = strawberry_django.filter_field() + description: FilterLookup[str] | None = strawberry_django.filter_field() + device: ( + Annotated["DeviceFilter", strawberry.lazy("dcim.graphql.filters")] | None + ) = strawberry_django.filter_field() + device_id: ID | None = strawberry_django.filter_field() + + virtualmachine: ( + Annotated[ + "VirtualMachineFilter", strawberry.lazy("virtualization.graphql.filters") + ] | None + ) = strawberry_django.filter_field() + virtualmachine_id: ID | None = strawberry_django.filter_field() + + redistribute_source: ( + Annotated[ + "NetBoxBGPRedistributingRedistributeSourceEnum", strawberry.lazy("netbox_bgp.graphql.enums") + ] | None + ) = strawberry_django.filter_field() + redistribute_policy: ( + Annotated[ + "NetBoxBGPRoutingPolicyFilter", strawberry.lazy("netbox_bgp.graphql.filters") + ] | None + ) = strawberry_django.filter_field() diff --git a/netbox_bgp/graphql/schema.py b/netbox_bgp/graphql/schema.py index 2294093..c2c43c4 100644 --- a/netbox_bgp/graphql/schema.py +++ b/netbox_bgp/graphql/schema.py @@ -27,7 +27,8 @@ CommunityListType, CommunityListRuleType, ASPathListType, - ASPathListRuleType + ASPathListRuleType, + RedistributingType ) @@ -65,4 +66,7 @@ class NetBoxBGPQuery: netbox_bgp_aspathlist_list: List[ASPathListType] = strawberry_django.field() netbox_bgp_aspathlist_rule: ASPathListRuleType = strawberry_django.field() - netbox_bgp_aspathlist_rule_list: List[ASPathListRuleType] = strawberry_django.field() \ No newline at end of file + netbox_bgp_aspathlist_rule_list: List[ASPathListRuleType] = strawberry_django.field() + + netbox_bgp_redistributing: RedistributingType = strawberry_django.field() + netbox_bgp_redistributing_list: List[RedistributingType] = strawberry_django.field() \ No newline at end of file diff --git a/netbox_bgp/graphql/types.py b/netbox_bgp/graphql/types.py index 8093636..2e30f7b 100644 --- a/netbox_bgp/graphql/types.py +++ b/netbox_bgp/graphql/types.py @@ -1,4 +1,4 @@ -from typing import Annotated, List +from typing import Annotated, List, TYPE_CHECKING, Optional, Union import strawberry import strawberry_django from netbox.graphql.types import NetBoxObjectType @@ -15,7 +15,8 @@ CommunityList, CommunityListRule, ASPathList, - ASPathListRule + ASPathListRule, + Redistributing, ) from .filters import ( NetBoxBGPCommunityFilter, @@ -28,9 +29,11 @@ NetBoxBGPCommunityListFilter, NetBoxBGPCommunityListRuleFilter, NetBoxBGPASPathListFilter, - NetBoxBGPASPathListRuleFilter + NetBoxBGPASPathListRuleFilter, + NetBoxBGPRedistributingFilter, ) + @strawberry_django.type(ASPathList, fields="__all__", filters=NetBoxBGPASPathListFilter) class ASPathListType(NetBoxObjectType): name: str @@ -39,6 +42,18 @@ class ASPathListType(NetBoxObjectType): Annotated["ASPathListRuleType", strawberry.lazy("netbox_bgp.graphql.types")] ] + @strawberry_django.field + def scope(self) -> Annotated[Union[ + Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')], + Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')], + Annotated["LocationType", strawberry.lazy('dcim.graphql.types')], + Annotated["RackType", strawberry.lazy('dcim.graphql.types')], + Annotated["RegionType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("CommunityScopeType")] | None: + return self.scope + @strawberry_django.type(ASPathListRule, fields="__all__", filters=NetBoxBGPASPathListRuleFilter) class ASPathListRuleType(NetBoxObjectType): @@ -53,12 +68,23 @@ class ASPathListRuleType(NetBoxObjectType): @strawberry_django.type(Community, fields="__all__", filters=NetBoxBGPCommunityFilter) class CommunityType(NetBoxObjectType): - site: Annotated["SiteType", strawberry.lazy("dcim.graphql.types")] | None tenant: Annotated["TenantType", strawberry.lazy("tenancy.graphql.types")] | None status: str role: Annotated["RoleType", strawberry.lazy("ipam.graphql.types")] | None description: str + @strawberry_django.field + def scope(self) -> Annotated[Union[ + Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')], + Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')], + Annotated["LocationType", strawberry.lazy('dcim.graphql.types')], + Annotated["RackType", strawberry.lazy('dcim.graphql.types')], + Annotated["RegionType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("CommunityScopeType")] | None: + return self.scope + @strawberry_django.type(BGPSession, fields="__all__", filters=NetBoxBGPSessionFilter) class BGPSessionType(NetBoxObjectType): @@ -98,6 +124,18 @@ class BGPPeerGroupType(NetBoxObjectType): Annotated["RoutingPolicyType", strawberry.lazy("netbox_bgp.graphql.types")] ] + @strawberry_django.field + def scope(self) -> Annotated[Union[ + Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')], + Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')], + Annotated["LocationType", strawberry.lazy('dcim.graphql.types')], + Annotated["RackType", strawberry.lazy('dcim.graphql.types')], + Annotated["RegionType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("BGPPeerGroupScopeType")] | None: + return self.scope + @strawberry_django.type(RoutingPolicy, fields="__all__", filters=NetBoxBGPRoutingPolicyFilter) class RoutingPolicyType(NetBoxObjectType): @@ -108,6 +146,18 @@ class RoutingPolicyType(NetBoxObjectType): Annotated["RoutingPolicyRuleType", strawberry.lazy("netbox_bgp.graphql.types")] ] + @strawberry_django.field + def scope(self) -> Annotated[Union[ + Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')], + Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')], + Annotated["LocationType", strawberry.lazy('dcim.graphql.types')], + Annotated["RackType", strawberry.lazy('dcim.graphql.types')], + Annotated["RegionType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("RoutingPolicyScopeType")] | None: + return self.scope + @strawberry_django.type( RoutingPolicyRule, fields="__all__", filters=NetBoxBGPRoutingPolicyRuleFilter @@ -146,6 +196,18 @@ class PrefixListType(NetBoxObjectType): Annotated["PrefixListRuleType", strawberry.lazy("netbox_bgp.graphql.types")] ] + @strawberry_django.field + def scope(self) -> Annotated[Union[ + Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')], + Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')], + Annotated["LocationType", strawberry.lazy('dcim.graphql.types')], + Annotated["RackType", strawberry.lazy('dcim.graphql.types')], + Annotated["RegionType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("PrefixListScopeType")] | None: + return self.scope + @strawberry_django.type(PrefixListRule, fields="__all__", filters=NetBoxBGPPrefixListRuleFilter) class PrefixListRuleType(NetBoxObjectType): @@ -169,6 +231,18 @@ class CommunityListType(NetBoxObjectType): Annotated["CommunityListRuleType", strawberry.lazy("netbox_bgp.graphql.types")] ] + @strawberry_django.field + def scope(self) -> Annotated[Union[ + Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')], + Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')], + Annotated["LocationType", strawberry.lazy('dcim.graphql.types')], + Annotated["RackType", strawberry.lazy('dcim.graphql.types')], + Annotated["RegionType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("CommunityListScopeType")] | None: + return self.scope + @strawberry_django.type( CommunityListRule, fields="__all__", filters=NetBoxBGPCommunityListRuleFilter @@ -180,3 +254,28 @@ class CommunityListRuleType(NetBoxObjectType): action: str community: Annotated["CommunityType", strawberry.lazy("netbox_bgp.graphql.types")] description: str + + +@strawberry_django.type(Redistributing, fields="__all__", filters=NetBoxBGPRedistributingFilter) +class RedistributingType(NetBoxObjectType): + name: str + tenant: Annotated["TenantType", strawberry.lazy("tenancy.graphql.types")] | None + device: Annotated["DeviceType", strawberry.lazy("dcim.graphql.types")] | None + virtualmachine: Annotated["VirtualMachineType", strawberry.lazy("virtualization.graphql.types")] | None + description: str + redistribute_source: str + redistribute_policy: ( + Annotated["RoutingPolicyType", strawberry.lazy("netbox_bgp.graphql.types")] + ) + + @strawberry_django.field + def scope(self) -> Annotated[Union[ + Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')], + Annotated["ClusterGroupType", strawberry.lazy('virtualization.graphql.types')], + Annotated["LocationType", strawberry.lazy('dcim.graphql.types')], + Annotated["RackType", strawberry.lazy('dcim.graphql.types')], + Annotated["RegionType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteType", strawberry.lazy('dcim.graphql.types')], + Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')], + ], strawberry.union("RedistributingScopeType")] | None: + return self.scope diff --git a/netbox_bgp/migrations/0037_redistributing_alter_bgpsession_options_and_more.py b/netbox_bgp/migrations/0037_redistributing_alter_bgpsession_options_and_more.py new file mode 100644 index 0000000..4037d02 --- /dev/null +++ b/netbox_bgp/migrations/0037_redistributing_alter_bgpsession_options_and_more.py @@ -0,0 +1,198 @@ +# Generated by Django 5.2.8 on 2025-12-04 14:44 + +import django.db.models.deletion +import netbox.models.deletion +import taggit.managers +import utilities.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('dcim', '0215_rackreservation_status'), + ('extras', '0133_make_cf_minmax_decimal'), + ('ipam', '0083_vlangroup_populate_total_vlan_ids'), + ('netbox_bgp', '0036_alter_routingpolicy_options_routingpolicy_weight'), + ('tenancy', '0020_remove_contactgroupmembership'), + ('virtualization', '0048_populate_mac_addresses'), + ] + + operations = [ + migrations.CreateModel( + name='Redistributing', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)), + ('created', models.DateTimeField(auto_now_add=True, null=True)), + ('last_updated', models.DateTimeField(auto_now=True, null=True)), + ('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)), + ('name', models.CharField(max_length=256)), + ('scope_id', models.PositiveBigIntegerField(blank=True, null=True)), + ('description', models.CharField(blank=True, max_length=200)), + ('redistribute_source', models.CharField(max_length=50)), + ('comments', models.TextField(blank=True)), + ], + options={ + 'verbose_name_plural': 'Redistributing', + }, + bases=(netbox.models.deletion.DeleteMixin, models.Model), + ), + migrations.AlterModelOptions( + name='bgpsession', + options={'ordering': ('name', 'pk')}, + ), + migrations.AlterUniqueTogether( + name='aspathlist', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='bgppeergroup', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='communitylist', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='prefixlist', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='routingpolicy', + unique_together=set(), + ), + migrations.AddField( + model_name='aspathlist', + name='scope_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='aspathlist', + name='scope_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='bgppeergroup', + name='scope_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='bgppeergroup', + name='scope_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='communitylist', + name='scope_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='communitylist', + name='scope_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='prefixlist', + name='scope_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='prefixlist', + name='scope_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='routingpolicy', + name='scope_id', + field=models.PositiveBigIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name='routingpolicy', + name='scope_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddIndex( + model_name='aspathlist', + index=models.Index(fields=['scope_type', 'scope_id'], name='netbox_bgp__scope_t_903943_idx'), + ), + migrations.AddIndex( + model_name='bgppeergroup', + index=models.Index(fields=['scope_type', 'scope_id'], name='netbox_bgp__scope_t_376c40_idx'), + ), + migrations.AddIndex( + model_name='communitylist', + index=models.Index(fields=['scope_type', 'scope_id'], name='netbox_bgp__scope_t_cec8fa_idx'), + ), + migrations.AddIndex( + model_name='prefixlist', + index=models.Index(fields=['scope_type', 'scope_id'], name='netbox_bgp__scope_t_a37fb1_idx'), + ), + migrations.AddIndex( + model_name='routingpolicy', + index=models.Index(fields=['scope_type', 'scope_id'], name='netbox_bgp__scope_t_484ff4_idx'), + ), + migrations.AddConstraint( + model_name='aspathlist', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name', 'description'), name='netbox_bgp_aspathlist_unique_scope_name_description'), + ), + migrations.AddConstraint( + model_name='bgppeergroup', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name', 'description'), name='netbox_bgp_bgppeergroup_unique_scope_name_description'), + ), + migrations.AddConstraint( + model_name='communitylist', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name', 'description'), name='netbox_bgp_communitylist_unique_scope_name_description'), + ), + migrations.AddConstraint( + model_name='prefixlist', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name', 'description', 'family'), name='netbox_bgp_prefixlist_unique_scope_name_description'), + ), + migrations.AddConstraint( + model_name='routingpolicy', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name', 'description'), name='netbox_bgp_routingpolicy_unique_scope_name_description'), + ), + migrations.AddField( + model_name='redistributing', + name='device', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.device'), + ), + migrations.AddField( + model_name='redistributing', + name='redistribute_policy', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='redistributing', to='netbox_bgp.routingpolicy'), + ), + migrations.AddField( + model_name='redistributing', + name='scope_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'), + ), + migrations.AddField( + model_name='redistributing', + name='tags', + field=taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag'), + ), + migrations.AddField( + model_name='redistributing', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='tenancy.tenant'), + ), + migrations.AddField( + model_name='redistributing', + name='virtualmachine', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='virtualization.virtualmachine'), + ), + migrations.AddField( + model_name='redistributing', + name='vrf', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='ipam.vrf'), + ), + migrations.AddIndex( + model_name='redistributing', + index=models.Index(fields=['scope_type', 'scope_id'], name='netbox_bgp__scope_t_a7effb_idx'), + ), + migrations.AddConstraint( + model_name='redistributing', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name', 'device', 'virtualmachine', 'redistribute_source', 'vrf'), name='netbox_bgp_redistributing_unique_scope_name_description'), + ), + ] diff --git a/netbox_bgp/models.py b/netbox_bgp/models.py index dd49c01..0cb2b5c 100644 --- a/netbox_bgp/models.py +++ b/netbox_bgp/models.py @@ -1,12 +1,19 @@ from django.urls import reverse from django.db import models +from django.contrib.contenttypes.fields import GenericForeignKey from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator from django.core.exceptions import ValidationError from netbox.models import NetBoxModel from ipam.fields import IPNetworkField -from .choices import IPAddressFamilyChoices, SessionStatusChoices, ActionChoices, CommunityStatusChoices +from .choices import ( + IPAddressFamilyChoices, + RedistributeSourceChoices, + SessionStatusChoices, + ActionChoices, + CommunityStatusChoices +) class ASPathList(NetBoxModel): @@ -20,14 +27,36 @@ class ASPathList(NetBoxModel): max_length=200, blank=True ) + scope_type = models.ForeignKey( + to='contenttypes.ContentType', + on_delete=models.CASCADE, + blank=True, + null=True + ) + scope_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + scope = GenericForeignKey( + ct_field='scope_type', + fk_field='scope_id' + ) comments = models.TextField( blank=True ) class Meta: verbose_name_plural = 'AS Path Lists' - unique_together = ['name', 'description'] ordering = ['name'] + indexes = ( + models.Index(fields=('scope_type', 'scope_id')), + ) + constraints = ( + models.UniqueConstraint( + fields=('scope_type', 'scope_id', 'name', 'description'), + name='%(app_label)s_%(class)s_unique_scope_name_description' + ), + ) def __str__(self): return self.name @@ -84,6 +113,20 @@ class RoutingPolicy(NetBoxModel): max_length=200, blank=True ) + scope_type = models.ForeignKey( + to='contenttypes.ContentType', + on_delete=models.CASCADE, + blank=True, + null=True + ) + scope_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + scope = GenericForeignKey( + ct_field='scope_type', + fk_field='scope_id' + ) comments = models.TextField( blank=True ) @@ -94,8 +137,16 @@ class RoutingPolicy(NetBoxModel): class Meta: verbose_name_plural = 'Routing Policies' - unique_together = ['name', 'description'] ordering = ['weight', 'name'] + indexes = ( + models.Index(fields=('scope_type', 'scope_id')), + ) + constraints = ( + models.UniqueConstraint( + fields=('scope_type', 'scope_id', 'name', 'description'), + name='%(app_label)s_%(class)s_unique_scope_name_description' + ), + ) def __str__(self): return self.name @@ -114,6 +165,20 @@ class BGPPeerGroup(NetBoxModel): max_length=200, blank=True ) + scope_type = models.ForeignKey( + to='contenttypes.ContentType', + on_delete=models.CASCADE, + blank=True, + null=True + ) + scope_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + scope = GenericForeignKey( + ct_field='scope_type', + fk_field='scope_id' + ) import_policies = models.ManyToManyField( RoutingPolicy, blank=True, @@ -130,8 +195,16 @@ class BGPPeerGroup(NetBoxModel): class Meta: verbose_name_plural = 'Peer Groups' - unique_together = ['name', 'description'] ordering = ['name'] + indexes = ( + models.Index(fields=('scope_type', 'scope_id')), + ) + constraints = ( + models.UniqueConstraint( + fields=('scope_type', 'scope_id', 'name', 'description'), + name='%(app_label)s_%(class)s_unique_scope_name_description' + ), + ) def __str__(self): return self.name @@ -211,14 +284,36 @@ class CommunityList(NetBoxModel): max_length=200, blank=True ) + scope_type = models.ForeignKey( + to='contenttypes.ContentType', + on_delete=models.CASCADE, + blank=True, + null=True + ) + scope_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + scope = GenericForeignKey( + ct_field='scope_type', + fk_field='scope_id' + ) comments = models.TextField( blank=True ) class Meta: verbose_name_plural = 'Community Lists' - unique_together = ['name', 'description'] ordering = ['name'] + indexes = ( + models.Index(fields=('scope_type', 'scope_id')), + ) + constraints = ( + models.UniqueConstraint( + fields=('scope_type', 'scope_id', 'name', 'description'), + name='%(app_label)s_%(class)s_unique_scope_name_description' + ), + ) def __str__(self): return self.name @@ -279,14 +374,37 @@ class PrefixList(NetBoxModel): max_length=10, choices=IPAddressFamilyChoices ) + scope_type = models.ForeignKey( + to='contenttypes.ContentType', + on_delete=models.CASCADE, + blank=True, + null=True + ) + scope_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + scope = GenericForeignKey( + ct_field='scope_type', + fk_field='scope_id' + ) comments = models.TextField( blank=True ) class Meta: verbose_name_plural = 'Prefix Lists' - unique_together = ['name', 'description', 'family'] ordering = ['name'] + indexes = ( + models.Index(fields=('scope_type', 'scope_id')), + ) + constraints = ( + models.UniqueConstraint( + fields=('scope_type', 'scope_id', 'name', 'description', 'family'), + name='%(app_label)s_%(class)s_unique_scope_name_description' + ), + ) + def __str__(self): return self.name @@ -500,7 +618,7 @@ def label(self): """ if self.name: return self.name - return f'{self.remote_address}:{self.remote_as}' + return f'{self.remote_address}:{self.remote_as}' class RoutingPolicyRule(NetBoxModel): @@ -560,7 +678,6 @@ class RoutingPolicyRule(NetBoxModel): ) class Meta: - ordering = ['routing_policy', 'index'] unique_together = ('routing_policy', 'index') ordering = ['routing_policy', 'index'] @@ -617,3 +734,96 @@ def set_statements(self): if self.set_actions: return self.set_actions return {} + + +class Redistributing(NetBoxModel): + name = models.CharField( + max_length=256, + ) + scope_type = models.ForeignKey( + to='contenttypes.ContentType', + on_delete=models.CASCADE, + blank=True, + null=True + ) + scope_id = models.PositiveBigIntegerField( + blank=True, + null=True + ) + scope = GenericForeignKey( + ct_field='scope_type', + fk_field='scope_id' + ) + vrf = models.ForeignKey( + to='ipam.VRF', + on_delete=models.CASCADE, + blank=True, + null=True + ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + blank=True, + null=True + ) + device = models.ForeignKey( + to='dcim.Device', + on_delete=models.CASCADE, + blank=True, + null=True, + ) + virtualmachine = models.ForeignKey( + to='virtualization.VirtualMachine', + on_delete=models.CASCADE, + null=True, + blank=True, + ) + description = models.CharField( + max_length=200, + blank=True, + ) + redistribute_source = models.CharField( + max_length=50, + choices=RedistributeSourceChoices, + ) + redistribute_policy = models.ForeignKey( + RoutingPolicy, + related_name='redistributing', + on_delete=models.CASCADE, + ) + comments = models.TextField( + blank=True + ) + + class Meta: + verbose_name_plural = 'Redistributing' + indexes = ( + models.Index(fields=('scope_type', 'scope_id')), + ) + constraints = ( + models.UniqueConstraint( + fields=('scope_type', 'scope_id', 'name', 'device', 'virtualmachine', 'redistribute_source', 'vrf'), + name='%(app_label)s_%(class)s_unique_scope_name_description' + ), + ) + + def __str__(self): + if self.device: + return f'{self.device}:{self.name}' + elif self.virtualmachine: + return f'{self.virtualmachine}:{self.name}' + else: + return f':{self.name}' + + def clean(self): + super().clean() + if not self.device and not self.virtualmachine: + raise ValidationError('You need to fill one of required fields: "device" or "virtualmachine".') + if self.device and self.virtualmachine: + raise ValidationError('You can to fill only one of required fields: "device" or "virtualmachine".') + + def get_redistribute_source_color(self): + return RedistributeSourceChoices.colors.get(self.redistribute_source) + + def get_absolute_url(self): + return reverse('plugins:netbox_bgp:redistributing', args=[self.pk]) \ No newline at end of file diff --git a/netbox_bgp/navigation.py b/netbox_bgp/navigation.py index 40f2f59..fa0d56f 100644 --- a/netbox_bgp/navigation.py +++ b/netbox_bgp/navigation.py @@ -193,7 +193,26 @@ permissions=['netbox_bgp.add_bgppeergroup'], ), ), - ) + ), + PluginMenuItem( + link='plugins:netbox_bgp:redistributing_list', + link_text='Redistributing', + permissions=['netbox_bgp.view_redistributing'], + buttons=( + PluginMenuButton( + link='plugins:netbox_bgp:redistributing_add', + title='Add', + icon_class='mdi mdi-plus-thick', + permissions=['netbox_bgp.add_redistributing'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:redistributing_bulk_import', + title='Import', + icon_class='mdi mdi-upload', + permissions=['netbox_bgp.add_redistributing'], + ) + ), + ), ) diff --git a/netbox_bgp/tables.py b/netbox_bgp/tables.py index bdac655..dfd2bc6 100644 --- a/netbox_bgp/tables.py +++ b/netbox_bgp/tables.py @@ -1,15 +1,16 @@ import django_tables2 as tables from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ from django_tables2.utils import A -from netbox.tables import NetBoxTable +from netbox.tables import NetBoxTable, columns from netbox.tables.columns import ChoiceFieldColumn, TagColumn from .models import ( Community, BGPSession, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule, PrefixList, PrefixListRule, CommunityList, CommunityListRule, - ASPathList, ASPathListRule + ASPathList, ASPathListRule, Redistributing, ) @@ -36,10 +37,18 @@ class ASPathListTable(NetBoxTable): name = tables.LinkColumn() + scope_type = columns.ContentTypeColumn( + verbose_name=_('Scope Type'), + ) + scope = tables.Column( + verbose_name=_('Scope'), + linkify=True, + orderable=False + ) class Meta(NetBoxTable.Meta): model = ASPathList - fields = ('pk', 'name', 'description', 'actions') + fields = ('pk', 'name', 'description', 'scope_type', 'scope', 'actions') class ASPathListRuleTable(NetBoxTable): @@ -81,10 +90,18 @@ class Meta(NetBoxTable.Meta): class CommunityListTable(NetBoxTable): name = tables.LinkColumn() + scope_type = columns.ContentTypeColumn( + verbose_name=_('Scope Type'), + ) + scope = tables.Column( + verbose_name=_('Scope'), + linkify=True, + orderable=False + ) class Meta(NetBoxTable.Meta): model = CommunityList - fields = ('pk', 'name', 'description', 'actions') + fields = ('pk', 'name', 'description', 'scope_type', 'scope', 'actions') class CommunityListRuleTable(NetBoxTable): @@ -144,10 +161,18 @@ class Meta(NetBoxTable.Meta): class RoutingPolicyTable(NetBoxTable): name = tables.LinkColumn() + scope_type = columns.ContentTypeColumn( + verbose_name=_('Scope Type'), + ) + scope = tables.Column( + verbose_name=_('Scope'), + linkify=True, + orderable=False + ) class Meta(NetBoxTable.Meta): model = RoutingPolicy - fields = ('pk', 'name', 'description', 'weight', 'actions') + fields = ('pk', 'name', 'description', 'scope_type', 'scope', 'weight', 'actions') class BGPPeerGroupTable(NetBoxTable): @@ -160,6 +185,14 @@ class BGPPeerGroupTable(NetBoxTable): template_code=POLICIES, orderable=False ) + scope_type = columns.ContentTypeColumn( + verbose_name=_('Scope Type'), + ) + scope = tables.Column( + verbose_name=_('Scope'), + linkify=True, + orderable=False + ) tags = TagColumn( url_name='plugins:netbox_bgp:bgppeergroup_list' ) @@ -167,7 +200,7 @@ class BGPPeerGroupTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = BGPPeerGroup fields = ( - 'pk', 'name', 'description', 'tags', + 'pk', 'name', 'description', 'scope_type', 'scope', 'tags', 'import_policies', 'export_policies', 'actions' ) default_columns = ( @@ -195,10 +228,18 @@ class Meta(NetBoxTable.Meta): class PrefixListTable(NetBoxTable): name = tables.LinkColumn() family = ChoiceFieldColumn() + scope_type = columns.ContentTypeColumn( + verbose_name=_('Scope Type'), + ) + scope = tables.Column( + verbose_name=_('Scope'), + linkify=True, + orderable=False + ) class Meta(NetBoxTable.Meta): model = PrefixList - fields = ('pk', 'name', 'description', 'family', 'actions') + fields = ('pk', 'name', 'description', 'family', 'scope_type', 'scope', 'actions') class PrefixListRuleTable(NetBoxTable): @@ -220,3 +261,37 @@ class Meta(NetBoxTable.Meta): 'pk', 'prefix_list', 'index', 'action', 'network', 'ge', 'le' ) + + +class RedistributingTable(NetBoxTable): + name = tables.LinkColumn() + device = tables.LinkColumn() + virtualmachine = tables.LinkColumn() + redistribute_source = ChoiceFieldColumn( + default=AVAILABLE_LABEL + ) + redistribute_policy = tables.LinkColumn() + scope_type = columns.ContentTypeColumn( + verbose_name=_('Scope Type'), + ) + scope = tables.Column( + verbose_name=_('Scope'), + linkify=True, + orderable=False + ) + vrf = tables.LinkColumn() + + tenant = tables.TemplateColumn( + template_code=COL_TENANT + ) + + class Meta(NetBoxTable.Meta): + model = Redistributing + fields = ( + 'pk', 'name', 'device', 'virtualmachine', 'redistribute_source', + 'redistribute_policy', 'scope_type', 'scope', 'vrf', 'description', 'tenant' + ) + default_columns = ( + 'pk', 'name', 'device', 'virtualmachine', 'redistribute_source', + 'redistribute_policy', 'scope_type', 'scope', 'vrf', 'description', 'tenant' + ) diff --git a/netbox_bgp/template_content.py b/netbox_bgp/template_content.py index 19f4b87..7c7bd3b 100644 --- a/netbox_bgp/template_content.py +++ b/netbox_bgp/template_content.py @@ -11,8 +11,8 @@ from virtualization.models import VirtualMachine from .filtersets import BGPSessionFilterSet -from .models import BGPSession -from .tables import BGPSessionTable +from .models import BGPSession, Redistributing +from .tables import BGPSessionTable, RedistributingTable # Load configuration config = getattr(settings, "PLUGINS_CONFIG", {}).get("netbox_bgp", {}) @@ -238,28 +238,39 @@ def get_children( )(DeviceBGPSessionsView) -class DeviceBGPSession(PluginTemplateExtension): - models = ("dcim.device",) +class DeviceRelatedObjects(PluginTemplateExtension): + models = ('dcim.device',) - def left_page(self): - if self.context["config"].get("device_ext_page") == "left": - return self.x_page() - return "" + def full_width_page(self): + obj = self.context['object'] + sess = BGPSession.objects.filter(device=obj) + sess_table = BGPSessionTable(sess) + redistributing = Redistributing.objects.filter(device=obj) + redistributing_table = RedistributingTable(redistributing) + return self.render( + 'netbox_bgp/device_extend.html', + extra_context={ + 'related_session_table': sess_table, + 'related_redistributing_table': redistributing_table + } + ) - def right_page(self): - if self.context["config"].get("device_ext_page") == "right": - return self.x_page() - return "" - def full_width_page(self): - if self.context["config"].get("device_ext_page") == "full_width": - return self.x_page() - return "" +class VirtualmachineRelatedObjects(PluginTemplateExtension): + models = ('virtualization.virtualmachine',) - def x_page(self): + def full_width_page(self): + obj = self.context['object'] + sess = BGPSession.objects.filter(virtualmachine=obj) + sess_table = BGPSessionTable(sess) + redistributing = Redistributing.objects.filter(virtualmachine=obj) + redistributing_table = RedistributingTable(redistributing) return self.render( - "netbox_bgp/device_extend.html", + 'netbox_bgp/device_extend.html', + extra_context={ + 'related_session_table': sess_table, + 'related_redistributing_table': redistributing_table + } ) - -template_extensions = [DeviceBGPSession] +template_extensions = [DeviceRelatedObjects, VirtualmachineRelatedObjects] diff --git a/netbox_bgp/templates/netbox_bgp/aspathlist.html b/netbox_bgp/templates/netbox_bgp/aspathlist.html index 04a3314..a95eb0d 100644 --- a/netbox_bgp/templates/netbox_bgp/aspathlist.html +++ b/netbox_bgp/templates/netbox_bgp/aspathlist.html @@ -35,6 +35,16 @@
Description {{ object.description|placeholder }} + + Scope + + {% if object.scope %} + {{ object.scope }} + {% else %} + None + {% endif %} + + diff --git a/netbox_bgp/templates/netbox_bgp/bgppeergroup.html b/netbox_bgp/templates/netbox_bgp/bgppeergroup.html index 0239318..f71e404 100644 --- a/netbox_bgp/templates/netbox_bgp/bgppeergroup.html +++ b/netbox_bgp/templates/netbox_bgp/bgppeergroup.html @@ -27,6 +27,16 @@
Description {{ object.description|placeholder }} + + Scope + + {% if object.scope %} + {{ object.scope }} + {% else %} + None + {% endif %} + + diff --git a/netbox_bgp/templates/netbox_bgp/communitylist.html b/netbox_bgp/templates/netbox_bgp/communitylist.html index b36e5dd..e9306d0 100644 --- a/netbox_bgp/templates/netbox_bgp/communitylist.html +++ b/netbox_bgp/templates/netbox_bgp/communitylist.html @@ -35,6 +35,16 @@
Description {{ object.description|placeholder }} + + Scope + + {% if object.scope %} + {{ object.scope }} + {% else %} + None + {% endif %} + + diff --git a/netbox_bgp/templates/netbox_bgp/device_extend.html b/netbox_bgp/templates/netbox_bgp/device_extend.html index 70b14fc..8348493 100644 --- a/netbox_bgp/templates/netbox_bgp/device_extend.html +++ b/netbox_bgp/templates/netbox_bgp/device_extend.html @@ -1,5 +1,14 @@ +{% load render_table from django_tables2 %} {% load helpers %} +
+
+ Related Redistributing +
+
+ {% render_table related_redistributing_table 'inc/table.html' %} +
+
Related BGP Sessions diff --git a/netbox_bgp/templates/netbox_bgp/prefixlist.html b/netbox_bgp/templates/netbox_bgp/prefixlist.html index a669d45..3185730 100644 --- a/netbox_bgp/templates/netbox_bgp/prefixlist.html +++ b/netbox_bgp/templates/netbox_bgp/prefixlist.html @@ -39,6 +39,16 @@
Description {{ object.description|placeholder }} + + Scope + + {% if object.scope %} + {{ object.scope }} + {% else %} + None + {% endif %} + +
diff --git a/netbox_bgp/templates/netbox_bgp/redistributing.html b/netbox_bgp/templates/netbox_bgp/redistributing.html new file mode 100644 index 0000000..eff872a --- /dev/null +++ b/netbox_bgp/templates/netbox_bgp/redistributing.html @@ -0,0 +1,114 @@ +{% extends 'generic/object.html' %} +{% load buttons %} +{% load custom_links %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} + +{% block breadcrumbs %} + +{% endblock %} + + +{% block content %} +
+
+
+
+ Redistributing +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Name{{ object.name | placeholder}}
VRF + {% if object.vrf %} + {{ object.vrf }} + {% else %} + {{ "" | placeholder }} + {% endif %} +
Device + {% if object.device %} + {{ object.device }} + {% else %} + {{ "" | placeholder }} + {% endif %} +
Virtual Machine + {% if object.virtualmachine %} + {{ object.virtualmachine }} + {% else %} + {{ "" | placeholder }} + {% endif %} +
Redistribute source + {% if object.redistribute_source %} + {% badge object.get_redistribute_source_display bg_color=object.get_redistribute_source_color %} + {% else %} + {{ "" | placeholder }} + {% endif %} +
Redistribute policy + {% if object.redistribute_policy %} + {{ object.redistribute_policy }} + {% else %} + {{ "" | placeholder }} + {% endif %} +
Scope + {% if object.scope %} + {{ object.scope }} + {% else %} + None + {% endif %} +
Description{{ object.description | placeholder }}
Tenant + {% if object.tenant %} + {{ object.tenant }} + {% else %} + {{ "" | placeholder }} + {% endif %} +
+
+
+ {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} + {% plugin_left_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock %} \ No newline at end of file diff --git a/netbox_bgp/templates/netbox_bgp/routingpolicy.html b/netbox_bgp/templates/netbox_bgp/routingpolicy.html index 9e58b80..3459f06 100644 --- a/netbox_bgp/templates/netbox_bgp/routingpolicy.html +++ b/netbox_bgp/templates/netbox_bgp/routingpolicy.html @@ -39,6 +39,16 @@
Weight {{ object.weight|placeholder }} + + Scope + + {% if object.scope %} + {{ object.scope }} + {% else %} + None + {% endif %} + + @@ -59,6 +69,19 @@
+
+
+
+
+ Related Redistributing +
+
+ {% render_table related_redistributing_table 'inc/table.html' %} +
+ {% plugin_full_width_page object %} +
+
+
diff --git a/netbox_bgp/templates/netbox_bgp/virtualmachine_extend.html b/netbox_bgp/templates/netbox_bgp/virtualmachine_extend.html new file mode 100644 index 0000000..677d526 --- /dev/null +++ b/netbox_bgp/templates/netbox_bgp/virtualmachine_extend.html @@ -0,0 +1,18 @@ +{% load render_table from django_tables2 %} + +
+
+ Related Redistributing +
+
+ {% render_table related_redistributing_table 'inc/table.html' %} +
+
+
+
+ Related BGP Sessions +
+
+ {% render_table related_session_table 'inc/table.html' %} +
+
diff --git a/netbox_bgp/urls.py b/netbox_bgp/urls.py index 827d733..fa98f38 100644 --- a/netbox_bgp/urls.py +++ b/netbox_bgp/urls.py @@ -4,7 +4,7 @@ from .models import ( BGPSession, Community, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule, PrefixList, - PrefixListRule, CommunityList, CommunityListRule + PrefixListRule, CommunityList, CommunityListRule, Redistributing ) from . import views @@ -118,4 +118,14 @@ "prefix-list-rule//", include(get_model_urls("netbox_bgp", "prefixlistrule")), ), + + # Redistributing + path( + "redistributing/", + include(get_model_urls("netbox_bgp", "redistributing", detail=False)), + ), + path( + "redistributing//", + include(get_model_urls("netbox_bgp", "redistributing")), + ), ) diff --git a/netbox_bgp/views.py b/netbox_bgp/views.py index 81d2f2a..f87ff6f 100644 --- a/netbox_bgp/views.py +++ b/netbox_bgp/views.py @@ -11,7 +11,7 @@ Community, BGPSession, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule, PrefixList, PrefixListRule, CommunityList, CommunityListRule, - ASPathList, ASPathListRule + ASPathList, ASPathListRule, Redistributing ) from . import filtersets, forms, tables @@ -64,7 +64,7 @@ class CommunityBulkImportView(generic.BulkImportView): @register_model_view(CommunityList, "list", path="", detail=False) class CommunityListListView(generic.ObjectListView): - queryset = CommunityList.objects.all() + queryset = CommunityList.objects.select_related('scope_type').prefetch_related('tags') filterset = filtersets.CommunityListFilterSet filterset_form = forms.CommunityListFilterForm table = tables.CommunityListTable @@ -149,7 +149,19 @@ class CommunityListRuleView(generic.ObjectView): @register_model_view(BGPSession, "list", path="", detail=False) class BGPSessionListView(generic.ObjectListView): - queryset = BGPSession.objects.all() + queryset = BGPSession.objects.select_related( + 'device', + 'virtualmachine', + 'local_address', + 'remote_address', + 'local_as', + 'remote_as', + 'peer_group', + 'prefix_list_in', + 'prefix_list_out', + 'site', + 'tenant' + ).prefetch_related('import_policies', 'export_policies', 'tags') filterset = filtersets.BGPSessionFilterSet filterset_form = forms.BGPSessionFilterForm table = tables.BGPSessionTable @@ -218,7 +230,7 @@ class BGPSessionDeleteView(generic.ObjectDeleteView): @register_model_view(RoutingPolicy, "list", path="", detail=False) class RoutingPolicyListView(generic.ObjectListView): - queryset = RoutingPolicy.objects.all() + queryset = RoutingPolicy.objects.select_related('scope_type').prefetch_related('tags') filterset = filtersets.RoutingPolicyFilterSet filterset_form = forms.RoutingPolicyFilterForm table = tables.RoutingPolicyTable @@ -255,11 +267,17 @@ def get_extra_context(self, request, instance): ) sess = sess.distinct() sess_table = tables.BGPSessionTable(sess) + redistiributing = Redistributing.objects.filter( + Q(redistribute_policy=instance) + ) + redistiributing = redistiributing.distinct() + redistiributing_table = tables.RedistributingTable(redistiributing) rules = instance.rules.all() rules_table = tables.RoutingPolicyRuleTable(rules) return { 'rules_table': rules_table, - 'related_session_table': sess_table + 'related_session_table': sess_table, + 'related_redistributing_table': redistiributing_table, } @register_model_view(RoutingPolicy, "delete") @@ -328,7 +346,9 @@ class RoutingPolicyRuleImportView(generic.BulkImportView): @register_model_view(BGPPeerGroup, "list", path="", detail=False) class BGPPeerGroupListView(generic.ObjectListView): - queryset = BGPPeerGroup.objects.all() + queryset = BGPPeerGroup.objects.select_related( + 'scope_type' + ).prefetch_related('import_policies', 'export_policies', 'tags') filterset = filtersets.BGPPeerGroupFilterSet filterset_form = forms.BGPPeerGroupFilterForm table = tables.BGPPeerGroupTable @@ -390,7 +410,7 @@ class BGPPeerGroupBulkEditView(generic.BulkEditView): @register_model_view(PrefixList, "list", path="", detail=False) class PrefixListListView(generic.ObjectListView): - queryset = PrefixList.objects.all() + queryset = PrefixList.objects.select_related('scope_type').prefetch_related('tags') filterset = filtersets.PrefixListFilterSet filterset_form = forms.PrefixListFilterForm table = tables.PrefixListTable @@ -501,7 +521,7 @@ def get_children(self, request, parent): @register_model_view(ASPathList, "list", path="", detail=False) class ASPathListListView(generic.ObjectListView): - queryset = ASPathList.objects.all() + queryset = ASPathList.objects.select_related('scope_type').prefetch_related('tags') filterset = filtersets.ASPathListFilterSet filterset_form = forms.ASPathListFilterForm table = tables.ASPathListTable @@ -589,3 +609,54 @@ class ASPathListRuleBulkImportView(generic.BulkImportView): class ASPathListRuleView(generic.ObjectView): queryset = ASPathListRule.objects.all() template_name = 'netbox_bgp/aspathlistrule.html' + + +# Redistributing + +@register_model_view(Redistributing, "list", path="", detail=False) +class RedistributingListView(generic.ObjectListView): + queryset = Redistributing.objects.all() + filterset = filtersets.RedistributingFilterSet + filterset_form = forms.RedistributingFilterForm + table = tables.RedistributingTable + +@register_model_view(Redistributing, "add", detail=False) +@register_model_view(Redistributing, "edit") +class RedistributingEditView(generic.ObjectEditView): + queryset = Redistributing.objects.all() + form = forms.RedistributingForm + +@register_model_view(Redistributing, "bulk_import", path="import", detail=False) +class RedistributingBulkImportView(generic.BulkImportView): + queryset = Redistributing.objects.all() + model_form = forms.RedistributingImportForm + +@register_model_view(Redistributing, "bulk_edit", path="edit", detail=False) +class RedistributingBulkEditView(generic.BulkEditView): + queryset = Redistributing.objects.all() + filterset = filtersets.RedistributingFilterSet + table = tables.RedistributingTable + form = forms.RedistributingBulkEditForm + +@register_model_view(Redistributing, "bulk_delete", path="delete", detail=False) +class RedistributingBulkDeleteView(generic.BulkDeleteView): + queryset = Redistributing.objects.all() + table = tables.RedistributingTable + +@register_model_view(Redistributing) +class RedistributingView(generic.ObjectView): + queryset = Redistributing.objects.select_related( + 'device', + 'virtualmachine', + 'redistribute_policy', + 'scope_type', + 'vrf', + 'tenant' + ).prefetch_related('tags') + table = tables.RedistributingTable + template_name = 'netbox_bgp/redistributing.html' + +@register_model_view(Redistributing, "delete") +class RedistributingDeleteView(generic.ObjectDeleteView): + queryset = Redistributing.objects.all() + default_return_url = 'plugins:netbox_bgp:redistributing_list'