From 7ee8f30f57a5f7e8d9aa8785cef4f194b11838d5 Mon Sep 17 00:00:00 2001 From: Chris Russell Date: Sat, 21 Jun 2025 22:39:45 +0100 Subject: [PATCH] Add ASPATH, fix graphql filters --- Makefile | 2 +- develop/Dockerfile | 2 +- netbox_bgp/api/serializers.py | 52 ++++++++++ netbox_bgp/api/urls.py | 5 +- netbox_bgp/api/views.py | 21 +++- netbox_bgp/filtersets.py | 38 +++++++- netbox_bgp/forms.py | 61 ++++++++++++ netbox_bgp/graphql/enums.py | 8 +- netbox_bgp/graphql/filters.py | 36 ++++++- netbox_bgp/graphql/schema.py | 10 ++ netbox_bgp/graphql/types.py | 23 +++++ ...ngpolicyrule_match_aspath_list_and_more.py | 57 +++++++++++ netbox_bgp/models.py | 71 ++++++++++++++ netbox_bgp/navigation.py | 82 +++++++++++++++- netbox_bgp/tables.py | 28 +++++- .../templates/netbox_bgp/aspathlist.html | 71 ++++++++++++++ .../templates/netbox_bgp/aspathlistrule.html | 42 ++++++++ netbox_bgp/urls.py | 18 ++++ netbox_bgp/views.py | 96 ++++++++++++++++++- 19 files changed, 708 insertions(+), 15 deletions(-) create mode 100644 netbox_bgp/migrations/0034_aspathlist_routingpolicyrule_match_aspath_list_and_more.py create mode 100644 netbox_bgp/templates/netbox_bgp/aspathlist.html create mode 100644 netbox_bgp/templates/netbox_bgp/aspathlistrule.html diff --git a/Makefile b/Makefile index 48aecd9..4a4e54a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PYTHON_VER?=3.10 -NETBOX_VER?=v4.0.2 +NETBOX_VER?=v4.3.2 NAME=netbox-bgp diff --git a/develop/Dockerfile b/develop/Dockerfile index 920d674..9b8badd 100644 --- a/develop/Dockerfile +++ b/develop/Dockerfile @@ -1,7 +1,7 @@ ARG python_ver=3.12 FROM python:${python_ver} -ARG netbox_ver=v4.3.1 +ARG netbox_ver=v4.3.2 ENV PYTHONUNBUFFERED 1 RUN mkdir -p /opt diff --git a/netbox_bgp/api/serializers.py b/netbox_bgp/api/serializers.py index a9f692b..ae740f6 100644 --- a/netbox_bgp/api/serializers.py +++ b/netbox_bgp/api/serializers.py @@ -18,11 +18,53 @@ PrefixListRule, CommunityList, CommunityListRule, + ASPathList, + ASPathListRule ) from netbox_bgp.choices import CommunityStatusChoices, SessionStatusChoices +class ASPathListSerializer(NetBoxModelSerializer): + url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:aspathlist-detail") + + class Meta: + model = ASPathList + fields = [ + "id", + "url", + "name", + "display", + "description", + "tags", + "custom_fields", + "comments", + ] + brief_fields = ("id", "url", "display", "name", "description") + + +class ASPathListRuleSerializer(NetBoxModelSerializer): + aspath_list = ASPathListSerializer(nested=True) + + class Meta: + model = ASPathListRule + fields = [ + "id", + "description", + "tags", + "custom_fields", + "display", + "aspath_list", + "created", + "last_updated", + "index", + "action", + "pattern", + "comments", + ] + brief_fields = ("id", "display", "description") + + class RoutingPolicySerializer(NetBoxModelSerializer): url = HyperlinkedIdentityField(view_name="plugins-api:netbox_bgp-api:routingpolicy-detail") @@ -286,6 +328,14 @@ class RoutingPolicyRuleSerializer(NetBoxModelSerializer): allow_null=True, many=True, ) + match_aspath_list = SerializedPKRelatedField( + queryset=ASPathList.objects.all(), + serializer=ASPathListSerializer, + nested=True, + required=False, + allow_null=True, + many=True, + ) class Meta: model = RoutingPolicyRule @@ -298,6 +348,7 @@ class Meta: "routing_policy", "match_community", "match_community_list", + "match_aspath_list", "match_custom", "set_actions", "match_ipv6_address", @@ -335,3 +386,4 @@ class Meta: "comments", ) brief_fields = ("id", "display", "description") + diff --git a/netbox_bgp/api/urls.py b/netbox_bgp/api/urls.py index 5cd1777..c8b8ddf 100644 --- a/netbox_bgp/api/urls.py +++ b/netbox_bgp/api/urls.py @@ -3,7 +3,8 @@ from .views import ( BGPSessionViewSet, RoutingPolicyViewSet, BGPPeerGroupViewSet, CommunityViewSet, PrefixListViewSet, PrefixListRuleViewSet, RoutingPolicyRuleViewSet, - CommunityListViewSet, CommunityListRuleViewSet, RootView + CommunityListViewSet, CommunityListRuleViewSet, RootView, + ASPathListViewSet, ASPathListRuleViewSet ) @@ -20,5 +21,7 @@ router.register('prefix-list-rule', PrefixListRuleViewSet) router.register('community-list', CommunityListViewSet) router.register('community-list-rule', CommunityListRuleViewSet) +router.register('aspath-list', ASPathListViewSet) +router.register('aspath-list-rule', ASPathListRuleViewSet) urlpatterns = router.urls diff --git a/netbox_bgp/api/views.py b/netbox_bgp/api/views.py index e6dc428..c23219b 100644 --- a/netbox_bgp/api/views.py +++ b/netbox_bgp/api/views.py @@ -4,17 +4,20 @@ from .serializers import ( BGPSessionSerializer, RoutingPolicySerializer, BGPPeerGroupSerializer, CommunitySerializer, PrefixListSerializer, PrefixListRuleSerializer, - RoutingPolicyRuleSerializer, CommunityListSerializer, CommunityListRuleSerializer + RoutingPolicyRuleSerializer, CommunityListSerializer, CommunityListRuleSerializer, + ASPathListSerializer, ASPathListRuleSerializer ) from netbox_bgp.models import ( BGPSession, RoutingPolicy, BGPPeerGroup, Community, PrefixList, PrefixListRule, - RoutingPolicyRule, CommunityList, CommunityListRule + RoutingPolicyRule, CommunityList, CommunityListRule, + ASPathList, ASPathListRule ) from netbox_bgp.filtersets import ( BGPSessionFilterSet, RoutingPolicyFilterSet, BGPPeerGroupFilterSet, CommunityFilterSet, PrefixListFilterSet, PrefixListRuleFilterSet, - RoutingPolicyRuleFilterSet, CommunityListFilterSet, CommunityListRuleFilterSet + RoutingPolicyRuleFilterSet, CommunityListFilterSet, CommunityListRuleFilterSet, + ASPathListFilterSet, ASPathListRuleFilterSet ) class RootView(APIRootView): @@ -74,3 +77,15 @@ class PrefixListRuleViewSet(NetBoxModelViewSet): queryset = PrefixListRule.objects.all() serializer_class = PrefixListRuleSerializer filterset_class = PrefixListRuleFilterSet + + +class ASPathListViewSet(NetBoxModelViewSet): + queryset = ASPathList.objects.all() + serializer_class = ASPathListSerializer + filterset_class = ASPathListFilterSet + + +class ASPathListRuleViewSet(NetBoxModelViewSet): + queryset = ASPathListRule.objects.all() + serializer_class = ASPathListRuleSerializer + filterset_class = ASPathListRuleFilterSet diff --git a/netbox_bgp/filtersets.py b/netbox_bgp/filtersets.py index aaa2014..3c33598 100644 --- a/netbox_bgp/filtersets.py +++ b/netbox_bgp/filtersets.py @@ -8,12 +8,48 @@ from .models import ( Community, BGPSession, RoutingPolicy, RoutingPolicyRule, BGPPeerGroup, PrefixList, PrefixListRule, CommunityList, - CommunityListRule + CommunityListRule, ASPathList, ASPathListRule ) from ipam.models import IPAddress, ASN from dcim.models import Device, Site from virtualization.models import VirtualMachine + +class ASPathListFilterSet(NetBoxModelFilterSet): + + class Meta: + model = ASPathList + fields = ['id', 'name', 'description'] + + def search(self, queryset, name, value): + """Perform the filtered search.""" + if not value.strip(): + return queryset + qs_filter = ( + Q(name__icontains=value) + | Q(description__icontains=value) + ) + return queryset.filter(qs_filter) + + +class ASPathListRuleFilterSet(NetBoxModelFilterSet): + + class Meta: + model = ASPathListRule + fields = ['id', 'action', 'aspath_list', 'aspath_list_id'] + + def search(self, queryset, name, value): + """Perform the filtered search.""" + if not value.strip(): + return queryset + qs_filter = ( + Q(action__icontains=value) + | Q(aspath_list__icontains=value) + | Q(aspath_list_id__icontains=value) + ) + return queryset.filter(qs_filter) + + class CommunityFilterSet(NetBoxModelFilterSet, TenancyFilterSet): class Meta: diff --git a/netbox_bgp/forms.py b/netbox_bgp/forms.py index f34f368..641b6d7 100644 --- a/netbox_bgp/forms.py +++ b/netbox_bgp/forms.py @@ -40,6 +40,8 @@ PrefixListRule, CommunityList, CommunityListRule, + ASPathList, + ASPathListRule, ) from .choices import ( @@ -50,6 +52,59 @@ from virtualization.models import VirtualMachine +class ASPathListFilterForm(NetBoxModelFilterSetForm): + model = ASPathList + q = forms.CharField(required=False, label="Search") + + tag = TagFilterField(model) + +class ASPathListRuleFilterForm(NetBoxModelFilterSetForm): + model = ASPathListRule + q = forms.CharField(required=False, label="Search") + aspath_list = DynamicModelChoiceField(queryset=ASPathList.objects.all(), required=False) + tag = TagFilterField(model) + + +class ASPathListForm(NetBoxModelForm): + + comments = CommentField() + + class Meta: + model = ASPathList + fields = ["name", "description", "tags", "comments"] + + +class ASPathListBulkEditForm(NetBoxModelBulkEditForm): + description = forms.CharField(max_length=200, required=False) + + model = ASPathList + nullable_fields = [ + "description", + ] + + +class ASPathListImportForm(NetBoxModelImportForm): + + class Meta: + model = ASPathList + fields = ["name", "description", "tags"] + + +class ASPathListRuleImportForm(NetBoxModelImportForm): + + class Meta: + model = ASPathListRule + fields = ["aspath_list", "index", "action", "pattern", "description", "tags", "comments"] + + +class ASPathListRuleForm(NetBoxModelForm): + comments = CommentField() + + class Meta: + model = ASPathListRule + fields = ["aspath_list", "index", "action", "pattern", "description", "tags", "comments"] + + class CommunityForm(NetBoxModelForm): status = forms.ChoiceField( required=False, @@ -647,6 +702,11 @@ class RoutingPolicyRuleForm(NetBoxModelForm): } ) + match_aspath_list = DynamicModelMultipleChoiceField( + queryset=ASPathList.objects.all(), + required=False, + ) + match_custom = forms.JSONField( label="Custom Match", help_text='Any custom match statements, e.g., {"ip nexthop": "1.1.1.1"}', @@ -674,6 +734,7 @@ class Meta: "match_community_list", "match_ip_address", "match_ipv6_address", + "match_aspath_list", "match_custom", "set_actions", "description", diff --git a/netbox_bgp/graphql/enums.py b/netbox_bgp/graphql/enums.py index 1e6fa2f..3f837ff 100644 --- a/netbox_bgp/graphql/enums.py +++ b/netbox_bgp/graphql/enums.py @@ -14,8 +14,8 @@ "NetBoxBGPIPAddressFamilyEnum", ) -NetBoxBGPCommunityStatusEnum = strawberry.enum(CommunityStatusChoices.as_enum()) -NetBoxBGPSessionStatusEnum = strawberry.enum(SessionStatusChoices.as_enum()) -NetBoxBGPActionEnum = strawberry.enum(ActionChoices.as_enum()) -NetBoxBGPIPAddressFamilyEnum = strawberry.enum(IPAddressFamilyChoices.as_enum()) +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") diff --git a/netbox_bgp/graphql/filters.py b/netbox_bgp/graphql/filters.py index 7334617..430a86c 100644 --- a/netbox_bgp/graphql/filters.py +++ b/netbox_bgp/graphql/filters.py @@ -7,7 +7,7 @@ from netbox.graphql.filter_mixins import NetBoxModelFilterMixin from tenancy.graphql.filter_mixins import TenancyFilterMixin from ipam.graphql.filters import IPAddressFilter, ASNFilter -from dcim.graphql.filter import DeviceFilter +from dcim.graphql.filters import DeviceFilter from netbox_bgp.models import ( Community, @@ -19,6 +19,8 @@ PrefixListRule, CommunityList, CommunityListRule, + ASPathList, + ASPathListRule ) from netbox_bgp.filtersets import ( @@ -31,6 +33,8 @@ PrefixListRuleFilterSet, CommunityListFilterSet, CommunityListRuleFilterSet, + ASPathListFilterSet, + ASPathListRuleFilterSet ) from netbox_bgp.graphql.enums import ( @@ -51,8 +55,31 @@ "NetBoxBGPPrefixListRuleFilter", "NetBoxBGPCommunityListFilter", "NetBoxBGPCommunityListRuleFilter", + "NetBoxBGPASPathListFilter", + "NetBoxBGPASPathListRuleFilter" ) +@strawberry_django.filter_type(ASPathList, lookups=True) +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() + aspath_list: ( + Annotated[ + "NetBoxBGPASPathListFilter", strawberry.lazy("netbox_bgp.graphql.filters") + ] + | None + ) = strawberry_django.filter_field() + aspath_list_id: ID | None = strawberry_django.filter_field() + action: ( + Annotated[ + "NetBoxBGPActionEnum", strawberry.lazy("netbox_bgp.graphql.enums") + ] + | None + ) = strawberry_django.filter_field() @strawberry_django.filter_type(Community, lookups=True) class NetBoxBGPCommunityFilter(TenancyFilterMixin, NetBoxModelFilterMixin): @@ -152,6 +179,13 @@ class NetBoxBGPRoutingPolicyRuleFilter(NetBoxModelFilterMixin): ] | None ) = strawberry_django.filter_field() + aspath_list: ( + Annotated[ + "NetBoxBGPASPathListFilter", strawberry.lazy("netbox_bgp.graphql.filters") + ] + | None + ) = strawberry_django.filter_field() + aspath_list_id: ID | None = strawberry_django.filter_field() @strawberry_django.filter_type(PrefixList, lookups=True) diff --git a/netbox_bgp/graphql/schema.py b/netbox_bgp/graphql/schema.py index 459bb9b..2294093 100644 --- a/netbox_bgp/graphql/schema.py +++ b/netbox_bgp/graphql/schema.py @@ -13,6 +13,8 @@ PrefixListRule, CommunityList, CommunityListRule, + ASPathList, + ASPathListRule ) from .types import ( CommunityType, @@ -24,6 +26,8 @@ PrefixListRuleType, CommunityListType, CommunityListRuleType, + ASPathListType, + ASPathListRuleType ) @@ -56,3 +60,9 @@ class NetBoxBGPQuery: netbox_bgp_communitylist_rule: CommunityListRuleType = strawberry_django.field() netbox_bgp_communitylist_rule_list: List[CommunityListRuleType] = strawberry_django.field() + + netbox_bgp_aspathlist: ASPathListType = strawberry_django.field() + 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 diff --git a/netbox_bgp/graphql/types.py b/netbox_bgp/graphql/types.py index 1a40861..c11d06c 100644 --- a/netbox_bgp/graphql/types.py +++ b/netbox_bgp/graphql/types.py @@ -14,6 +14,8 @@ PrefixListRule, CommunityList, CommunityListRule, + ASPathList, + ASPathListRule ) from .filters import ( NetBoxBGPCommunityFilter, @@ -25,8 +27,29 @@ NetBoxBGPPrefixListRuleFilter, NetBoxBGPCommunityListFilter, NetBoxBGPCommunityListRuleFilter, + NetBoxBGPASPathListFilter, + NetBoxBGPASPathListRuleFilter ) +@strawberry_django.type(ASPathList, fields="__all__", filters=NetBoxBGPASPathListFilter) +class ASPathListType(NetBoxObjectType): + name: str + description: str + rules: List[ + Annotated["ASPathListRuleType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + + +@strawberry_django.type(ASPathListRule, fields="__all__", filters=NetBoxBGPASPathListRuleFilter) +class ASPathListRuleType(NetBoxObjectType): + aspath_list: Annotated[ + "ASPathListType", strawberry.lazy("netbox_bgp.graphql.types") + ] + index: BigInt + action: str + pattern: str + description: str + @strawberry_django.type(Community, fields="__all__", filters=NetBoxBGPCommunityFilter) class CommunityType(NetBoxObjectType): diff --git a/netbox_bgp/migrations/0034_aspathlist_routingpolicyrule_match_aspath_list_and_more.py b/netbox_bgp/migrations/0034_aspathlist_routingpolicyrule_match_aspath_list_and_more.py new file mode 100644 index 0000000..438d492 --- /dev/null +++ b/netbox_bgp/migrations/0034_aspathlist_routingpolicyrule_match_aspath_list_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 5.2.2 on 2025-06-21 20:43 + +import django.db.models.deletion +import taggit.managers +import utilities.json +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('netbox_bgp', '0033_alter_bgpsession_unique_together_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ASPathList', + 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=100)), + ('description', models.CharField(blank=True, max_length=200)), + ('comments', models.TextField(blank=True)), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'verbose_name_plural': 'AS Path Lists', + 'unique_together': {('name', 'description')}, + }, + ), + migrations.AddField( + model_name='routingpolicyrule', + name='match_aspath_list', + field=models.ManyToManyField(blank=True, related_name='aspathrules', to='netbox_bgp.aspathlist'), + ), + migrations.CreateModel( + name='ASPathListRule', + 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)), + ('index', models.PositiveIntegerField()), + ('action', models.CharField(max_length=30)), + ('pattern', models.CharField(max_length=200)), + ('description', models.CharField(blank=True, max_length=200)), + ('comments', models.TextField(blank=True)), + ('aspath_list', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='aspathlistrules', to='netbox_bgp.aspathlist')), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/netbox_bgp/models.py b/netbox_bgp/models.py index 39c135a..d9fe459 100644 --- a/netbox_bgp/models.py +++ b/netbox_bgp/models.py @@ -9,6 +9,68 @@ from .choices import IPAddressFamilyChoices, SessionStatusChoices, ActionChoices, CommunityStatusChoices +class ASPathList(NetBoxModel): + """ + as-path access list, as-path filter + """ + name = models.CharField( + max_length=100 + ) + description = models.CharField( + max_length=200, + blank=True + ) + comments = models.TextField( + blank=True + ) + + class Meta: + verbose_name_plural = 'AS Path Lists' + unique_together = ['name', 'description'] + + def __str__(self): + return self.name + + def get_absolute_url(self): + return reverse('plugins:netbox_bgp:aspathlist', args=[self.pk]) + + +class ASPathListRule(NetBoxModel): + """ + Rules for AS Path List + """ + aspath_list = models.ForeignKey( + to=ASPathList, + on_delete=models.CASCADE, + related_name='aspathlistrules' + ) + index = models.PositiveIntegerField() + action = models.CharField( + max_length=30, + choices=ActionChoices + ) + pattern = models.CharField( + max_length=200, + ) + description = models.CharField( + max_length=200, + blank=True + ) + comments = models.TextField( + blank=True + ) + + def __str__(self): + return f'{self.aspath_list}: {self.action} {self.pattern}' + + def get_absolute_url(self): + return reverse('plugins:netbox_bgp:aspathlistrule', args=[self.pk]) + + def get_action_color(self): + return ActionChoices.colors.get(self.action) + + + class RoutingPolicy(NetBoxModel): """ """ @@ -445,6 +507,11 @@ class RoutingPolicyRule(NetBoxModel): blank=True, related_name='cmrules' ) + match_aspath_list = models.ManyToManyField( + to=ASPathList, + blank=True, + related_name='aspathrules' + ) match_ip_address = models.ManyToManyField( to=PrefixList, blank=True, @@ -504,12 +571,16 @@ def match_statements(self): result.update( {'ipv6 address': [str(prefix_list) for prefix_list in self.match_ipv6_address.all().values_list('name', flat=True)]} ) + result.update( + {'as-path': list(self.match_aspath_list.all().values_list('name', flat=True))} + ) custom_match = self.get_match_custom() # update community from custom result['community'].extend(custom_match.get('community', [])) result['ip address'].extend(custom_match.get('ip address', [])) result['ipv6 address'].extend(custom_match.get('ipv6 address', [])) + result['as-path'].extend(custom_match.get('as-path', [])) # remove empty matches result = {k: v for k, v in result.items() if v} result.update(custom_match) diff --git a/netbox_bgp/navigation.py b/netbox_bgp/navigation.py index d59b7d0..40f2f59 100644 --- a/netbox_bgp/navigation.py +++ b/netbox_bgp/navigation.py @@ -137,6 +137,44 @@ ), ), ), + PluginMenuItem( + link='plugins:netbox_bgp:aspathlist_list', + link_text='AS Path Lists', + permissions=['netbox_bgp.view_aspathlist'], + buttons=( + PluginMenuButton( + link='plugins:netbox_bgp:aspathlist_add', + title='Add', + icon_class='mdi mdi-plus-thick', + permissions=['netbox_bgp.add_aspathlist'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:aspathlist_bulk_import', + title='Import', + icon_class='mdi mdi-upload', + permissions=['netbox_bgp.add_aspathlist'], + ), + ), + ), + PluginMenuItem( + link='plugins:netbox_bgp:aspathlistrule_list', + link_text='AS Path List Rules', + permissions=['netbox_bgp.view_aspathlistrule'], + buttons=( + PluginMenuButton( + link='plugins:netbox_bgp:aspathlistrule_add', + title='Add', + icon_class='mdi mdi-plus-thick', + permissions=['netbox_bgp.add_aspathlistrule'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:aspathlistrule_bulk_import', + title='Import', + icon_class='mdi mdi-upload', + permissions=['netbox_bgp.add_aspathlistrule'], + ), + ), + ), PluginMenuItem( link='plugins:netbox_bgp:bgppeergroup_list', link_text='Peer Groups', @@ -239,6 +277,47 @@ ) ) +_aspath_list_menu = ( + PluginMenuItem( + link='plugins:netbox_bgp:aspathlist_list', + link_text='AS Path Lists', + permissions=['netbox_bgp.view_aspathlist'], + buttons=( + PluginMenuButton( + link='plugins:netbox_bgp:aspathlist_add', + title='Add', + icon_class='mdi mdi-plus-thick', + permissions=['netbox_bgp.add_aspathlist'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:aspathlist_bulk_import', + title='Import', + icon_class='mdi mdi-upload', + permissions=['netbox_bgp.add_aspathlist'], + ), + ), + ), + PluginMenuItem( + link='plugins:netbox_bgp:aspathlistrule_list', + link_text='AS Path List Rules', + permissions=['netbox_bgp.view_aspathlistrule'], + buttons=( + PluginMenuButton( + link='plugins:netbox_bgp:aspathlistrule_add', + title='Add', + icon_class='mdi mdi-plus-thick', + permissions=['netbox_bgp.add_aspathlistrule'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:aspathlistrule_bulk_import', + title='Import', + icon_class='mdi mdi-upload', + permissions=['netbox_bgp.add_aspathlistrule'], + ), + ), + ), +) + _routing_policy_menu = ( PluginMenuItem( link='plugins:netbox_bgp:routingpolicy_list', @@ -330,7 +409,8 @@ groups=( ("BGP", _menu_items_grouped), ("Prefix Lists", _prefix_list_menu), - ("Routing Policies", _routing_policy_menu) + ("Routing Policies", _routing_policy_menu), + ("AS Path Lists", _aspath_list_menu), ), icon_class="mdi mdi-bootstrap", ) diff --git a/netbox_bgp/tables.py b/netbox_bgp/tables.py index b5e6f93..54c0d03 100644 --- a/netbox_bgp/tables.py +++ b/netbox_bgp/tables.py @@ -8,7 +8,8 @@ from .models import ( Community, BGPSession, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule, PrefixList, - PrefixListRule, CommunityList, CommunityListRule + PrefixListRule, CommunityList, CommunityListRule, + ASPathList, ASPathListRule ) @@ -30,6 +31,31 @@ """ +class ASPathListTable(NetBoxTable): + name = tables.LinkColumn() + + class Meta(NetBoxTable.Meta): + model = ASPathList + fields = ('pk', 'name', 'description', 'actions') + + +class ASPathListRuleTable(NetBoxTable): + aspath_list = tables.Column( + linkify=True + ) + index = tables.Column( + linkify=True + ) + action = ChoiceFieldColumn() + + class Meta(NetBoxTable.Meta): + model = ASPathListRule + fields = ( + 'pk', 'aspath_list', + 'index', + 'action', 'pattern', + ) + class CommunityTable(NetBoxTable): value = tables.LinkColumn() status = ChoiceFieldColumn( diff --git a/netbox_bgp/templates/netbox_bgp/aspathlist.html b/netbox_bgp/templates/netbox_bgp/aspathlist.html new file mode 100644 index 0000000..04a3314 --- /dev/null +++ b/netbox_bgp/templates/netbox_bgp/aspathlist.html @@ -0,0 +1,71 @@ +{% extends 'generic/object.html' %} +{% load buttons %} +{% load custom_links %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} + +{% block breadcrumbs %} + +{% endblock %} +{% block extra_controls %} +
+ {% if perms.netbox_bgp.change_aspathlist %} + + Rule + + {% endif %} +
+{% endblock extra_controls %} + +{% block content %} +
+
+
+
+ AS Path List +
+
+ + + + + + + + + +
Name{{ object.name }}
Description{{ object.description|placeholder }}
+
+
+ {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} + {% plugin_left_page object %} +
+
+
+
+ Related Routing Policy Rules +
+
+ {% render_table rprules_table 'inc/table.html' %} +
+
+ {% plugin_right_page object %} +
+
+
+
+
+
+ Rules +
+
+ {% render_table rules_table 'inc/table.html' %} +
+ {% plugin_full_width_page object %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/netbox_bgp/templates/netbox_bgp/aspathlistrule.html b/netbox_bgp/templates/netbox_bgp/aspathlistrule.html new file mode 100644 index 0000000..51c25a7 --- /dev/null +++ b/netbox_bgp/templates/netbox_bgp/aspathlistrule.html @@ -0,0 +1,42 @@ +{% extends 'generic/object.html' %} +{% load helpers %} +{% load static %} + +{% block content %} +
+
+
+
AS Path List Rule
+
+ + + + + + + + + + + + + + + + + + + + + +
AS Path List + {{ object.aspath_list }} +
Index{{ object.index }}
Action{% badge object.get_action_display bg_color=object.get_action_color %}
Pattern{{ object.pattern }}
Description{{ object.description|placeholder }}
+
+
+ {% include 'inc/panels/custom_fields.html' %} + {% include 'inc/panels/tags.html' %} + {% include 'inc/panels/comments.html' %} +
+
+{% endblock content %} \ No newline at end of file diff --git a/netbox_bgp/urls.py b/netbox_bgp/urls.py index 6873df6..827d733 100644 --- a/netbox_bgp/urls.py +++ b/netbox_bgp/urls.py @@ -11,6 +11,24 @@ app_name = 'netbox_bgp' urlpatterns = ( + # AS Path Lists + path( + "aspath-list/", + include(get_model_urls("netbox_bgp", "aspathlist", detail=False)), + ), + path( + "aspath-list//", + include(get_model_urls("netbox_bgp", "aspathlist")), + ), + # AS Path List Rules + path( + "aspath-list-rule/", + include(get_model_urls("netbox_bgp", "aspathlistrule", detail=False)), + ), + path( + "aspath-list-rule//", + include(get_model_urls("netbox_bgp","aspathlistrule")), + ), # Community path( "community/", diff --git a/netbox_bgp/views.py b/netbox_bgp/views.py index c3270af..5a56c1e 100644 --- a/netbox_bgp/views.py +++ b/netbox_bgp/views.py @@ -10,7 +10,8 @@ from .models import ( Community, BGPSession, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule, PrefixList, - PrefixListRule, CommunityList, CommunityListRule + PrefixListRule, CommunityList, CommunityListRule, + ASPathList, ASPathListRule ) from . import filtersets, forms, tables @@ -496,3 +497,96 @@ class VMBGPSessionView(generic.ObjectChildrenView): def get_children(self, request, parent): return parent.bgpsession_set.all() + +# AS Path List + +@register_model_view(ASPathList, "list", path="", detail=False) +class ASPathListListView(generic.ObjectListView): + queryset = ASPathList.objects.all() + filterset = filtersets.ASPathListFilterSet + filterset_form = forms.ASPathListFilterForm + table = tables.ASPathListTable + +@register_model_view(ASPathList, "add", detail=False) +@register_model_view(ASPathList, "edit") +class ASPathListEditView(generic.ObjectEditView): + queryset = ASPathList.objects.all() + form = forms.ASPathListForm + +@register_model_view(ASPathList, "bulk_delete", path="delete", detail=False) +class ASPathListBulkDeleteView(generic.BulkDeleteView): + queryset = ASPathList.objects.all() + table = tables.ASPathListTable + +@register_model_view(ASPathList, "bulk_edit", path="edit", detail=False) +class ASPathListBulkEditView(generic.BulkEditView): + queryset = ASPathList.objects.all() + filterset = filtersets.ASPathListFilterSet + table = tables.ASPathListTable + form = forms.ASPathListBulkEditForm + +@register_model_view(ASPathList) +class ASPathListView(generic.ObjectView): + queryset = ASPathList.objects.all() + template_name = 'netbox_bgp/aspathlist.html' + + def get_extra_context(self, request, instance): + rprules = instance.aspathrules.all() + rprules_table = tables.RoutingPolicyRuleTable(rprules) + rules = instance.aspathlistrules.all() + rules_table = tables.ASPathListRuleTable(rules) + return { + 'rules_table': rules_table, + 'rprules_table': rprules_table + } + +@register_model_view(ASPathList, "delete") +class ASPathListDeleteView(generic.ObjectDeleteView): + queryset = ASPathList.objects.all() + default_return_url = 'plugins:netbox_bgp:aspathlist_list' + +@register_model_view(ASPathList, "bulk_import", path="import", detail=False) +class ASPathListBulkImportView(generic.BulkImportView): + queryset = ASPathList.objects.all() + model_form = forms.ASPathListImportForm + +# AS Path List Rule + +@register_model_view(ASPathListRule, "list", path="", detail=False) +class ASPathListRuleListView(generic.ObjectListView): + queryset = ASPathListRule.objects.all() + filterset = filtersets.ASPathListRuleFilterSet + # filterset_form = ASPathListRuleFilterForm + table = tables.ASPathListRuleTable + actions = {'add': {'add'}, 'bulk_delete': {'delete'}} + + +@register_model_view(ASPathListRule, "add", detail=False) +@register_model_view(ASPathListRule, "edit") +class ASPathListRuleEditView(generic.ObjectEditView): + queryset = ASPathListRule.objects.all() + form = forms.ASPathListRuleForm + + +@register_model_view(ASPathListRule, "bulk_delete", path="delete", detail=False) +class ASPathListRuleBulkDeleteView(generic.BulkDeleteView): + queryset = ASPathListRule.objects.all() + table = tables.ASPathListRuleTable + + +@register_model_view(ASPathListRule, "delete") +class ASPathListRuleDeleteView(generic.ObjectDeleteView): + queryset = ASPathListRule.objects.all() + default_return_url = 'plugins:netbox_bgp:aspathlistrule_list' + + +@register_model_view(ASPathListRule, "bulk_import", path="import", detail=False) +class ASPathListRuleBulkImportView(generic.BulkImportView): + queryset = ASPathListRule.objects.all() + model_form = forms.ASPathListRuleImportForm + + +@register_model_view(ASPathListRule) +class ASPathListRuleView(generic.ObjectView): + queryset = ASPathListRule.objects.all() + template_name = 'netbox_bgp/aspathlistrule.html'