diff --git a/Makefile b/Makefile index b8cbc36..48aecd9 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ -PYTHON_VER?=3.8 -NETBOX_VER?=v3.7.0 +PYTHON_VER?=3.10 +NETBOX_VER?=v4.0.2 NAME=netbox-bgp diff --git a/README.md b/README.md index 917a038..c4c58f3 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This plugin provide following Models: | NetBox 3.5 | >= 0.10.0 | | NetBox 3.6 | >= 0.11.0 | | NetBox 3.7 | >= 0.12.0 | +| NetBox 4.0 | >= 0.13.0 | ## Installation diff --git a/netbox_bgp/__init__.py b/netbox_bgp/__init__.py index e30191d..d1c7bdf 100644 --- a/netbox_bgp/__init__.py +++ b/netbox_bgp/__init__.py @@ -1,4 +1,4 @@ -from extras.plugins import PluginConfig +from netbox.plugins import PluginConfig from .version import __version__ @@ -11,8 +11,8 @@ class BGPConfig(PluginConfig): author_email = 'mgk.kolek@gmail.com' base_url = 'bgp' required_settings = [] - min_version = '3.5.0' - max_version = '3.7.99' + min_version = '4.0.0' + max_version = '4.0.99' default_settings = { 'device_ext_page': 'right', 'top_level_menu' : False, diff --git a/netbox_bgp/api/serializers.py b/netbox_bgp/api/serializers.py index cc846bb..4402b7e 100644 --- a/netbox_bgp/api/serializers.py +++ b/netbox_bgp/api/serializers.py @@ -1,246 +1,326 @@ from rest_framework.serializers import HyperlinkedIdentityField, ValidationError from rest_framework.relations import PrimaryKeyRelatedField -from netbox.api.fields import ChoiceField -from netbox.api.serializers.nested import WritableNestedSerializer +from netbox.api.fields import ChoiceField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer -from dcim.api.nested_serializers import NestedSiteSerializer, NestedDeviceSerializer -from tenancy.api.nested_serializers import NestedTenantSerializer -from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedASNSerializer, NestedPrefixSerializer -from ipam.api.field_serializers import IPNetworkField +from ipam.api.serializers import IPAddressSerializer, ASNSerializer, PrefixSerializer +from tenancy.api.serializers import TenantSerializer +from dcim.api.serializers import SiteSerializer, DeviceSerializer +from ipam.api.field_serializers import IPNetworkField from netbox_bgp.models import ( - BGPSession, RoutingPolicy, BGPPeerGroup, - Community, RoutingPolicyRule, PrefixList, - PrefixListRule, CommunityList, CommunityListRule + BGPSession, + RoutingPolicy, + BGPPeerGroup, + Community, + RoutingPolicyRule, + PrefixList, + PrefixListRule, + CommunityList, + CommunityListRule, ) from netbox_bgp.choices import CommunityStatusChoices, SessionStatusChoices -class SerializedPKRelatedField(PrimaryKeyRelatedField): - def __init__(self, serializer, **kwargs): - self.serializer = serializer - self.pk_field = kwargs.pop('pk_field', None) - super().__init__(**kwargs) - - def to_representation(self, value): - return self.serializer(value, context={'request': self.context['request']}).data - - class RoutingPolicySerializer(NetBoxModelSerializer): - class Meta: - model = RoutingPolicy - fields = ['id', 'name', 'description', 'tags', 'custom_fields', 'comments'] - - -class NestedRoutingPolicySerializer(WritableNestedSerializer): - url = HyperlinkedIdentityField(view_name='plugins:netbox_bgp:routingpolicy') + url = HyperlinkedIdentityField(view_name="plugins:netbox_bgp:routingpolicy") class Meta: model = RoutingPolicy - fields = ['id', 'url', 'name', 'display', 'description'] - validators = [] - - -class NestedPrefixListSerializer(WritableNestedSerializer): - url = HyperlinkedIdentityField(view_name='plugins:netbox_bgp:prefixlist') - - class Meta: - model = PrefixList - fields = ['id', 'url', 'display', 'name'] + fields = [ + "id", + "url", + "display", + "name", + "description", + "tags", + "custom_fields", + "comments", + ] + brief_fields = ("id", "url", "display", "name", "description") class PrefixListSerializer(NetBoxModelSerializer): + url = HyperlinkedIdentityField(view_name="plugins:netbox_bgp:prefixlist") + class Meta: model = PrefixList - fields = ['id', 'name', 'display','description', 'family', 'tags', 'custom_fields', 'comments'] + fields = [ + "id", + "url", + "name", + "display", + "description", + "family", + "tags", + "custom_fields", + "comments", + ] + brief_fields = ("id", "url", "display", "name", "description") class BGPPeerGroupSerializer(NetBoxModelSerializer): + url = HyperlinkedIdentityField(view_name="plugins:netbox_bgp:bgppeergroup") + import_policies = SerializedPKRelatedField( queryset=RoutingPolicy.objects.all(), - serializer=NestedRoutingPolicySerializer, + serializer=RoutingPolicySerializer, + nested=True, required=False, allow_null=True, - many=True + many=True, ) export_policies = SerializedPKRelatedField( queryset=RoutingPolicy.objects.all(), - serializer=NestedRoutingPolicySerializer, + serializer=RoutingPolicySerializer, + nested=True, required=False, allow_null=True, - many=True + many=True, ) class Meta: model = BGPPeerGroup - fields = ['id', 'display', 'name', 'description', 'import_policies', 'export_policies', 'comments'] - - -class NestedBGPPeerGroupSerializer(WritableNestedSerializer): - url = HyperlinkedIdentityField(view_name='plugins:netbox_bgp:bgppeergroup') - - class Meta: - model = BGPPeerGroup - fields = ['id', 'display', 'url', 'name', 'description'] - validators = [] + fields = [ + "id", + "url", + "display", + "name", + "description", + "import_policies", + "export_policies", + "comments", + ] + brief_fields = ("id", "url", "display", "name", "description") class BGPSessionSerializer(NetBoxModelSerializer): + url = HyperlinkedIdentityField(view_name="plugins:netbox_bgp:bgpsession") status = ChoiceField(choices=SessionStatusChoices, required=False) - site = NestedSiteSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - device = NestedDeviceSerializer(required=False, allow_null=True) - local_address = NestedIPAddressSerializer(required=True, allow_null=False) - remote_address = NestedIPAddressSerializer(required=True, allow_null=False) - local_as = NestedASNSerializer(required=True, allow_null=False) - remote_as = NestedASNSerializer(required=True, allow_null=False) - peer_group = NestedBGPPeerGroupSerializer(required=False, allow_null=True) - prefix_list_in = NestedPrefixListSerializer(required=False, allow_null=True) - prefix_list_out = NestedPrefixListSerializer(required=False, allow_null=True) + site = SiteSerializer(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) + local_address = IPAddressSerializer(nested=True, required=True, allow_null=False) + remote_address = IPAddressSerializer(nested=True, required=True, allow_null=False) + local_as = ASNSerializer(nested=True, required=True, allow_null=False) + remote_as = ASNSerializer(nested=True, required=True, allow_null=False) + peer_group = BGPPeerGroupSerializer(nested=True, required=False, allow_null=True) + prefix_list_in = PrefixListSerializer(nested=True, required=False, allow_null=True) + prefix_list_out = PrefixListSerializer(nested=True, required=False, allow_null=True) import_policies = SerializedPKRelatedField( queryset=RoutingPolicy.objects.all(), - serializer=NestedRoutingPolicySerializer, + serializer=RoutingPolicySerializer, + nested=True, required=False, allow_null=True, - many=True + many=True, ) export_policies = SerializedPKRelatedField( queryset=RoutingPolicy.objects.all(), - serializer=NestedRoutingPolicySerializer, + serializer=RoutingPolicySerializer, + nested=True, required=False, allow_null=True, - many=True + many=True, ) class Meta: model = BGPSession fields = [ - 'id', 'tags', 'custom_fields', - 'display', 'status', 'site', 'tenant', - 'device', 'local_address', 'remote_address', - 'local_as', 'remote_as', 'peer_group', 'import_policies', - 'export_policies', 'prefix_list_in','prefix_list_out', - 'created', 'last_updated', - 'name', 'description', 'comments' - ] - + "id", + "url", + "tags", + "custom_fields", + "display", + "status", + "site", + "tenant", + "device", + "local_address", + "remote_address", + "local_as", + "remote_as", + "peer_group", + "import_policies", + "export_policies", + "prefix_list_in", + "prefix_list_out", + "created", + "last_updated", + "name", + "description", + "comments", + ] + brief_fields = ("id", "url", "display", "name", "description") def to_representation(self, instance): ret = super().to_representation(instance) if instance is not None: if instance.peer_group: - for pol in instance.peer_group.import_policies.difference(instance.import_policies.all()): - ret['import_policies'].append( - NestedRoutingPolicySerializer(pol, context={'request': self.context['request']}).data + for pol in instance.peer_group.import_policies.difference( + instance.import_policies.all() + ): + ret["import_policies"].append( + RoutingPolicySerializer( + pol, + context={"request": self.context["request"]}, + nested=True, + ).data ) - for pol in instance.peer_group.export_policies.difference(instance.export_policies.all()): - ret['export_policies'].append( - NestedRoutingPolicySerializer(pol, context={'request': self.context['request']}).data + for pol in instance.peer_group.export_policies.difference( + instance.export_policies.all() + ): + ret["export_policies"].append( + RoutingPolicySerializer( + pol, + context={"request": self.context["request"]}, + nested=True, + ).data ) return ret -class NestedBGPSessionSerializer(WritableNestedSerializer): - url = HyperlinkedIdentityField(view_name='plugins:netbox_bgp:bgpsession') - - class Meta: - model = BGPSession - fields = ['id', 'url', 'name', 'display','description'] - validators = [] - - class CommunitySerializer(NetBoxModelSerializer): status = ChoiceField(choices=CommunityStatusChoices, required=False) - tenant = NestedTenantSerializer(required=False, allow_null=True) - - class Meta: - model = Community - fields = [ - 'id', 'tags', 'custom_fields', 'display', - 'status', 'tenant', 'created', 'last_updated', - 'description', - 'value', 'site', 'role', 'comments' - ] - -class NestedCommunitySerializer(WritableNestedSerializer): - url = HyperlinkedIdentityField(view_name='plugins:netbox_bgp:community') + tenant = TenantSerializer(nested=True, required=False, allow_null=True) + url = HyperlinkedIdentityField(view_name="plugins:netbox_bgp:community") class Meta: model = Community fields = [ - 'id', 'url', 'display', 'value' + "id", + "url", + "tags", + "custom_fields", + "display", + "status", + "tenant", + "created", + "last_updated", + "description", + "value", + "site", + "role", + "comments", ] + brief_fields = ("id", "url", "display", "value", "description") class CommunityListSerializer(NetBoxModelSerializer): - class Meta: - model = CommunityList - fields = ['id', 'name', 'display','description', 'tags', 'custom_fields', 'comments'] - - -class NestedCommunityListSerializer(WritableNestedSerializer): - url = HyperlinkedIdentityField(view_name='plugins:netbox_bgp:communitylist') + url = HyperlinkedIdentityField(view_name="plugins:netbox_bgp:communitylist") class Meta: model = CommunityList - fields = ['id', 'url', 'display', 'name'] + fields = [ + "id", + "url", + "name", + "display", + "description", + "tags", + "custom_fields", + "comments", + ] + brief_fields = ("id", "url", "display", "name", "description") class CommunityListRuleSerializer(NetBoxModelSerializer): - community_list = NestedCommunityListSerializer() - community = NestedCommunitySerializer(required=False, allow_null=True) + community_list = CommunityListSerializer(nested=True) + community = CommunitySerializer(nested=True, required=False, allow_null=True) class Meta: model = CommunityListRule fields = [ - 'id', 'tags', 'custom_fields', 'display', - 'community_list', 'created', 'last_updated', - 'action', 'community', 'comments' + "id", + "tags", + "custom_fields", + "display", + "description", + "community_list", + "created", + "last_updated", + "action", + "community", + "comments", ] + brief_fields = ("id", "display", "description") class RoutingPolicyRuleSerializer(NetBoxModelSerializer): match_ip_address = SerializedPKRelatedField( queryset=PrefixList.objects.all(), - serializer=NestedPrefixListSerializer, + serializer=PrefixListSerializer, + nested=True, required=False, allow_null=True, - many=True + many=True, ) - routing_policy = NestedRoutingPolicySerializer() + routing_policy = RoutingPolicySerializer(nested=True) match_community = SerializedPKRelatedField( queryset=Community.objects.all(), - serializer=NestedCommunitySerializer, + serializer=CommunitySerializer, + nested=True, + required=False, + allow_null=True, + many=True, + ) + match_community_list = SerializedPKRelatedField( + queryset=CommunityList.objects.all(), + serializer=CommunityListSerializer, + nested=True, required=False, allow_null=True, - many=True + many=True, ) - class Meta: model = RoutingPolicyRule fields = [ - 'id', 'index', 'display' ,'action', 'match_ip_address', - 'routing_policy', 'match_community', 'match_custom', 'set_actions', - 'match_ipv6_address', 'description', 'tags', 'custom_fields', 'comments' + "id", + "index", + "display", + "action", + "match_ip_address", + "routing_policy", + "match_community", + "match_community_list", + "match_custom", + "set_actions", + "match_ipv6_address", + "description", + "tags", + "custom_fields", + "comments", ] + brief_fields = ("id", "display", "description") class PrefixListRuleSerializer(NetBoxModelSerializer): - prefix_list = NestedPrefixListSerializer() - prefix = NestedPrefixSerializer(required=False, allow_null=True) + prefix_list = PrefixListSerializer(nested=True) + prefix = PrefixSerializer(nested=True, required=False, allow_null=True) prefix_custom = IPNetworkField(required=False, allow_null=True) class Meta: model = PrefixListRule fields = [ - 'id', 'tags', 'custom_fields', 'display', - 'prefix_list', 'created', 'last_updated', - 'index', 'action', - 'prefix_custom', 'ge', 'le', 'prefix', 'comments' + "id", + "description", + "tags", + "custom_fields", + "display", + "prefix_list", + "created", + "last_updated", + "index", + "action", + "prefix_custom", + "ge", + "le", + "prefix", + "comments", ] + brief_fields = ("id", "display", "description") diff --git a/netbox_bgp/api/urls.py b/netbox_bgp/api/urls.py index 5383a9b..734d275 100644 --- a/netbox_bgp/api/urls.py +++ b/netbox_bgp/api/urls.py @@ -1,4 +1,4 @@ -from rest_framework import routers +from netbox.api.routers import NetBoxRouter from .views import ( BGPSessionViewSet, RoutingPolicyViewSet, BGPPeerGroupViewSet, CommunityViewSet, @@ -6,7 +6,7 @@ CommunityListViewSet, CommunityListRuleViewSet ) -router = routers.DefaultRouter() +router = NetBoxRouter() router.register('session', BGPSessionViewSet, 'session') router.register('bgpsession', BGPSessionViewSet, 'bgpsession') router.register('routing-policy', RoutingPolicyViewSet) diff --git a/netbox_bgp/choices.py b/netbox_bgp/choices.py index 8fbc60d..d804e52 100644 --- a/netbox_bgp/choices.py +++ b/netbox_bgp/choices.py @@ -78,7 +78,7 @@ class IPAddressFamilyChoices(ChoiceSet): FAMILY_4 = 'ipv4' FAMILY_6 = 'ipv6' - + CHOICES = ( (FAMILY_4, 'IPv4'), (FAMILY_6, 'IPv6'), diff --git a/netbox_bgp/filtersets.py b/netbox_bgp/filtersets.py index 4f9f455..b12b6ff 100644 --- a/netbox_bgp/filtersets.py +++ b/netbox_bgp/filtersets.py @@ -2,7 +2,6 @@ import netaddr from django.db.models import Q from netaddr.core import AddrFormatError -from extras.filters import TagFilter from netbox.filtersets import NetBoxModelFilterSet from .models import ( @@ -251,7 +250,7 @@ class PrefixListFilterSet(NetBoxModelFilterSet): class Meta: model = PrefixList - fields = ['id', 'name', 'description'] + fields = ['id', 'name', 'description', 'family'] def search(self, queryset, name, value): """Perform the filtered search.""" diff --git a/netbox_bgp/forms.py b/netbox_bgp/forms.py index c8a8b90..10801fa 100644 --- a/netbox_bgp/forms.py +++ b/netbox_bgp/forms.py @@ -1,6 +1,10 @@ from django import forms -from django.conf import settings -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist, ValidationError +from utilities.forms.rendering import FieldSet +from django.core.exceptions import ( + MultipleObjectsReturned, + ObjectDoesNotExist, + ValidationError, +) from django.utils.translation import gettext as _ from tenancy.models import Tenant @@ -8,20 +12,39 @@ from ipam.models import IPAddress, Prefix, ASN from ipam.formfields import IPNetworkFormField from utilities.forms.fields import ( - DynamicModelChoiceField, CSVModelChoiceField, + DynamicModelChoiceField, + CSVModelChoiceField, + CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField, - TagFilterField, CSVChoiceField, CommentField + TagFilterField, + CSVChoiceField, + CommentField, ) from utilities.forms.widgets import APISelect, APISelectMultiple -from netbox.forms import NetBoxModelForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm, NetBoxModelImportForm +from netbox.forms import ( + NetBoxModelForm, + NetBoxModelBulkEditForm, + NetBoxModelFilterSetForm, + NetBoxModelImportForm, +) from .models import ( - Community, BGPSession, RoutingPolicy, BGPPeerGroup, - RoutingPolicyRule, PrefixList, PrefixListRule, - CommunityList, CommunityListRule + Community, + BGPSession, + RoutingPolicy, + BGPPeerGroup, + RoutingPolicyRule, + PrefixList, + PrefixListRule, + CommunityList, + CommunityListRule, ) -from .choices import SessionStatusChoices, CommunityStatusChoices +from .choices import ( + SessionStatusChoices, + CommunityStatusChoices, + IPAddressFamilyChoices, +) class CommunityForm(NetBoxModelForm): @@ -29,36 +52,22 @@ class CommunityForm(NetBoxModelForm): required=False, choices=CommunityStatusChoices, ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) + tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False) comments = CommentField() class Meta: model = Community - fields = [ - 'value', 'description', 'status', 'tenant', 'tags', 'comments' - ] + fields = ["value", "description", "status", "tenant", "tags", "comments"] class CommunityFilterForm(NetBoxModelFilterSetForm): - q = forms.CharField( - required=False, - label='Search' - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) + q = forms.CharField(required=False, label="Search") + tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False) status = forms.MultipleChoiceField( choices=CommunityStatusChoices, required=False, ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False - ) + site = DynamicModelChoiceField(queryset=Site.objects.all(), required=False) tag = TagFilterField(Community) @@ -66,18 +75,8 @@ class CommunityFilterForm(NetBoxModelFilterSetForm): class CommunityBulkEditForm(NetBoxModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=Community.objects.all(), - widget=forms.MultipleHiddenInput - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) + tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False) + description = forms.CharField(max_length=200, required=False) status = forms.ChoiceField( required=False, choices=CommunityStatusChoices, @@ -85,7 +84,8 @@ class CommunityBulkEditForm(NetBoxModelBulkEditForm): model = Community nullable_fields = [ - 'tenant', 'description', + "tenant", + "description", ] @@ -93,26 +93,22 @@ class CommunityImportForm(NetBoxModelImportForm): tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, - to_field_name='name', - help_text=_('Assigned tenant') + to_field_name="name", + help_text=_("Assigned tenant"), ) status = CSVChoiceField( - choices=CommunityStatusChoices, - help_text=_('Operational status') - ) + choices=CommunityStatusChoices, help_text=_("Operational status") + ) class Meta: model = Community - fields = ('value', 'description', 'tags') + fields = ("value", "description", "tags") class CommunityListFilterForm(NetBoxModelFilterSetForm): model = CommunityList - q = forms.CharField( - required=False, - label='Search' - ) + q = forms.CharField(required=False, label="Search") tag = TagFilterField(model) @@ -123,63 +119,56 @@ class CommunityListForm(NetBoxModelForm): class Meta: model = CommunityList - fields = ['name', 'description', 'tags', 'comments'] + fields = ["name", "description", "tags", "comments"] + + +class CommunityListBulkEditForm(NetBoxModelBulkEditForm): + description = forms.CharField(max_length=200, required=False) + + model = CommunityList + nullable_fields = [ + "description", + ] + + +class CommunityListImportForm(NetBoxModelImportForm): + + class Meta: + model = CommunityList + fields = ("name", "description", "tags") class CommunityListRuleForm(NetBoxModelForm): community = DynamicModelChoiceField( queryset=Community.objects.all(), required=False, - help_text='Community', + help_text="Community", ) comments = CommentField() class Meta: model = CommunityListRule - fields = [ - 'community_list', - 'action', 'community', - 'tags', 'comments' - ] + fields = ["community_list", "action", "community", "tags", "comments"] class BGPSessionForm(NetBoxModelForm): - name = forms.CharField( - max_length=64, - required=True - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False - ) + name = forms.CharField(max_length=64, required=True) + site = DynamicModelChoiceField(queryset=Site.objects.all(), required=False) device = DynamicModelChoiceField( - queryset=Device.objects.all(), - required=False, - query_params={ - 'site_id': '$site' - } - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False + queryset=Device.objects.all(), required=False, query_params={"site_id": "$site"} ) + tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False) local_as = DynamicModelChoiceField( queryset=ASN.objects.all(), - query_params={ - 'site_id': '$site' - }, - label=_('Local AS') + query_params={"site_id": "$site"}, + label=_("Local AS"), ) remote_as = DynamicModelChoiceField( - queryset=ASN.objects.all(), - label=_('Remote AS') + queryset=ASN.objects.all(), label=_("Remote AS") ) local_address = DynamicModelChoiceField( - queryset=IPAddress.objects.all(), - query_params={ - 'device_id': '$device' - } + queryset=IPAddress.objects.all(), query_params={"device_id": "$device"} ) remote_address = DynamicModelChoiceField( queryset=IPAddress.objects.all(), @@ -188,55 +177,77 @@ class BGPSessionForm(NetBoxModelForm): queryset=BGPPeerGroup.objects.all(), required=False, widget=APISelect( - api_url='/api/plugins/bgp/peer-group/', - ) + api_url="/api/plugins/bgp/peer-group/", + ), ) import_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/routing-policy/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), ) export_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/routing-policy/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), ) prefix_list_in = DynamicModelChoiceField( queryset=PrefixList.objects.all(), required=False, widget=APISelect( - api_url='/api/plugins/bgp/prefix-list/', - ) + api_url="/api/plugins/bgp/prefix-list/", + ), ) prefix_list_out = DynamicModelChoiceField( queryset=PrefixList.objects.all(), required=False, widget=APISelect( - api_url='/api/plugins/bgp/prefix-list/', - ) - ) + api_url="/api/plugins/bgp/prefix-list/", + ), + ) comments = CommentField() + fieldsets = ( + FieldSet( + "name", + "description", + "site", + "device", + "status", + "peer_group", + "tenant", + "tags", + name="Session", + ), + FieldSet("remote_as", "remote_address", name="Remote"), + FieldSet("local_as", "local_address", name="Local"), + FieldSet("import_policies", "export_policies", name="Policies"), + FieldSet("prefix_list_in", "prefix_list_out", name="Prefixes"), + ) + class Meta: model = BGPSession fields = [ - 'name', 'site', 'device', - 'local_as', 'remote_as', 'local_address', 'remote_address', - 'description', 'status', 'peer_group', 'tenant', 'tags', 'import_policies', 'export_policies', - 'prefix_list_in', 'prefix_list_out','comments' + "name", + "site", + "device", + "local_as", + "remote_as", + "local_address", + "remote_address", + "description", + "status", + "peer_group", + "tenant", + "tags", + "import_policies", + "export_policies", + "prefix_list_in", + "prefix_list_out", + "comments", ] - fieldsets = ( - ('Session', ('name', 'site', 'device', 'description', 'status', 'peer_group', 'tenant', 'tags')), - ('Remote', ('remote_as', 'remote_address')), - ('Local', ('local_as', 'local_address')), - ('Policies', ('import_policies', 'export_policies', 'prefix_list_in', 'prefix_list_out')) - ) + widgets = { - 'status': forms.Select(), + "status": forms.Select(), } @@ -245,120 +256,130 @@ class BGPSessionAddForm(BGPSessionForm): def clean_remote_address(self): try: - ip = IPAddress.objects.get(address=str(self.cleaned_data['remote_address'])) + ip = IPAddress.objects.get(address=str(self.cleaned_data["remote_address"])) except MultipleObjectsReturned: - ip = IPAddress.objects.filter(address=str(self.cleaned_data['remote_address'])).first() + ip = IPAddress.objects.filter( + address=str(self.cleaned_data["remote_address"]) + ).first() except ObjectDoesNotExist: - ip = IPAddress.objects.create(address=str(self.cleaned_data['remote_address'])) - self.cleaned_data['remote_address'] = ip - return self.cleaned_data['remote_address'] + ip = IPAddress.objects.create( + address=str(self.cleaned_data["remote_address"]) + ) + self.cleaned_data["remote_address"] = ip + return self.cleaned_data["remote_address"] class BGPSessionImportForm(NetBoxModelImportForm): site = CSVModelChoiceField( - label=_('Site'), + label=_("Site"), required=False, queryset=Site.objects.all(), - to_field_name='name', - help_text=_('Assigned site') + to_field_name="name", + help_text=_("Assigned site"), ) tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, - to_field_name='name', - help_text=_('Assigned tenant') + to_field_name="name", + help_text=_("Assigned tenant"), ) device = CSVModelChoiceField( queryset=Device.objects.all(), - to_field_name='name', - help_text=_('Assigned device') + to_field_name="name", + help_text=_("Assigned device"), ) status = CSVChoiceField( - choices=SessionStatusChoices, - required=False, - help_text=_('Operational status') - ) + choices=SessionStatusChoices, required=False, help_text=_("Operational status") + ) local_address = CSVModelChoiceField( queryset=IPAddress.objects.all(), - to_field_name='address', - help_text=_('Local IP Address'), + to_field_name="address", + help_text=_("Local IP Address"), ) remote_address = CSVModelChoiceField( queryset=IPAddress.objects.all(), - to_field_name='address', - help_text=_('Remote IP Address'), + to_field_name="address", + help_text=_("Remote IP Address"), ) local_as = CSVModelChoiceField( queryset=ASN.objects.all(), - to_field_name='asn', - help_text=_('Local ASN'), + to_field_name="asn", + help_text=_("Local ASN"), ) remote_as = CSVModelChoiceField( queryset=ASN.objects.all(), - to_field_name='asn', - help_text=_('Remote ASN'), + to_field_name="asn", + help_text=_("Remote ASN"), ) peer_group = CSVModelChoiceField( queryset=BGPPeerGroup.objects.all(), required=False, - to_field_name='name', - help_text=_('Peer Group'), + to_field_name="name", + help_text=_("Peer Group"), + ) + import_policies = CSVModelMultipleChoiceField( + queryset=RoutingPolicy.objects.all(), + to_field_name="name", + required=False, + help_text=_("Import policies name"), + ) + export_policies = CSVModelMultipleChoiceField( + queryset=RoutingPolicy.objects.all(), + to_field_name="name", + required=False, + help_text=_("Export policies name"), ) prefix_list_in = CSVModelChoiceField( queryset=PrefixList.objects.all(), required=False, - to_field_name='name', - help_text=_('Prefix list In'), - ) + to_field_name="name", + help_text=_("Prefix list In"), + ) prefix_list_out = CSVModelChoiceField( queryset=PrefixList.objects.all(), required=False, - to_field_name='name', - help_text=_('Prefix List Out'), + to_field_name="name", + help_text=_("Prefix List Out"), ) class Meta: model = BGPSession fields = [ - 'name', 'device', 'site', 'description', 'tenant', 'status', 'peer_group', - 'local_address', 'remote_address', 'local_as', 'remote_as', 'tags', - 'prefix_list_in', 'prefix_list_out' + "name", + "device", + "site", + "description", + "tenant", + "status", + "peer_group", + "import_policies", + "export_policies", + "local_address", + "remote_address", + "local_as", + "remote_as", + "tags", + "prefix_list_in", + "prefix_list_out", ] class BGPSessionFilterForm(NetBoxModelFilterSetForm): model = BGPSession - q = forms.CharField( - required=False, - label='Search' - ) + q = forms.CharField(required=False, label="Search") remote_as_id = DynamicModelMultipleChoiceField( - queryset=ASN.objects.all(), - required=False, - label=_('Remote AS') + queryset=ASN.objects.all(), required=False, label=_("Remote AS") ) local_as_id = DynamicModelMultipleChoiceField( - queryset=ASN.objects.all(), - required=False, - label=_('Local AS') - ) - by_local_address = forms.CharField( - required=False, - label='Local Address' - ) - by_remote_address = forms.CharField( - required=False, - label='Remote Address' + queryset=ASN.objects.all(), required=False, label=_("Local AS") ) + by_local_address = forms.CharField(required=False, label="Local Address") + by_remote_address = forms.CharField(required=False, label="Remote Address") device_id = DynamicModelMultipleChoiceField( - queryset=Device.objects.all(), - required=False, - label=_('Device') + queryset=Device.objects.all(), required=False, label=_("Device") ) site_id = DynamicModelMultipleChoiceField( - queryset=Site.objects.all(), - required=False, - label=_('Site') + queryset=Site.objects.all(), required=False, label=_("Site") ) status = forms.MultipleChoiceField( choices=SessionStatusChoices, @@ -367,116 +388,107 @@ class BGPSessionFilterForm(NetBoxModelFilterSetForm): peer_group = DynamicModelMultipleChoiceField( queryset=BGPPeerGroup.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/peer-group/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/peer-group/"), ) import_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/routing-policy/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), ) export_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/routing-policy/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), ) prefix_list_in = DynamicModelMultipleChoiceField( queryset=PrefixList.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/prefix-list/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/prefix-list/"), ) prefix_list_out = DynamicModelMultipleChoiceField( queryset=PrefixList.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/prefix-list/' - ) - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False + widget=APISelectMultiple(api_url="/api/plugins/bgp/prefix-list/"), ) + tenant = DynamicModelChoiceField(queryset=Tenant.objects.all(), required=False) tag = TagFilterField(model) class BGPSessionBulkEditForm(NetBoxModelBulkEditForm): device = DynamicModelChoiceField( - label=_('Device'), + label=_("Device"), queryset=Device.objects.all(), required=False, ) site = DynamicModelChoiceField( - label=_('Site'), - queryset=Site.objects.all(), - required=False + label=_("Site"), queryset=Site.objects.all(), required=False ) status = forms.ChoiceField( - label=_('Status'), + label=_("Status"), required=False, choices=SessionStatusChoices, ) description = forms.CharField( - label=_('Description'), - max_length=200, - required=False + label=_("Description"), max_length=200, required=False ) tenant = DynamicModelChoiceField( - label=_('Tenant'), - queryset=Tenant.objects.all(), - required=False - ) - local_as = DynamicModelChoiceField( - queryset=ASN.objects.all(), - required=False - ) - remote_as = DynamicModelChoiceField( - queryset=ASN.objects.all(), - required=False + label=_("Tenant"), queryset=Tenant.objects.all(), required=False ) + local_as = DynamicModelChoiceField(queryset=ASN.objects.all(), required=False) + remote_as = DynamicModelChoiceField(queryset=ASN.objects.all(), required=False) peer_group = DynamicModelChoiceField( queryset=BGPPeerGroup.objects.all(), required=False, widget=APISelect( - api_url='/api/plugins/bgp/peer-group/', - ) + api_url="/api/plugins/bgp/peer-group/", + ), ) import_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/routing-policy/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), ) export_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/routing-policy/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), ) model = BGPSession + fieldsets = ( - (('Session'), ('device', 'site', 'description', 'status', 'tenant', 'peer_group')), - (('AS'), ('local_as', 'remote_as')), - (('Policies'), ('import_policies', 'export_policies')), + FieldSet( + "name", + "description", + "site", + "device", + "status", + "peer_group", + "tenant", + "tags", + name="Session", + ), + FieldSet("remote_as", "remote_address", name="Remote"), + FieldSet("local_as", "local_address", name="Local"), + FieldSet("import_policies", "export_policies", name="Policies"), + FieldSet("prefix_list_in", "prefix_list_out", name="Prefixes"), ) - nullable_fields = ['tenant', 'description', 'peer_group', 'import_policies', 'export_policies'] + + nullable_fields = [ + "tenant", + "description", + "peer_group", + "import_policies", + "export_policies", + "prefix_list_in", + "prefix_list_out", + ] + class RoutingPolicyFilterForm(NetBoxModelFilterSetForm): model = RoutingPolicy - q = forms.CharField( - required=False, - label='Search' - ) + q = forms.CharField(required=False, label="Search") tag = TagFilterField(model) @@ -487,15 +499,28 @@ class RoutingPolicyForm(NetBoxModelForm): class Meta: model = RoutingPolicy - fields = ['name', 'description', 'tags', 'comments'] + fields = ["name", "description", "tags", "comments"] + + +class RoutingPolicyImportForm(NetBoxModelImportForm): + + class Meta: + model = RoutingPolicy + fields = ("name", "description", "tags") + + +class RoutingPolicyBulkEditForm(NetBoxModelBulkEditForm): + description = forms.CharField(max_length=200, required=False) + + model = RoutingPolicy + nullable_fields = [ + "description", + ] class BGPPeerGroupFilterForm(NetBoxModelFilterSetForm): model = BGPPeerGroup - q = forms.CharField( - required=False, - label='Search' - ) + q = forms.CharField(required=False, label="Search") tag = TagFilterField(model) @@ -504,29 +529,72 @@ class BGPPeerGroupForm(NetBoxModelForm): import_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/routing-policy/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), ) export_policies = DynamicModelMultipleChoiceField( queryset=RoutingPolicy.objects.all(), required=False, - widget=APISelectMultiple( - api_url='/api/plugins/bgp/routing-policy/' - ) + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), ) comments = CommentField() class Meta: model = BGPPeerGroup - fields = ['name', 'description', 'import_policies', 'export_policies', 'tags', 'comments'] + fields = [ + "name", + "description", + "import_policies", + "export_policies", + "tags", + "comments", + ] + + +class BGPPeerGroupImportForm(NetBoxModelImportForm): + + import_policies = CSVModelMultipleChoiceField( + queryset=RoutingPolicy.objects.all(), + to_field_name="name", + required=False, + help_text=_("Import policies name"), + ) + export_policies = CSVModelMultipleChoiceField( + queryset=RoutingPolicy.objects.all(), + to_field_name="name", + required=False, + help_text=_("Export policies name"), + ) + + class Meta: + model = BGPPeerGroup + fields = ("name", "description", "import_policies", "export_policies", "tags") + + +class BGPPeerGroupBulkEditForm(NetBoxModelBulkEditForm): + description = forms.CharField(max_length=200, required=False) + + import_policies = DynamicModelMultipleChoiceField( + queryset=RoutingPolicy.objects.all(), + required=False, + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), + ) + export_policies = DynamicModelMultipleChoiceField( + queryset=RoutingPolicy.objects.all(), + required=False, + widget=APISelectMultiple(api_url="/api/plugins/bgp/routing-policy/"), + ) + + model = BGPPeerGroup + nullable_fields = [ + "description", "import_policies", "export_policies" + ] class RoutingPolicyRuleForm(NetBoxModelForm): continue_entry = forms.IntegerField( required=False, - label='Continue', - help_text='Null for disable, 0 to next entry, or any sequence number' + label="Continue", + help_text="Null for disable, 0 to next entry, or any sequence number", ) match_community = DynamicModelMultipleChoiceField( queryset=Community.objects.all(), @@ -535,44 +603,60 @@ class RoutingPolicyRuleForm(NetBoxModelForm): match_community_list = DynamicModelMultipleChoiceField( queryset=CommunityList.objects.all(), required=False, - ) - match_ip_address = DynamicModelMultipleChoiceField( - queryset=PrefixList.objects.all(), - required=False, - label='Match IP address Prefix lists', ) - match_ipv6_address = DynamicModelMultipleChoiceField( - queryset=PrefixList.objects.all(), - required=False, - label='Match IPv6 address Prefix lists', + match_ip_address = forms.MultipleChoiceField( + choices=[], required=False, label="Match IPv4 address Prefix lists" + ) + + match_ipv6_address = forms.MultipleChoiceField( + choices=[], required=False, label="Match IPv6 address Prefix lists" ) + match_custom = forms.JSONField( - label='Custom Match', + label="Custom Match", help_text='Any custom match statements, e.g., {"ip nexthop": "1.1.1.1"}', required=False, ) set_actions = forms.JSONField( - label='Set statements', + label="Set statements", help_text='Set statements, e.g., {"as-path prepend": [12345,12345]}', - required=False + required=False, ) comments = CommentField() + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + instance = kwargs.get("instance", {}) + if instance: + _prefix_v4 = PrefixList.objects.filter(family="ipv4") + _prefix_v6 = PrefixList.objects.filter(family="ipv6") + prefix_v4 = list(set([(prefix.id, prefix.name) for prefix in _prefix_v4])) + prefix_v6 = list(set([(prefix.id, prefix.name) for prefix in _prefix_v6])) + self.fields["match_ip_address"].choices = prefix_v4 + self.fields["match_ipv6_address"].choices = prefix_v6 + class Meta: model = RoutingPolicyRule fields = [ - 'routing_policy', 'index', 'action', 'continue_entry', 'match_community', - 'match_community_list','match_ip_address', 'match_ipv6_address', 'match_custom', - 'set_actions', 'description', 'tags', 'comments' + "routing_policy", + "index", + "action", + "continue_entry", + "match_community", + "match_community_list", + "match_ip_address", + "match_ipv6_address", + "match_custom", + "set_actions", + "description", + "tags", + "comments", ] class PrefixListFilterForm(NetBoxModelFilterSetForm): model = PrefixList - q = forms.CharField( - required=False, - label='Search' - ) + q = forms.CharField(required=False, label="Search") tag = TagFilterField(model) @@ -583,26 +667,51 @@ class PrefixListForm(NetBoxModelForm): class Meta: model = PrefixList - fields = ['name', 'description', 'family', 'tags', 'comments'] + fields = ["name", "description", "family", "tags", "comments"] + + +class PrefixListImportForm(NetBoxModelImportForm): + family = CSVChoiceField( + choices=IPAddressFamilyChoices, required=True, help_text=_("Family address") + ) + + class Meta: + model = PrefixList + fields = ("name", "description", "family", "tags") + + +class PrefixListBulkEditForm(NetBoxModelBulkEditForm): + description = forms.CharField(max_length=200, required=False) + + family = forms.ChoiceField( + label=_("Family"), + required=False, + choices=IPAddressFamilyChoices, + ) + + model = PrefixList + nullable_fields = [ + "description", + ] class PrefixListRuleForm(NetBoxModelForm): prefix = DynamicModelChoiceField( queryset=Prefix.objects.all(), required=False, - help_text='NetBox Prefix Object', + help_text="NetBox Prefix Object", ) prefix_custom = IPNetworkFormField( required=False, - label='Prefix', - help_text='Just IP field for define special prefix like 0.0.0.0/0', + label="Prefix", + help_text="Just IP field for define special prefix like 0.0.0.0/0", ) ge = forms.IntegerField( - label='Greater than or equal to', + label="Greater than or equal to", required=False, ) le = forms.IntegerField( - label='Less than or equal to', + label="Less than or equal to", required=False, ) comments = CommentField() @@ -610,7 +719,13 @@ class PrefixListRuleForm(NetBoxModelForm): class Meta: model = PrefixListRule fields = [ - 'prefix_list', 'index', - 'action', 'prefix', 'prefix_custom', - 'ge', 'le', 'tags', 'comments' + "prefix_list", + "index", + "action", + "prefix", + "prefix_custom", + "ge", + "le", + "tags", + "comments", ] diff --git a/netbox_bgp/graphql.py b/netbox_bgp/graphql.py deleted file mode 100755 index f048b99..0000000 --- a/netbox_bgp/graphql.py +++ /dev/null @@ -1,52 +0,0 @@ -from graphene import ObjectType, Field - -from netbox.graphql.scalars import BigInt -from netbox.graphql.types import NetBoxObjectType -from netbox.graphql.fields import ObjectField, ObjectListField - -from . import filtersets, models - - -class CommunityType(NetBoxObjectType): - class Meta: - model = models.Community - fields = '__all__' - filterset_class = filtersets.CommunityFilterSet - - -class BgpSessionType(NetBoxObjectType): - class Meta: - model = models.BGPSession - fields = '__all__' - filterset_class = filtersets.BGPSessionFilterSet - - -class PeerGroupType(NetBoxObjectType): - class Meta: - model = models.BGPPeerGroup - fields = '__all__' - filterset_class = filtersets.BGPPeerGroupFilterSet - - -class RoutingPolicyType(NetBoxObjectType): - class Meta: - model = models.RoutingPolicy - fields = '__all__' - filterset_class = filtersets.RoutingPolicyFilterSet - - -class BGPQuery(ObjectType): - community = ObjectField(CommunityType) - community_list = ObjectListField(CommunityType) - - bgp_session = ObjectField(BgpSessionType) - bgp_session_list = ObjectListField(BgpSessionType) - - peer_group = ObjectField(PeerGroupType) - peer_group_list = ObjectListField(PeerGroupType) - - routing_policy = ObjectField(RoutingPolicyType) - routing_policy_list = ObjectListField(RoutingPolicyType) - - -schema = BGPQuery diff --git a/netbox_bgp/graphql/__init__.py b/netbox_bgp/graphql/__init__.py new file mode 100644 index 0000000..40f45b7 --- /dev/null +++ b/netbox_bgp/graphql/__init__.py @@ -0,0 +1,3 @@ +from .schema import NetBoxBGPQuery + +schema = [NetBoxBGPQuery] diff --git a/netbox_bgp/graphql/filters.py b/netbox_bgp/graphql/filters.py new file mode 100644 index 0000000..d842450 --- /dev/null +++ b/netbox_bgp/graphql/filters.py @@ -0,0 +1,93 @@ +import strawberry_django +from netbox.graphql.filter_mixins import autotype_decorator, BaseFilterMixin + +from netbox_bgp.models import ( + Community, + BGPSession, + RoutingPolicy, + BGPPeerGroup, + RoutingPolicyRule, + PrefixList, + PrefixListRule, + CommunityList, + CommunityListRule, +) + +from netbox_bgp.filtersets import ( + CommunityFilterSet, + BGPSessionFilterSet, + BGPPeerGroupFilterSet, + RoutingPolicyFilterSet, + RoutingPolicyRuleFilterSet, + PrefixListFilterSet, + PrefixListRuleFilterSet, + CommunityListFilterSet, + CommunityListRuleFilterSet, +) + + +__all__ = ( + "CommunityFilter", + "BGPSessionFilter", + "BGPPeerGroupFilter", + "RoutingPolicyFilter", + "RoutingPolicyRuleFilter", + "PrefixListFilter", + "PrefixListRuleFilter", + "CommunityListFilter", + "CommunityListRuleFilter", +) + + +@strawberry_django.filter(Community, lookups=True) +@autotype_decorator(CommunityFilterSet) +class CommunityFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(BGPSession, lookups=True) +@autotype_decorator(BGPSessionFilterSet) +class BGPSessionFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(BGPPeerGroup, lookups=True) +@autotype_decorator(BGPPeerGroupFilterSet) +class BGPPeerGroupFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(RoutingPolicy, lookups=True) +@autotype_decorator(RoutingPolicyFilterSet) +class RoutingPolicyFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(RoutingPolicyRule, lookups=True) +@autotype_decorator(RoutingPolicyRuleFilterSet) +class RoutingPolicyRuleFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(PrefixList, lookups=True) +@autotype_decorator(PrefixListFilterSet) +class PrefixListFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(PrefixListRule, lookups=True) +@autotype_decorator(PrefixListRuleFilterSet) +class PrefixListRuleFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(CommunityList, lookups=True) +@autotype_decorator(CommunityListFilterSet) +class CommunityListFilter(BaseFilterMixin): + pass + + +@strawberry_django.filter(CommunityListRule, lookups=True) +@autotype_decorator(CommunityListRuleFilterSet) +class CommunityListRuleFilter(BaseFilterMixin): + pass diff --git a/netbox_bgp/graphql/schema.py b/netbox_bgp/graphql/schema.py new file mode 100644 index 0000000..9154578 --- /dev/null +++ b/netbox_bgp/graphql/schema.py @@ -0,0 +1,84 @@ +from typing import List + +import strawberry +import strawberry_django + +from netbox_bgp.models import ( + Community, + BGPSession, + RoutingPolicy, + BGPPeerGroup, + RoutingPolicyRule, + PrefixList, + PrefixListRule, + CommunityList, + CommunityListRule, +) +from .types import ( + CommunityType, + BGPSessionType, + BGPPeerGroupType, + RoutingPolicyType, + RoutingPolicyRuleType, + PrefixListType, + PrefixListRuleType, + CommunityListType, + CommunityListRuleType, +) + + +@strawberry.type +class NetBoxBGPQuery: + @strawberry.field + def netbox_bgp_community(self, id: int) -> CommunityType: + return Community.objects.get(pk=id) + + netbox_bgp_community_list: List[CommunityType] = strawberry_django.field() + + @strawberry.field + def netbox_bgp_session(self, id: int) -> BGPSessionType: + return BGPSession.objects.get(pk=id) + + netbox_bgp_session_list: List[BGPSessionType] = strawberry_django.field() + + @strawberry.field + def netbox_bgp_peer_group(self, id: int) -> BGPPeerGroupType: + return BGPPeerGroup.objects.get(pk=id) + + netbox_bgp_peer_group_list: List[BGPPeerGroupType] = strawberry_django.field() + + @strawberry.field + def netbox_bgp_routing_policy(self, id: int) -> RoutingPolicyType: + return RoutingPolicy.objects.get(pk=id) + + netbox_bgp_routing_policy_list: List[RoutingPolicyType] = strawberry_django.field() + + @strawberry.field + def netbox_bgp_routing_policy_rule(self, id: int) -> RoutingPolicyRuleType: + return RoutingPolicyRule.objects.get(pk=id) + + netbox_bgp_routing_policy_rule_list: List[RoutingPolicyRuleType] = strawberry_django.field() + + @strawberry.field + def netbox_bgp_prefixlist(self, id: int) -> PrefixListType: + return PrefixList.objects.get(pk=id) + + netbox_bgp_prefixlist_list: List[PrefixListType] = strawberry_django.field() + + @strawberry.field + def netbox_bgp_prefixlist_rule(self, id: int) -> PrefixListRuleType: + return PrefixListRule.objects.get(pk=id) + + netbox_bgp_prefixlist_rule_list: List[PrefixListRuleType] = strawberry_django.field() + + @strawberry.field + def netbox_bgp_communitylist(self, id: int) -> CommunityListType: + return CommunityList.objects.get(pk=id) + + netbox_bgp_communitylist_list: List[CommunityListType] = strawberry_django.field() + + @strawberry.field + def netbox_bgp_communitylist_rule(self, id: int) -> CommunityListRuleType: + return CommunityListRule.objects.get(pk=id) + + netbox_bgp_communitylist_rule_list: List[CommunityListRuleType] = strawberry_django.field() diff --git a/netbox_bgp/graphql/types.py b/netbox_bgp/graphql/types.py new file mode 100644 index 0000000..716c01e --- /dev/null +++ b/netbox_bgp/graphql/types.py @@ -0,0 +1,152 @@ +from typing import Annotated, List + +import strawberry +import strawberry_django + + +from netbox.graphql.types import NetBoxObjectType +from netbox.graphql.scalars import BigInt + +from netbox_bgp.models import ( + Community, + BGPSession, + RoutingPolicy, + BGPPeerGroup, + RoutingPolicyRule, + PrefixList, + PrefixListRule, + CommunityList, + CommunityListRule, +) +from .filters import ( + CommunityFilter, + BGPSessionFilter, + BGPPeerGroupFilter, + RoutingPolicyFilter, + RoutingPolicyRuleFilter, + PrefixListFilter, + PrefixListRuleFilter, + CommunityListFilter, + CommunityListRuleFilter, +) + + +@strawberry_django.type(Community, fields="__all__", filters=CommunityFilter) +class CommunityType(NetBoxObjectType): + site: Annotated["SiteType", strawberry.lazy("dcim.graphql.types")] | None + tenant: Annotated["TenantType", strawberry.lazy("tenancy.graphql.types")] | None + status: str + role: str + description: str + + +@strawberry_django.type(BGPSession, fields="__all__", filters=BGPSessionFilter) +class BGPSessionType(NetBoxObjectType): + name: str + site: Annotated["SiteType", strawberry.lazy("dcim.graphql.types")] | None + tenant: Annotated["TenantType", strawberry.lazy("tenancy.graphql.types")] | None + device: Annotated["DeviceType", strawberry.lazy("dcim.graphql.types")] + local_address: Annotated["IPAddressType", strawberry.lazy("ipam.graphql.types")] + remote_address: Annotated["IPAddressType", strawberry.lazy("ipam.graphql.types")] + local_as: Annotated["ASNType", strawberry.lazy("ipam.graphql.types")] + remote_as: Annotated["ASNType", strawberry.lazy("ipam.graphql.types")] + status: str + description: str + peer_group: ( + Annotated["BGPPeerGroupType", strawberry.lazy("netbox_bgp.graphql.types")] + | None + ) + import_policies: List[ + Annotated["RoutingPolicyType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + export_policies: List[ + Annotated["RoutingPolicyType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + prefix_list_in: List[ + Annotated["PrefixListType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + prefix_list_out: List[ + Annotated["PrefixListType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + + +@strawberry_django.type(BGPPeerGroup, fields="__all__", filters=BGPPeerGroupFilter) +class BGPPeerGroupType(NetBoxObjectType): + name: str + description: str + import_policies: List[ + Annotated["RoutingPolicyType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + export_policies: List[ + Annotated["RoutingPolicyType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + + +@strawberry_django.type(RoutingPolicy, fields="__all__", filters=RoutingPolicyFilter) +class RoutingPolicyType(NetBoxObjectType): + name: str + description: str + + +@strawberry_django.type( + RoutingPolicyRule, fields="__all__", filters=RoutingPolicyRuleFilter +) +class RoutingPolicyRuleType(NetBoxObjectType): + routing_policy: Annotated[ + "RoutingPolicyType", strawberry.lazy("netbox_bgp.graphql.types") + ] + index: BigInt + action: str + description: str + continue_entry: BigInt + match_community: List[ + Annotated["CommunityType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + match_community_list: List[ + Annotated["CommunityListType", strawberry.lazy("netbox_bgp.graphql.types")] + ] + match_ip_address: List[ + Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] + ] + match_ipv6_address: List[ + Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] + ] + + +@strawberry_django.type(PrefixList, fields="__all__", filters=PrefixListFilter) +class PrefixListType(NetBoxObjectType): + name: str + description: str + family: str + + +@strawberry_django.type(PrefixListRule, fields="__all__", filters=PrefixListRuleFilter) +class PrefixListRuleType(NetBoxObjectType): + prefix_list: Annotated[ + "PrefixListType", strawberry.lazy("netbox_bgp.graphql.types") + ] + index: BigInt + action: str + prefix: Annotated["PrefixType", strawberry.lazy("ipam.graphql.types")] + prefix_custom: str + ge: BigInt + le: BigInt + description: str + + +@strawberry_django.type(CommunityList, fields="__all__", filters=CommunityListFilter) +class CommunityListType(NetBoxObjectType): + name: str + description: str + + +@strawberry_django.type( + CommunityListRule, fields="__all__", filters=CommunityListRuleFilter +) +class CommunityListRuleType(NetBoxObjectType): + community_list: Annotated[ + "CommunityListType", strawberry.lazy("netbox_bgp.graphql.types") + ] + action: str + community: Annotated["CommunityType", strawberry.lazy("netbox_bgp.graphql.types")] + description: str diff --git a/netbox_bgp/navigation.py b/netbox_bgp/navigation.py index 430f38d..dcf7999 100644 --- a/netbox_bgp/navigation.py +++ b/netbox_bgp/navigation.py @@ -1,7 +1,6 @@ from django.conf import settings -from extras.plugins import PluginMenuButton, PluginMenuItem, PluginMenu -from utilities.choices import ButtonColorChoices +from netbox.plugins import PluginMenuButton, PluginMenuItem, PluginMenu _menu_items = ( @@ -12,9 +11,14 @@ buttons=( PluginMenuButton( link='plugins:netbox_bgp:community_add', - title='Communities', + title='Add', icon_class='mdi mdi-plus-thick', - color=ButtonColorChoices.GREEN, + permissions=['netbox_bgp.add_community'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:community_import', + title='Import', + icon_class='mdi mdi-upload', permissions=['netbox_bgp.add_community'], ), ), @@ -26,9 +30,14 @@ buttons=( PluginMenuButton( link='plugins:netbox_bgp:communitylist_add', - title='Community Lists', + title='Add', icon_class='mdi mdi-plus-thick', - color=ButtonColorChoices.GREEN, + permissions=['netbox_bgp.add_communitylist'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:communitylist_import', + title='Import', + icon_class='mdi mdi-upload', permissions=['netbox_bgp.add_communitylist'], ), ), @@ -40,11 +49,16 @@ buttons=( PluginMenuButton( link='plugins:netbox_bgp:bgpsession_add', - title='Sessions', + title='Add', icon_class='mdi mdi-plus-thick', - color=ButtonColorChoices.GREEN, permissions=['netbox_bgp.add_bgpsession'], ), + PluginMenuButton( + link='plugins:netbox_bgp:bgpsession_import', + title='Import', + icon_class='mdi mdi-upload', + permissions=['netbox_bgp.add_bgpsession'], + ) ), ), PluginMenuItem( @@ -54,9 +68,14 @@ buttons=( PluginMenuButton( link='plugins:netbox_bgp:routingpolicy_add', - title='Routing Policies', + title='Add', icon_class='mdi mdi-plus-thick', - color=ButtonColorChoices.GREEN, + permissions=['netbox_bgp.add_routingpolicy'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:routingpolicy_import', + title='Import', + icon_class='mdi mdi-upload', permissions=['netbox_bgp.add_routingpolicy'], ), ), @@ -68,9 +87,14 @@ buttons=( PluginMenuButton( link='plugins:netbox_bgp:prefixlist_add', - title='Prefix Lists', + title='Add', icon_class='mdi mdi-plus-thick', - color=ButtonColorChoices.GREEN, + permissions=['netbox_bgp.add_prefixlist'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:prefixlist_import', + title='Import', + icon_class='mdi mdi-upload', permissions=['netbox_bgp.add_prefixlist'], ), ), @@ -82,9 +106,14 @@ buttons=( PluginMenuButton( link='plugins:netbox_bgp:bgppeergroup_add', - title='Peer Groups', + title='Add', icon_class='mdi mdi-plus-thick', - color=ButtonColorChoices.GREEN, + permissions=['netbox_bgp.add_bgppeergroup'], + ), + PluginMenuButton( + link='plugins:netbox_bgp:bgppeergroup_import', + title='Import', + icon_class='mdi mdi-upload', permissions=['netbox_bgp.add_bgppeergroup'], ), ), diff --git a/netbox_bgp/tables.py b/netbox_bgp/tables.py index b455e48..b46f6c9 100644 --- a/netbox_bgp/tables.py +++ b/netbox_bgp/tables.py @@ -44,7 +44,7 @@ class CommunityTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = Community - fields = ('pk', 'value', 'description', 'status', 'tenant', 'tags') + fields = ('pk', 'value', 'description', 'status', 'tenant', 'tags', 'actions') default_columns = ( 'pk', 'value', 'description', 'status', 'tenant' ) @@ -55,7 +55,7 @@ class CommunityListTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = CommunityList - fields = ('pk', 'name', 'description') + fields = ('pk', 'name', 'description', 'actions') class CommunityListRuleTable(NetBoxTable): @@ -97,7 +97,7 @@ class Meta(NetBoxTable.Meta): fields = ( 'pk', 'name', 'device', 'local_address', 'local_as', 'remote_address', 'remote_as', 'description', 'peer_group', - 'site', 'status', 'tenant' + 'site', 'status', 'tenant', 'actions' ) default_columns = ( 'pk', 'name', 'device', 'local_address', 'local_as', @@ -111,7 +111,7 @@ class RoutingPolicyTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = RoutingPolicy - fields = ('pk', 'name', 'description') + fields = ('pk', 'name', 'description', 'actions') class BGPPeerGroupTable(NetBoxTable): @@ -132,7 +132,7 @@ class Meta(NetBoxTable.Meta): model = BGPPeerGroup fields = ( 'pk', 'name', 'description', 'tags', - 'import_policies', 'export_policies' + 'import_policies', 'export_policies', 'actions' ) default_columns = ( 'pk', 'name', 'description' @@ -158,10 +158,11 @@ class Meta(NetBoxTable.Meta): class PrefixListTable(NetBoxTable): name = tables.LinkColumn() + family = ChoiceFieldColumn() class Meta(NetBoxTable.Meta): model = PrefixList - fields = ('pk', 'name', 'description') + fields = ('pk', 'name', 'description', 'family', 'actions') class PrefixListRuleTable(NetBoxTable): diff --git a/netbox_bgp/template_content.py b/netbox_bgp/template_content.py index 57198a9..ead17ff 100644 --- a/netbox_bgp/template_content.py +++ b/netbox_bgp/template_content.py @@ -1,4 +1,4 @@ -from extras.plugins import PluginTemplateExtension +from netbox.plugins import PluginTemplateExtension from .models import BGPSession from .tables import BGPSessionTable diff --git a/netbox_bgp/templates/netbox_bgp/bgppeergroup.html b/netbox_bgp/templates/netbox_bgp/bgppeergroup.html index 881c507..7a724a4 100644 --- a/netbox_bgp/templates/netbox_bgp/bgppeergroup.html +++ b/netbox_bgp/templates/netbox_bgp/bgppeergroup.html @@ -9,20 +9,6 @@ {% endblock %} -{% block controls %} -
- {% if perms.netbox_bgp.change_peergroup %} - - Edit - - {% endif %} - {% if perms.netbox_bgp.delete_peergroup %} - - Delete - - {% endif %} -
-{% endblock controls %} {% block tabs %}