diff --git a/README.md b/README.md index a3d0e60..a27403f 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,6 @@ BGP Session BGP Sessions ![BGP Session Table](docs/img/bgp_sess_list.png) -ASN -![ASN](docs/img/asn.png) - -ASNs -![ASN Table](docs/img/asn_list.png) - Community ![Community](docs/img/commun.png) diff --git a/develop/Dockerfile b/develop/Dockerfile index 1635006..9481362 100644 --- a/develop/Dockerfile +++ b/develop/Dockerfile @@ -1,4 +1,4 @@ -ARG python_ver=3.7 +ARG python_ver=3.8 FROM python:${python_ver} ARG netbox_ver=master diff --git a/docs/img/asn.png b/docs/img/asn.png deleted file mode 100644 index d5cc2c8..0000000 Binary files a/docs/img/asn.png and /dev/null differ diff --git a/docs/img/asn_list.png b/docs/img/asn_list.png deleted file mode 100644 index 8657ab0..0000000 Binary files a/docs/img/asn_list.png and /dev/null differ diff --git a/netbox_bgp/admin.py b/netbox_bgp/admin.py index 91382f2..f78f40d 100644 --- a/netbox_bgp/admin.py +++ b/netbox_bgp/admin.py @@ -1,10 +1,5 @@ from django.contrib import admin -from .models import ASN, Community, BGPSession, BGPPeerGroup, RoutingPolicy - - -@admin.register(ASN) -class ASNAdmin(admin.ModelAdmin): - fields = ('number', 'status', 'description') +from .models import Community, BGPSession, BGPPeerGroup, RoutingPolicy @admin.register(Community) diff --git a/netbox_bgp/api/serializers.py b/netbox_bgp/api/serializers.py index 495c678..3e11d68 100644 --- a/netbox_bgp/api/serializers.py +++ b/netbox_bgp/api/serializers.py @@ -6,11 +6,11 @@ from dcim.api.nested_serializers import NestedSiteSerializer, NestedDeviceSerializer from tenancy.api.nested_serializers import NestedTenantSerializer from extras.api.nested_serializers import NestedTagSerializer -from ipam.api.nested_serializers import NestedIPAddressSerializer +from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedASNSerializer from netbox_bgp.models import ( - ASN, ASNStatusChoices, BGPSession, SessionStatusChoices, RoutingPolicy, BGPPeerGroup, + CommunityStatusChoices, BGPSession, SessionStatusChoices, RoutingPolicy, BGPPeerGroup, Community, RoutingPolicyRule ) @@ -25,39 +25,6 @@ def to_representation(self, value): return self.serializer(value, context={'request': self.context['request']}).data -class ASNSerializer(NetBoxModelSerializer): - status = ChoiceField(choices=ASNStatusChoices, required=False) - site = NestedSiteSerializer(required=False, allow_null=True) - tenant = NestedTenantSerializer(required=False, allow_null=True) - - def validate(self, attrs): - try: - number = attrs['number'] - tenant = attrs.get('tenant') - except KeyError: - # this is patch - return attrs - if ASN.objects.filter(number=number, tenant=tenant).exists(): - raise ValidationError( - {'error': 'Asn with this Number and Tenant already exists.'} - ) - return attrs - - class Meta: - ref_name = 'BGP_ASN' - model = ASN - fields = ['number', 'id', 'display', 'status', 'description', 'custom_fields', 'site', 'tenant', 'tags'] - - -class NestedASNSerializer(WritableNestedSerializer): - url = HyperlinkedIdentityField(view_name='plugins:netbox_bgp:asn') - - class Meta: - ref_name = 'BGP_ASN_Nested' - model = ASN - fields = ['id', 'url', 'number', 'description'] - - class RoutingPolicySerializer(NetBoxModelSerializer): class Meta: model = RoutingPolicy @@ -170,7 +137,7 @@ class Meta: validators = [] class CommunitySerializer(NetBoxModelSerializer): - status = ChoiceField(choices=ASNStatusChoices, required=False) + status = ChoiceField(choices=CommunityStatusChoices, required=False) tenant = NestedTenantSerializer(required=False, allow_null=True) class Meta: diff --git a/netbox_bgp/api/urls.py b/netbox_bgp/api/urls.py index 4aea08a..82164b0 100644 --- a/netbox_bgp/api/urls.py +++ b/netbox_bgp/api/urls.py @@ -1,12 +1,8 @@ from rest_framework import routers -from .views import ( - ASNViewSet, BGPSessionViewSet, RoutingPolicyViewSet, BGPPeerGroupViewSet, - CommunityViewSet -) +from .views import BGPSessionViewSet, RoutingPolicyViewSet, BGPPeerGroupViewSet, CommunityViewSet router = routers.DefaultRouter() -router.register('asn', ASNViewSet) router.register('session', BGPSessionViewSet, 'session') router.register('bgpsession', BGPSessionViewSet, 'bgpsession') router.register('routing-policy', RoutingPolicyViewSet) diff --git a/netbox_bgp/api/views.py b/netbox_bgp/api/views.py index 7619f2a..9184740 100644 --- a/netbox_bgp/api/views.py +++ b/netbox_bgp/api/views.py @@ -1,22 +1,16 @@ from rest_framework.viewsets import ModelViewSet from .serializers import ( - ASNSerializer, BGPSessionSerializer, RoutingPolicySerializer, BGPPeerGroupSerializer, + BGPSessionSerializer, RoutingPolicySerializer, BGPPeerGroupSerializer, CommunitySerializer ) -from netbox_bgp.models import ASN, BGPSession, RoutingPolicy, BGPPeerGroup, Community +from netbox_bgp.models import BGPSession, RoutingPolicy, BGPPeerGroup, Community from netbox_bgp.filters import ( - ASNFilterSet, BGPSessionFilterSet, RoutingPolicyFilterSet, BGPPeerGroupFilterSet, + BGPSessionFilterSet, RoutingPolicyFilterSet, BGPPeerGroupFilterSet, CommunityFilterSet ) -class ASNViewSet(ModelViewSet): - queryset = ASN.objects.all() - serializer_class = ASNSerializer - filterset_class = ASNFilterSet - - class BGPSessionViewSet(ModelViewSet): queryset = BGPSession.objects.all() serializer_class = BGPSessionSerializer diff --git a/netbox_bgp/filters.py b/netbox_bgp/filters.py index f58ff43..9c2093a 100644 --- a/netbox_bgp/filters.py +++ b/netbox_bgp/filters.py @@ -4,34 +4,11 @@ from netaddr.core import AddrFormatError from extras.filters import TagFilter -from .models import ASN, Community, BGPSession, RoutingPolicy, BGPPeerGroup -from ipam.models import IPAddress +from .models import Community, BGPSession, RoutingPolicy, BGPPeerGroup +from ipam.models import IPAddress, ASN from dcim.models import Device -class ASNFilterSet(django_filters.FilterSet): - q = django_filters.CharFilter( - method='search', - label='Search', - ) - tag = TagFilter() - - class Meta: - model = ASN - fields = ['number', 'description', 'status', 'tenant', 'site'] - - def search(self, queryset, name, value): - """Perform the filtered search.""" - if not value.strip(): - return queryset - qs_filter = ( - Q(id__icontains=value) - | Q(number__icontains=value) - | Q(description__icontains=value) - ) - return queryset.filter(qs_filter) - - class CommunityFilterSet(django_filters.FilterSet): q = django_filters.CharFilter( method='search', @@ -65,7 +42,7 @@ class BGPSessionFilterSet(django_filters.FilterSet): remote_as = django_filters.ModelMultipleChoiceFilter( field_name='remote_as__number', queryset=ASN.objects.all(), - to_field_name='number', + to_field_name='asn', label='Remote AS (Number)', ) remote_as_id = django_filters.ModelMultipleChoiceFilter( @@ -77,7 +54,7 @@ class BGPSessionFilterSet(django_filters.FilterSet): local_as = django_filters.ModelMultipleChoiceFilter( field_name='local_as__number', queryset=ASN.objects.all(), - to_field_name='number', + to_field_name='asn', label='Local AS (Number)', ) local_as_id = django_filters.ModelMultipleChoiceFilter( diff --git a/netbox_bgp/forms.py b/netbox_bgp/forms.py index 9f8ca94..0abf567 100644 --- a/netbox_bgp/forms.py +++ b/netbox_bgp/forms.py @@ -8,7 +8,7 @@ from extras.models import Tag from tenancy.models import Tenant from dcim.models import Device, Site -from ipam.models import IPAddress, Prefix +from ipam.models import IPAddress, Prefix, ASN from ipam.formfields import IPNetworkFormField from utilities.forms import ( DynamicModelChoiceField, @@ -18,7 +18,7 @@ from netbox.forms import NetBoxModelForm, NetBoxModelBulkEditForm, NetBoxModelFilterSetForm from .models import ( - ASN, ASNStatusChoices, Community, BGPSession, + CommunityStatusChoices, Community, BGPSession, SessionStatusChoices, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule ) @@ -26,137 +26,6 @@ from django.forms.widgets import TextInput -class ASNField(forms.CharField): - ''' - Return int value, but allows to input dotted digit text - ''' - - def to_python(self, value): - if not re.match(r'^\d+(\.\d+)?$', value): - raise ValidationError('Invalid AS Number: {}'.format(value)) - if '.' in value: - if int(value.split('.')[0]) > 65535 or int(value.split('.')[1]) > 65535: - raise ValidationError('Invalid AS Number: {}'.format(value)) - try: - return int(value.split('.')[0]) * 65536 + int(value.split('.')[1]) - except ValueError: - raise ValidationError('Invalid AS Number: {}'.format(value)) - try: - return int(value) - except ValueError: - raise ValidationError('Invalid AS Number: {}'.format(value)) - - -class ASdotInput(TextInput): - def _format_value(self, value): - if not value: - return 0 - if type(value) is str: - return value - if int(value) > 65535: - return '{}.{}'.format(value // 65536, value % 65536) - else: - return value - - def render(self, name, value, attrs=None, renderer=None): - nb_settings = settings.PLUGINS_CONFIG.get('netbox_bgp', {}) - asdot = nb_settings.get('asdot', False) - if asdot: - value = self._format_value(value) - return super().render(name, value, attrs, renderer) - - -class ASNFilterForm(NetBoxModelFilterSetForm): - model = ASN - q = forms.CharField( - required=False, - label='Search' - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - status = forms.MultipleChoiceField( - choices=ASNStatusChoices, - required=False, - widget=StaticSelectMultiple() - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False - ) - - tag = TagFilterField(model) - - -class ASNForm(NetBoxModelForm): - number = ASNField( - widget=ASdotInput - ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - site = DynamicModelChoiceField( - queryset=Site.objects.all(), - required=False - ) - status = forms.ChoiceField( - required=False, - choices=ASNStatusChoices, - widget=StaticSelect() - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - - def clean(self): - cleaned_data = super().clean() - if self.errors.get('number'): - return cleaned_data - number = cleaned_data.get('number') - tenant = cleaned_data.get('tenant') - if 'number' in self.changed_data or 'tenant' in self.changed_data: - if ASN.objects.filter(number=number, tenant=tenant).exists(): - raise forms.ValidationError('AS number with this number and tenant is already exists.') - return cleaned_data - - class Meta: - model = ASN - fields = [ - 'number', 'description', 'status', 'site', 'tenant', 'tags', - ] - - -class ASNBulkEditForm(NetBoxModelBulkEditForm): - pk = forms.ModelMultipleChoiceField( - queryset=ASN.objects.all(), - widget=forms.MultipleHiddenInput - ) - tenant = DynamicModelChoiceField( - queryset=Tenant.objects.all(), - required=False - ) - description = forms.CharField( - max_length=200, - required=False - ) - status = forms.ChoiceField( - required=False, - choices=ASNStatusChoices, - widget=StaticSelect() - ) - - model = ASN - fieldsets = ( - (None, ('tenant', 'description', 'status')), - ) - nullable_fields = ( - 'tenant', 'description', - ) - - class CommunityForm(NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -164,7 +33,7 @@ class CommunityForm(NetBoxModelForm): ) status = forms.ChoiceField( required=False, - choices=ASNStatusChoices, + choices=CommunityStatusChoices, widget=StaticSelect() ) tenant = DynamicModelChoiceField( @@ -189,7 +58,7 @@ class CommunityFilterForm(NetBoxModelFilterSetForm): required=False ) status = forms.MultipleChoiceField( - choices=ASNStatusChoices, + choices=CommunityStatusChoices, required=False, widget=StaticSelectMultiple() ) @@ -218,7 +87,7 @@ class CommunityBulkEditForm(NetBoxModelBulkEditForm): ) status = forms.ChoiceField( required=False, - choices=ASNStatusChoices, + choices=CommunityStatusChoices, widget=StaticSelect() ) @@ -257,19 +126,11 @@ class BGPSessionForm(NetBoxModelForm): query_params={ 'site_id': '$site' }, - widget=APISelect( - api_url='/api/plugins/bgp/asn/', - ) - + label=_('Local AS') ) remote_as = DynamicModelChoiceField( queryset=ASN.objects.all(), - query_params={ - 'site_id': '$site' - }, - widget=APISelect( - api_url='/api/plugins/bgp/asn/', - ) + label=_('Remote AS') ) local_address = DynamicModelChoiceField( queryset=IPAddress.objects.all(), @@ -343,18 +204,12 @@ class BGPSessionFilterForm(NetBoxModelFilterSetForm): remote_as_id = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(), required=False, - label=_('Remote AS'), - widget=APISelectMultiple( - api_url='/api/plugins/bgp/asn/', - ) + label=_('Remote AS') ) local_as_id = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(), required=False, - label=_('Local AS'), - widget=APISelectMultiple( - api_url='/api/plugins/bgp/asn/', - ) + label=_('Local AS') ) by_local_address = forms.CharField( required=False, diff --git a/netbox_bgp/graphql.py b/netbox_bgp/graphql.py index 3145d6e..8022cc2 100755 --- a/netbox_bgp/graphql.py +++ b/netbox_bgp/graphql.py @@ -14,15 +14,6 @@ class Meta: filterset_class = filters.CommunityFilterSet -class AsnType(NetBoxObjectType): - number = Field(BigInt) - - class Meta: - model = models.ASN - fields = '__all__' - filterset_class = filters.ASNFilterSet - - class BgpSessionType(NetBoxObjectType): class Meta: model = models.BGPSession @@ -48,9 +39,6 @@ class BGPQuery(ObjectType): community = ObjectField(CommunityType) community_list = ObjectListField(CommunityType) - bgp_asn = ObjectField(AsnType) - bgp_asn_list = ObjectListField(AsnType) - bgp_session = ObjectField(BgpSessionType) bgp_session_list = ObjectListField(BgpSessionType) diff --git a/netbox_bgp/migrations/0023_netbox_bgp.py b/netbox_bgp/migrations/0023_netbox_bgp.py new file mode 100644 index 0000000..836bbfa --- /dev/null +++ b/netbox_bgp/migrations/0023_netbox_bgp.py @@ -0,0 +1,35 @@ +# Generated by Django 4.0.6 on 2022-07-29 10:29 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0057_created_datetimefield'), + ('netbox_bgp', '0022_netbox_bgp'), + ] + + operations = [ + migrations.RemoveField( + model_name='asngroup', + name='site', + ), + migrations.AlterField( + model_name='bgpsession', + name='local_as', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='local_as', to='ipam.asn'), + ), + migrations.AlterField( + model_name='bgpsession', + name='remote_as', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='remote_as', to='ipam.asn'), + ), + migrations.DeleteModel( + name='ASN', + ), + migrations.DeleteModel( + name='ASNGroup', + ), + ] diff --git a/netbox_bgp/models.py b/netbox_bgp/models.py index cc592e4..4b99e86 100644 --- a/netbox_bgp/models.py +++ b/netbox_bgp/models.py @@ -12,7 +12,7 @@ from ipam.models import Prefix -class ASNStatusChoices(ChoiceSet): +class CommunityStatusChoices(ChoiceSet): STATUS_ACTIVE = 'active' STATUS_RESERVED = 'reserved' @@ -48,27 +48,6 @@ class ActionChoices(ChoiceSet): ] -class ASNGroup(ChangeLoggingMixin, models.Model): - """ - """ - name = models.CharField( - max_length=100 - ) - site = models.ForeignKey( - to='dcim.Site', - on_delete=models.SET_NULL, - blank=True, - null=True - ) - description = models.CharField( - max_length=200, - blank=True - ) - - def __str__(self): - return self.name - - class RoutingPolicy(NetBoxModel): """ """ @@ -141,8 +120,8 @@ class BGPBase(NetBoxModel): ) status = models.CharField( max_length=50, - choices=ASNStatusChoices, - default=ASNStatusChoices.STATUS_ACTIVE + choices=CommunityStatusChoices, + default=CommunityStatusChoices.STATUS_ACTIVE ) role = models.ForeignKey( to='ipam.Role', @@ -159,59 +138,6 @@ class Meta: abstract = True -class ASN(BGPBase): - - number = models.PositiveBigIntegerField( - validators=[MinValueValidator(1), MaxValueValidator(4294967295)] - ) - - group = models.ForeignKey( - ASNGroup, - on_delete=models.PROTECT, - blank=True, - null=True - ) - - tags = TaggableManager(through='extras.TaggedItem', related_name='asn_tags') - - clone_fields = ['description', 'status', 'tenant'] - - class Meta: - verbose_name = 'AS Number' - verbose_name_plural = 'AS Numbers' - constraints = [ - models.UniqueConstraint( - fields=['number', 'tenant'], - name='uniqie_number_tenant' - ), - models.UniqueConstraint( - fields=['number'], - condition=models.Q(tenant=None), - name='uniqie_number' - ), - ] - # unique_together = ['number', 'site', 'tenant'] - - def get_status_color(self): - return ASNStatusChoices.colors.get(self.status) - - def get_absolute_url(self): - return reverse('plugins:netbox_bgp:asn', args=[self.pk]) - - def get_asdot(self): - if self.number > 65535: - return '{}.{}'.format(self.number // 65536, self.number % 65536) - else: - return str(self.number) - - def __str__(self): - nb_settings = settings.PLUGINS_CONFIG.get('netbox_bgp', {}) - asdot = nb_settings.get('asdot', False) - if asdot: - return self.get_asdot() - return str(self.number) - - class Community(BGPBase): """ """ @@ -227,7 +153,7 @@ def __str__(self): return self.value def get_status_color(self): - return ASNStatusChoices.colors.get(self.status) + return CommunityStatusChoices.colors.get(self.status) def get_absolute_url(self): return reverse('plugins:netbox_bgp:community', args=[self.pk]) @@ -267,12 +193,12 @@ class BGPSession(NetBoxModel): related_name='remote_address' ) local_as = models.ForeignKey( - ASN, + to='ipam.ASN', on_delete=models.PROTECT, related_name='local_as' ) remote_as = models.ForeignKey( - ASN, + to='ipam.ASN', on_delete=models.PROTECT, related_name='remote_as' ) diff --git a/netbox_bgp/navigation.py b/netbox_bgp/navigation.py index 712e2bf..0f3d3b5 100644 --- a/netbox_bgp/navigation.py +++ b/netbox_bgp/navigation.py @@ -2,20 +2,6 @@ from utilities.choices import ButtonColorChoices menu_items = ( - PluginMenuItem( - link='plugins:netbox_bgp:asn_list', - link_text='AS Numbers', - permissions=['netbox_bgp.view_asn'], - buttons=( - PluginMenuButton( - link='plugins:netbox_bgp:asn_add', - title='AS Numbers', - icon_class='mdi mdi-plus-thick', - color=ButtonColorChoices.GREEN, - permissions=['netbox_bgp.add_asn'], - ), - ), - ), PluginMenuItem( link='plugins:netbox_bgp:community_list', link_text='Communities', diff --git a/netbox_bgp/tables.py b/netbox_bgp/tables.py index 509c4ee..3b2fcd1 100644 --- a/netbox_bgp/tables.py +++ b/netbox_bgp/tables.py @@ -5,7 +5,7 @@ from netbox.tables import NetBoxTable from netbox.tables.columns import ChoiceFieldColumn, TagColumn -from .models import ASN, Community, BGPSession, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule +from .models import Community, BGPSession, RoutingPolicy, BGPPeerGroup, RoutingPolicyRule AVAILABLE_LABEL = mark_safe('Available') COL_TENANT = """ @@ -25,21 +25,6 @@ """ -class ASNTable(NetBoxTable): - number = tables.LinkColumn(text=lambda record: record.__str__(), args=[A('pk')]) - status = ChoiceFieldColumn( - default=AVAILABLE_LABEL - ) - site = tables.LinkColumn() - tenant = tables.TemplateColumn( - template_code=COL_TENANT - ) - - class Meta(NetBoxTable.Meta): - model = ASN - fields = ('pk', 'number', 'description', 'status', 'tenant') - - class CommunityTable(NetBoxTable): value = tables.LinkColumn() status = ChoiceFieldColumn( diff --git a/netbox_bgp/templates/netbox_bgp/asn.html b/netbox_bgp/templates/netbox_bgp/asn.html deleted file mode 100644 index d5bec68..0000000 --- a/netbox_bgp/templates/netbox_bgp/asn.html +++ /dev/null @@ -1,106 +0,0 @@ -{% extends 'generic/object.html' %} -{% load buttons %} -{% load custom_links %} -{% load helpers %} -{% load plugins %} -{% load render_table from django_tables2 %} - -{% block breadcrumbs %} -
Site | -- {% if object.site %} - {{ object.site }} - {% else %} - None - {% endif %} - | -
Number | -{{ object }} | -
Tenant | -- {% if object.tenant %} - {{ object.tenant }} - {% else %} - None - {% endif %} - | -
Status | -- {{ object.get_status_display }} - | -
Description | -{{ object.description|placeholder }} | -