From 7e7c9e981d1a07a7acd596956d290562379599b7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Sat, 25 Feb 2023 14:52:42 -0500 Subject: [PATCH] Introduce ASNRange model --- netbox/ipam/api/nested_serializers.py | 13 +++++ netbox/ipam/api/serializers.py | 20 +++++-- netbox/ipam/api/urls.py | 24 +-------- netbox/ipam/api/views.py | 8 +++ netbox/ipam/filtersets.py | 14 +++++ netbox/ipam/forms/bulk_edit.py | 18 +++++++ netbox/ipam/forms/bulk_import.py | 14 +++++ netbox/ipam/forms/filtersets.py | 17 ++++++ netbox/ipam/forms/model_forms.py | 15 ++++++ netbox/ipam/graphql/schema.py | 3 ++ netbox/ipam/graphql/types.py | 10 +++- netbox/ipam/migrations/0064_asnrange.py | 45 ++++++++++++++++ netbox/ipam/models/__init__.py | 1 + netbox/ipam/models/asns.py | 57 +++++++++++++++++++- netbox/ipam/search.py | 8 +++ netbox/ipam/tables/__init__.py | 1 + netbox/ipam/tables/asn.py | 69 +++++++++++++++++++++++++ netbox/ipam/tables/ip.py | 42 --------------- netbox/ipam/urls.py | 8 +++ netbox/ipam/views.py | 66 +++++++++++++++++++++-- netbox/netbox/navigation/menu.py | 1 + netbox/templates/ipam/asnrange.html | 52 +++++++++++++++++++ 22 files changed, 432 insertions(+), 74 deletions(-) create mode 100644 netbox/ipam/migrations/0064_asnrange.py create mode 100644 netbox/ipam/tables/asn.py create mode 100644 netbox/templates/ipam/asnrange.html diff --git a/netbox/ipam/api/nested_serializers.py b/netbox/ipam/api/nested_serializers.py index 7809e84f8..ca8843201 100644 --- a/netbox/ipam/api/nested_serializers.py +++ b/netbox/ipam/api/nested_serializers.py @@ -7,6 +7,7 @@ from netbox.api.serializers import WritableNestedSerializer __all__ = [ 'NestedAggregateSerializer', 'NestedASNSerializer', + 'NestedASNRangeSerializer', 'NestedFHRPGroupSerializer', 'NestedFHRPGroupAssignmentSerializer', 'NestedIPAddressSerializer', @@ -25,6 +26,18 @@ __all__ = [ ] +# +# ASN ranges +# + +class NestedASNRangeSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail') + + class Meta: + model = models.ASNRange + fields = ['id', 'url', 'display', 'name'] + + # # ASNs # diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index 6ec062aee..de06bfd68 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -15,12 +15,26 @@ from virtualization.api.nested_serializers import NestedVirtualMachineSerializer from .nested_serializers import * +# +# ASN ranges +# + +class ASNRangeSerializer(NetBoxModelSerializer): + url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail') + tenant = NestedTenantSerializer(required=False, allow_null=True) + asn_count = serializers.IntegerField(read_only=True) + + class Meta: + model = ASNRange + fields = [ + 'id', 'url', 'display', 'name', 'slug', 'start', 'end', 'tenant', 'description', 'tags', 'custom_fields', + 'created', 'last_updated', 'asn_count', + ] + + # # ASNs # -from .nested_serializers import NestedL2VPNSerializer -from ..models.l2vpn import L2VPNTermination, L2VPN - class ASNSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') diff --git a/netbox/ipam/api/urls.py b/netbox/ipam/api/urls.py index 1e077c087..5e6510d4b 100644 --- a/netbox/ipam/api/urls.py +++ b/netbox/ipam/api/urls.py @@ -7,44 +7,22 @@ from . import views router = NetBoxRouter() router.APIRootView = views.IPAMRootView -# ASNs router.register('asns', views.ASNViewSet) - -# VRFs +router.register('asn-ranges', views.ASNRangeViewSet) router.register('vrfs', views.VRFViewSet) - -# Route targets router.register('route-targets', views.RouteTargetViewSet) - -# RIRs router.register('rirs', views.RIRViewSet) - -# Aggregates router.register('aggregates', views.AggregateViewSet) - -# Prefixes router.register('roles', views.RoleViewSet) router.register('prefixes', views.PrefixViewSet) - -# IP ranges router.register('ip-ranges', views.IPRangeViewSet) - -# IP addresses router.register('ip-addresses', views.IPAddressViewSet) - -# FHRP groups router.register('fhrp-groups', views.FHRPGroupViewSet) router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet) - -# VLANs router.register('vlan-groups', views.VLANGroupViewSet) router.register('vlans', views.VLANViewSet) - -# Services router.register('service-templates', views.ServiceTemplateViewSet) router.register('services', views.ServiceViewSet) - -# L2VPN router.register('l2vpns', views.L2VPNViewSet) router.register('l2vpn-terminations', views.L2VPNTerminationViewSet) diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 9ea38758d..a50e61300 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -33,6 +33,14 @@ class IPAMRootView(APIRootView): # Viewsets # +class ASNRangeViewSet(NetBoxModelViewSet): + queryset = ASNRange.objects.prefetch_related('tenant').annotate( + asn_count=count_related(ASN, 'range') + ) + serializer_class = serializers.ASNRangeSerializer + filterset_class = filtersets.ASNRangeFilterSet + + class ASNViewSet(NetBoxModelViewSet): queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate( site_count=count_related(Site, 'asns'), diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 2e9f56bbc..82f9178b1 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -20,6 +20,7 @@ from .models import * __all__ = ( 'AggregateFilterSet', 'ASNFilterSet', + 'ASNRangeFilterSet', 'FHRPGroupAssignmentFilterSet', 'FHRPGroupFilterSet', 'IPAddressFilterSet', @@ -167,6 +168,19 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet): return queryset.none() +class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet): + + class Meta: + model = ASNRange + fields = ['id', 'start', 'end', 'description'] + + def search(self, queryset, name, value): + if not value.strip(): + return queryset + qs_filter = Q(description__icontains=value) + return queryset.filter(qs_filter) + + class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet): rir_id = django_filters.ModelMultipleChoiceFilter( queryset=RIR.objects.all(), diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index e63b34d75..a879d1236 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -16,6 +16,7 @@ from utilities.forms import ( __all__ = ( 'AggregateBulkEditForm', 'ASNBulkEditForm', + 'ASNRangeBulkEditForm', 'FHRPGroupBulkEditForm', 'IPAddressBulkEditForm', 'IPRangeBulkEditForm', @@ -97,6 +98,23 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm): nullable_fields = ('is_private', 'description') +class ASNRangeBulkEditForm(NetBoxModelBulkEditForm): + tenant = DynamicModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) + description = forms.CharField( + max_length=200, + required=False + ) + + model = ASNRange + fieldsets = ( + (None, ('tenant', 'description')), + ) + nullable_fields = ('date_added', 'description') + + class ASNBulkEditForm(NetBoxModelBulkEditForm): sites = DynamicModelMultipleChoiceField( queryset=Site.objects.all(), diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 972b98db2..46266d6d7 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -15,6 +15,7 @@ from virtualization.models import VirtualMachine, VMInterface __all__ = ( 'AggregateImportForm', 'ASNImportForm', + 'ASNRangeImportForm', 'FHRPGroupImportForm', 'IPAddressImportForm', 'IPRangeImportForm', @@ -87,6 +88,19 @@ class AggregateImportForm(NetBoxModelImportForm): fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', 'tags') +class ASNRangeImportForm(NetBoxModelImportForm): + tenant = CSVModelChoiceField( + queryset=Tenant.objects.all(), + required=False, + to_field_name='name', + help_text=_('Assigned tenant') + ) + + class Meta: + model = ASNRange + fields = ('name', 'slug', 'start', 'end', 'tenant', 'description', 'tags') + + class ASNImportForm(NetBoxModelImportForm): rir = CSVModelChoiceField( queryset=RIR.objects.all(), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 1d505a168..ce2f762a4 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -17,6 +17,7 @@ from virtualization.models import VirtualMachine __all__ = ( 'AggregateFilterForm', 'ASNFilterForm', + 'ASNRangeFilterForm', 'FHRPGroupFilterForm', 'IPAddressFilterForm', 'IPRangeFilterForm', @@ -114,6 +115,22 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): tag = TagFilterField(model) +class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): + model = ASNRange + fieldsets = ( + (None, ('q', 'filter_id', 'tag')), + ('Range', ('start', 'end')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) + start = forms.IntegerField( + required=False + ) + end = forms.IntegerField( + required=False + ) + tag = TagFilterField(model) + + class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = ASN fieldsets = ( diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 4e50c4949..44353f9fa 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -20,6 +20,7 @@ from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInter __all__ = ( 'AggregateForm', 'ASNForm', + 'ASNRangeForm', 'FHRPGroupForm', 'FHRPGroupAssignmentForm', 'IPAddressAssignForm', @@ -128,6 +129,20 @@ class AggregateForm(TenancyForm, NetBoxModelForm): } +class ASNRangeForm(TenancyForm, NetBoxModelForm): + slug = SlugField() + fieldsets = ( + ('ASN Range', ('name', 'slug', 'start', 'end', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + + class Meta: + model = ASNRange + fields = [ + 'name', 'slug', 'start', 'end', 'tenant_group', 'tenant', 'description', 'tags' + ] + + class ASNForm(TenancyForm, NetBoxModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), diff --git a/netbox/ipam/graphql/schema.py b/netbox/ipam/graphql/schema.py index 5cd5e030e..ce943567f 100644 --- a/netbox/ipam/graphql/schema.py +++ b/netbox/ipam/graphql/schema.py @@ -8,6 +8,9 @@ class IPAMQuery(graphene.ObjectType): asn = ObjectField(ASNType) asn_list = ObjectListField(ASNType) + asnrange = ObjectField(ASNRangeType) + asnrange_list = ObjectListField(ASNRangeType) + aggregate = ObjectField(AggregateType) aggregate_list = ObjectListField(AggregateType) diff --git a/netbox/ipam/graphql/types.py b/netbox/ipam/graphql/types.py index b8f6221bc..a3405126f 100644 --- a/netbox/ipam/graphql/types.py +++ b/netbox/ipam/graphql/types.py @@ -1,6 +1,5 @@ import graphene -from graphene_django import DjangoObjectType from extras.graphql.mixins import ContactsMixin from ipam import filtersets, models from netbox.graphql.scalars import BigInt @@ -8,6 +7,7 @@ from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBo __all__ = ( 'ASNType', + 'ASNRangeType', 'AggregateType', 'FHRPGroupType', 'FHRPGroupAssignmentType', @@ -36,6 +36,14 @@ class ASNType(NetBoxObjectType): filterset_class = filtersets.ASNFilterSet +class ASNRangeType(NetBoxObjectType): + + class Meta: + model = models.ASNRange + fields = '__all__' + filterset_class = filtersets.ASNRangeFilterSet + + class AggregateType(NetBoxObjectType): class Meta: diff --git a/netbox/ipam/migrations/0064_asnrange.py b/netbox/ipam/migrations/0064_asnrange.py new file mode 100644 index 000000000..c6ba63e64 --- /dev/null +++ b/netbox/ipam/migrations/0064_asnrange.py @@ -0,0 +1,45 @@ +# Generated by Django 4.1.7 on 2023-02-25 19:49 + +from django.db import migrations, models +import django.db.models.deletion +import ipam.fields +import taggit.managers +import utilities.json + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0009_standardize_description_comments'), + ('extras', '0087_dashboard'), + ('ipam', '0063_standardize_description_comments'), + ] + + operations = [ + migrations.CreateModel( + name='ASNRange', + 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)), + ('description', models.CharField(blank=True, max_length=200)), + ('name', models.CharField(max_length=100, unique=True)), + ('slug', models.SlugField(max_length=100, unique=True)), + ('start', ipam.fields.ASNField()), + ('end', ipam.fields.ASNField()), + ('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')), + ('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='asn_ranges', to='tenancy.tenant')), + ], + options={ + 'verbose_name': 'ASN range', + 'verbose_name_plural': 'ASN ranges', + 'ordering': ('name',), + }, + ), + migrations.AddField( + model_name='asn', + name='range', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.asnrange'), + ), + ] diff --git a/netbox/ipam/models/__init__.py b/netbox/ipam/models/__init__.py index 1d890dee4..a00919ee0 100644 --- a/netbox/ipam/models/__init__.py +++ b/netbox/ipam/models/__init__.py @@ -9,6 +9,7 @@ from .vlans import * __all__ = ( 'ASN', + 'ASNRange', 'Aggregate', 'IPAddress', 'IPRange', diff --git a/netbox/ipam/models/asns.py b/netbox/ipam/models/asns.py index c4b7c022f..9f7433a43 100644 --- a/netbox/ipam/models/asns.py +++ b/netbox/ipam/models/asns.py @@ -1,13 +1,14 @@ -from django.contrib.postgres.fields import ArrayField +from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse from django.utils.translation import gettext as _ from ipam.fields import ASNField -from netbox.models import PrimaryModel +from netbox.models import OrganizationalModel, PrimaryModel __all__ = ( 'ASN', + 'ASNRange', ) @@ -16,6 +17,12 @@ class ASN(PrimaryModel): An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have one or more ASNs assigned to it. """ + range = models.ForeignKey( + to='ipam.ASNRange', + on_delete=models.PROTECT, + blank=True, + null=True + ) asn = ASNField( unique=True, verbose_name='ASN', @@ -68,3 +75,49 @@ class ASN(PrimaryModel): return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})' else: return self.asn + + def clean(self): + if self.range and self.asn not in self.range.range: + raise ValidationError(f"ASN {self.asn} is outside of assigned range ({self.range})") + + +class ASNRange(OrganizationalModel): + name = models.CharField( + max_length=100, + unique=True + ) + slug = models.SlugField( + max_length=100, + unique=True + ) + start = ASNField() + end = ASNField() + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='asn_ranges', + blank=True, + null=True + ) + + class Meta: + ordering = ('name',) + verbose_name = 'ASN range' + verbose_name_plural = 'ASN ranges' + + def __str__(self): + return f'ASNs {self.range_as_string()}' + + def get_absolute_url(self): + return reverse('ipam:asnrange', args=[self.pk]) + + @property + def range(self): + return range(self.start, self.end + 1) + + def range_as_string(self): + return f'{self.start}-{self.end}' + + def clean(self): + if self.end <= self.start: + raise ValidationError(f"Starting ASN ({self.start}) must be lower than ending ASN ({self.end}).") diff --git a/netbox/ipam/search.py b/netbox/ipam/search.py index ad4403321..4d97bf5f0 100644 --- a/netbox/ipam/search.py +++ b/netbox/ipam/search.py @@ -22,6 +22,14 @@ class ASNIndex(SearchIndex): ) +@register_search +class ASNRangeIndex(SearchIndex): + model = models.ASNRange + fields = ( + ('description', 500), + ) + + @register_search class FHRPGroupIndex(SearchIndex): model = models.FHRPGroup diff --git a/netbox/ipam/tables/__init__.py b/netbox/ipam/tables/__init__.py index 3bde78af0..7d04a5fea 100644 --- a/netbox/ipam/tables/__init__.py +++ b/netbox/ipam/tables/__init__.py @@ -1,3 +1,4 @@ +from .asn import * from .fhrp import * from .ip import * from .l2vpn import * diff --git a/netbox/ipam/tables/asn.py b/netbox/ipam/tables/asn.py new file mode 100644 index 000000000..c42add017 --- /dev/null +++ b/netbox/ipam/tables/asn.py @@ -0,0 +1,69 @@ +import django_tables2 as tables +from django.utils.translation import gettext as _ + +from ipam.models import * +from netbox.tables import NetBoxTable, columns +from tenancy.tables import TenancyColumnsMixin + +__all__ = ( + 'ASNTable', + 'ASNRangeTable', +) + + +class ASNRangeTable(TenancyColumnsMixin, NetBoxTable): + name = tables.Column( + linkify=True + ) + tags = columns.TagColumn( + url_name='ipam:asnrange_list' + ) + asn_count = columns.LinkedCountColumn( + viewname='ipam:asn_list', + url_params={'asn_id': 'pk'}, + verbose_name=_('ASN Count') + ) + + class Meta(NetBoxTable.Meta): + model = ASNRange + fields = ( + 'pk', 'name', 'slug', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description', 'tags', + 'created', 'last_updated', 'actions', + ) + default_columns = ('pk', 'name', 'start', 'end', 'tenant', 'asn_count', 'description') + + +class ASNTable(TenancyColumnsMixin, NetBoxTable): + asn = tables.Column( + linkify=True + ) + asn_asdot = tables.Column( + accessor=tables.A('asn_asdot'), + linkify=True, + verbose_name=_('ASDOT') + ) + site_count = columns.LinkedCountColumn( + viewname='dcim:site_list', + url_params={'asn_id': 'pk'}, + verbose_name=_('Site Count') + ) + provider_count = columns.LinkedCountColumn( + viewname='circuits:provider_list', + url_params={'asn_id': 'pk'}, + verbose_name=_('Provider Count') + ) + sites = columns.ManyToManyColumn( + linkify_item=True + ) + comments = columns.MarkdownColumn() + tags = columns.TagColumn( + url_name='ipam:asn_list' + ) + + class Meta(NetBoxTable.Meta): + model = ASN + fields = ( + 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', + 'comments', 'sites', 'tags', 'created', 'last_updated', 'actions', + ) + default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant') diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index f83831d2d..37aff148d 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -8,7 +8,6 @@ from tenancy.tables import TenancyColumnsMixin, TenantColumn __all__ = ( 'AggregateTable', - 'ASNTable', 'AssignedIPAddressesTable', 'IPAddressAssignTable', 'IPAddressTable', @@ -93,47 +92,6 @@ class RIRTable(NetBoxTable): default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description') -# -# ASNs -# - -class ASNTable(TenancyColumnsMixin, NetBoxTable): - asn = tables.Column( - linkify=True - ) - asn_asdot = tables.Column( - accessor=tables.A('asn_asdot'), - linkify=True, - verbose_name='ASDOT' - ) - site_count = columns.LinkedCountColumn( - viewname='dcim:site_list', - url_params={'asn_id': 'pk'}, - verbose_name='Site Count' - ) - provider_count = columns.LinkedCountColumn( - viewname='circuits:provider_list', - url_params={'asn_id': 'pk'}, - verbose_name='Provider Count' - ) - sites = columns.ManyToManyColumn( - linkify_item=True, - verbose_name='Sites' - ) - comments = columns.MarkdownColumn() - tags = columns.TagColumn( - url_name='ipam:asn_list' - ) - - class Meta(NetBoxTable.Meta): - model = ASN - fields = ( - 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description', - 'comments', 'sites', 'tags', 'created', 'last_updated', 'actions', - ) - default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant') - - # # Aggregates # diff --git a/netbox/ipam/urls.py b/netbox/ipam/urls.py index 032ddf498..3bfe34b7b 100644 --- a/netbox/ipam/urls.py +++ b/netbox/ipam/urls.py @@ -6,6 +6,14 @@ from . import views app_name = 'ipam' urlpatterns = [ + # ASN ranges + path('asn-ranges/', views.ASNRangeListView.as_view(), name='asnrange_list'), + path('asn-ranges/add/', views.ASNRangeEditView.as_view(), name='asnrange_add'), + path('asn-ranges/import/', views.ASNRangeBulkImportView.as_view(), name='asnrange_import'), + path('asn-ranges/edit/', views.ASNRangeBulkEditView.as_view(), name='asnrange_bulk_edit'), + path('asn-ranges/delete/', views.ASNRangeBulkDeleteView.as_view(), name='asnrange_bulk_delete'), + path('asn-ranges//', include(get_model_urls('ipam', 'asnrange'))), + # ASNs path('asns/', views.ASNListView.as_view(), name='asn_list'), path('asns/add/', views.ASNEditView.as_view(), name='asn_add'), diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index c80ca7d74..07d5598f8 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -7,16 +7,15 @@ from django.utils.translation import gettext as _ from circuits.models import Provider from dcim.filtersets import InterfaceFilterSet -from dcim.models import Interface, Site, Device +from dcim.models import Interface, Site from netbox.views import generic from utilities.utils import count_related from utilities.views import ViewTab, register_model_view from virtualization.filtersets import VMInterfaceFilterSet -from virtualization.models import VMInterface, VirtualMachine +from virtualization.models import VMInterface from . import filtersets, forms, tables from .constants import * from .models import * -from .models import ASN from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans @@ -195,6 +194,67 @@ class RIRBulkDeleteView(generic.BulkDeleteView): table = tables.RIRTable +# +# ASN ranges +# + +class ASNRangeListView(generic.ObjectListView): + queryset = ASNRange.objects.annotate( + asn_count=count_related(ASN, 'range'), + ) + filterset = filtersets.ASNRangeFilterSet + filterset_form = forms.ASNRangeFilterForm + table = tables.ASNRangeTable + + +@register_model_view(ASNRange) +class ASNRangeView(generic.ObjectView): + queryset = ASNRange.objects.all() + + def get_extra_context(self, request, instance): + related_models = ( + (ASN.objects.restrict(request.user, 'view').filter(range=instance), 'asn_id'), + ) + + return { + 'related_models': related_models, + } + + +@register_model_view(ASNRange, 'edit') +class ASNRangeEditView(generic.ObjectEditView): + queryset = ASNRange.objects.all() + form = forms.ASNRangeForm + + +@register_model_view(ASNRange, 'delete') +class ASNRangeDeleteView(generic.ObjectDeleteView): + queryset = ASNRange.objects.all() + + +class ASNRangeBulkImportView(generic.BulkImportView): + queryset = ASNRange.objects.all() + model_form = forms.ASNRangeImportForm + table = tables.ASNRangeTable + + +class ASNRangeBulkEditView(generic.BulkEditView): + queryset = ASNRange.objects.annotate( + site_count=count_related(Site, 'asns') + ) + filterset = filtersets.ASNRangeFilterSet + table = tables.ASNRangeTable + form = forms.ASNRangeBulkEditForm + + +class ASNRangeBulkDeleteView(generic.BulkDeleteView): + queryset = ASNRange.objects.annotate( + site_count=count_related(Site, 'asns') + ) + filterset = filtersets.ASNRangeFilterSet + table = tables.ASNRangeTable + + # # ASNs # diff --git a/netbox/netbox/navigation/menu.py b/netbox/netbox/navigation/menu.py index 03c361002..35be8cf55 100644 --- a/netbox/netbox/navigation/menu.py +++ b/netbox/netbox/navigation/menu.py @@ -158,6 +158,7 @@ IPAM_MENU = Menu( MenuGroup( label=_('ASNs'), items=( + get_model_item('ipam', 'asnrange', _('ASN Ranges')), get_model_item('ipam', 'asn', _('ASNs')), ), ), diff --git a/netbox/templates/ipam/asnrange.html b/netbox/templates/ipam/asnrange.html new file mode 100644 index 000000000..07f826d9f --- /dev/null +++ b/netbox/templates/ipam/asnrange.html @@ -0,0 +1,52 @@ +{% extends 'generic/object.html' %} +{% load buttons %} +{% load helpers %} +{% load plugins %} +{% load render_table from django_tables2 %} + +{% block content %} +
+
+
+
ASN Range
+
+ + + + + + + + + + + + + + + + + +
Name{{ object.name }}
Range{{ object.range_as_string }}
Tenant + {% if object.tenant.group %} + {{ object.tenant.group|linkify }} / + {% endif %} + {{ object.tenant|linkify|placeholder }} +
Description{{ object.description|placeholder }}
+
+
+ {% plugin_left_page object %} + {% include 'inc/panels/tags.html' %} +
+
+ {% include 'inc/panels/related_objects.html' %} + {% include 'inc/panels/custom_fields.html' %} + {% plugin_right_page object %} +
+
+
+
+ {% plugin_full_width_page object %} +
+
+{% endblock content %}