From bddc35bbc75e78e9fb0cf9c77c9465a2e519d30e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 30 Mar 2022 17:17:36 -0400 Subject: [PATCH] Closes #8496: Enable assigning multiple ASNs to a provider --- docs/release-notes/version-3.2.md | 7 +++- netbox/circuits/api/serializers.py | 14 ++++++- netbox/circuits/api/views.py | 2 +- netbox/circuits/filtersets.py | 6 +++ netbox/circuits/forms/bulk_edit.py | 18 +++++++-- netbox/circuits/forms/filtersets.py | 8 +++- netbox/circuits/forms/models.py | 12 ++++-- .../circuits/migrations/0035_provider_asns.py | 19 +++++++++ netbox/circuits/models/providers.py | 5 +++ netbox/circuits/tables/providers.py | 14 ++++++- netbox/circuits/tests/test_api.py | 39 ++++++++++++------- netbox/circuits/tests/test_filtersets.py | 19 ++++++++- netbox/circuits/tests/test_views.py | 16 +++++++- netbox/dcim/api/serializers.py | 8 ++-- netbox/dcim/forms/models.py | 1 - netbox/dcim/tables/sites.py | 8 ++-- netbox/ipam/api/serializers.py | 5 ++- netbox/ipam/api/views.py | 6 ++- netbox/ipam/tables/ip.py | 11 ++++-- netbox/ipam/views.py | 13 ++++++- netbox/templates/circuits/provider.html | 25 +++++++++--- netbox/templates/ipam/asn.html | 19 ++++++++- 22 files changed, 222 insertions(+), 53 deletions(-) create mode 100644 netbox/circuits/migrations/0035_provider_asns.py diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index c120ce931..d420b8108 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -142,6 +142,7 @@ Where it is desired to limit the range of available VLANs within a group, users * [#8296](https://github.com/netbox-community/netbox/issues/8296) - Allow disabling custom links * [#8307](https://github.com/netbox-community/netbox/issues/8307) - Add `data_type` indicator to REST API serializer for custom fields * [#8463](https://github.com/netbox-community/netbox/issues/8463) - Change the `created` field on all change-logged models from date to datetime +* [#8496](https://github.com/netbox-community/netbox/issues/8496) - Enable assigning multiple ASNs to a provider * [#8572](https://github.com/netbox-community/netbox/issues/8572) - Add a `pre_run()` method for reports * [#8593](https://github.com/netbox-community/netbox/issues/8593) - Add a `link` field for contacts * [#8649](https://github.com/netbox-community/netbox/issues/8649) - Enable customization of configuration module using `NETBOX_CONFIGURATION` environment variable @@ -176,6 +177,8 @@ Where it is desired to limit the range of available VLANs within a group, users * `/api/dcim/module-types/` * `/api/ipam/service-templates/` * `/api/ipam/vlan-groups//available-vlans/` +* circuits.Provider + * Added `asns` field * circuits.ProviderNetwork * Added `service_id` field * dcim.ConsolePort @@ -203,10 +206,12 @@ Where it is desired to limit the range of available VLANs within a group, users * Added `data_type` and `object_type` fields * extras.CustomLink * Added `enabled` field +* ipam.ASN + * Added `provider_count` field * ipam.VLANGroup * Added the `/availables-vlans/` endpoint * Added the `min_vid` and `max_vid` fields * tenancy.Contact - * Added the `link` field + * Added `link` field * virtualization.VMInterface * Added `vrf` field diff --git a/netbox/circuits/api/serializers.py b/netbox/circuits/api/serializers.py index 539ff9466..19570f067 100644 --- a/netbox/circuits/api/serializers.py +++ b/netbox/circuits/api/serializers.py @@ -4,7 +4,9 @@ from circuits.choices import CircuitStatusChoices from circuits.models import * from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer from dcim.api.serializers import LinkTerminationSerializer -from netbox.api import ChoiceField +from ipam.models import ASN +from ipam.api.nested_serializers import NestedASNSerializer +from netbox.api import ChoiceField, SerializedPKRelatedField from netbox.api.serializers import NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer from tenancy.api.nested_serializers import NestedTenantSerializer from .nested_serializers import * @@ -16,13 +18,21 @@ from .nested_serializers import * class ProviderSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail') + asns = SerializedPKRelatedField( + queryset=ASN.objects.all(), + serializer=NestedASNSerializer, + required=False, + many=True + ) + + # Related object counts circuit_count = serializers.IntegerField(read_only=True) class Meta: model = Provider fields = [ 'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', - 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', + 'comments', 'asns', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', ] diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 99687fe9d..3573c05e3 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -21,7 +21,7 @@ class CircuitsRootView(APIRootView): # class ProviderViewSet(NetBoxModelViewSet): - queryset = Provider.objects.prefetch_related('tags').annotate( + queryset = Provider.objects.prefetch_related('asns', 'tags').annotate( circuit_count=count_related(Circuit, 'provider') ) serializer_class = serializers.ProviderSerializer diff --git a/netbox/circuits/filtersets.py b/netbox/circuits/filtersets.py index 9bf2bb439..b7fa100a8 100644 --- a/netbox/circuits/filtersets.py +++ b/netbox/circuits/filtersets.py @@ -3,6 +3,7 @@ from django.db.models import Q from dcim.filtersets import CableTerminationFilterSet from dcim.models import Region, Site, SiteGroup +from ipam.models import ASN from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet from utilities.filters import TreeNodeMultipleChoiceFilter @@ -56,6 +57,11 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet): to_field_name='slug', label='Site (slug)', ) + asn_id = django_filters.ModelMultipleChoiceFilter( + field_name='asns', + queryset=ASN.objects.all(), + label='ASN (ID)', + ) class Meta: model = Provider diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 8fc76e940..6e283219a 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -1,10 +1,15 @@ from django import forms +from django.utils.translation import gettext as _ from circuits.choices import CircuitStatusChoices from circuits.models import * +from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant -from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect +from utilities.forms import ( + add_blank_choice, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SmallTextarea, + StaticSelect, +) __all__ = ( 'CircuitBulkEditForm', @@ -17,7 +22,12 @@ __all__ = ( class ProviderBulkEditForm(NetBoxModelBulkEditForm): asn = forms.IntegerField( required=False, - label='ASN' + label='ASN (legacy)' + ) + asns = DynamicModelMultipleChoiceField( + queryset=ASN.objects.all(), + label=_('ASNs'), + required=False ) account = forms.CharField( max_length=30, @@ -45,10 +55,10 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): model = Provider fieldsets = ( - (None, ('asn', 'account', 'portal_url', 'noc_contact', 'admin_contact')), + (None, ('asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact')), ) nullable_fields = ( - 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + 'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', ) diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 4f0d99895..ca3b003b9 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -4,6 +4,7 @@ from django.utils.translation import gettext as _ from circuits.choices import CircuitStatusChoices from circuits.models import * from dcim.models import Region, Site, SiteGroup +from ipam.models import ASN from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField @@ -45,7 +46,12 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm): ) asn = forms.IntegerField( required=False, - label=_('ASN') + label=_('ASN (legacy)') + ) + asn_id = DynamicModelMultipleChoiceField( + queryset=ASN.objects.all(), + required=False, + label=_('ASNs') ) tag = TagFilterField(model) diff --git a/netbox/circuits/forms/models.py b/netbox/circuits/forms/models.py index c7d7f8438..8fd5fb92d 100644 --- a/netbox/circuits/forms/models.py +++ b/netbox/circuits/forms/models.py @@ -1,8 +1,9 @@ from django import forms +from django.utils.translation import gettext as _ from circuits.models import * from dcim.models import Region, Site, SiteGroup -from extras.models import Tag +from ipam.models import ASN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import ( @@ -21,17 +22,22 @@ __all__ = ( class ProviderForm(NetBoxModelForm): slug = SlugField() + asns = DynamicModelMultipleChoiceField( + queryset=ASN.objects.all(), + label=_('ASNs'), + required=False + ) comments = CommentField() fieldsets = ( - ('Provider', ('name', 'slug', 'asn', 'tags')), + ('Provider', ('name', 'slug', 'asn', 'asns', 'tags')), ('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')), ) class Meta: model = Provider fields = [ - 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', + 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asns', 'comments', 'tags', ] widgets = { 'noc_contact': SmallTextarea( diff --git a/netbox/circuits/migrations/0035_provider_asns.py b/netbox/circuits/migrations/0035_provider_asns.py new file mode 100644 index 000000000..afb0da4d6 --- /dev/null +++ b/netbox/circuits/migrations/0035_provider_asns.py @@ -0,0 +1,19 @@ +# Generated by Django 4.0.3 on 2022-03-30 20:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0057_created_datetimefield'), + ('circuits', '0034_created_datetimefield'), + ] + + operations = [ + migrations.AddField( + model_name='provider', + name='asns', + field=models.ManyToManyField(blank=True, related_name='providers', to='ipam.asn'), + ), + ] diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index 9cf4bf5c1..4211a54a6 100644 --- a/netbox/circuits/models/providers.py +++ b/netbox/circuits/models/providers.py @@ -30,6 +30,11 @@ class Provider(NetBoxModel): verbose_name='ASN', help_text='32-bit autonomous system number' ) + asns = models.ManyToManyField( + to='ipam.ASN', + related_name='providers', + blank=True + ) account = models.CharField( max_length=30, blank=True, diff --git a/netbox/circuits/tables/providers.py b/netbox/circuits/tables/providers.py index d5b4329fb..c50610398 100644 --- a/netbox/circuits/tables/providers.py +++ b/netbox/circuits/tables/providers.py @@ -14,6 +14,16 @@ class ProviderTable(NetBoxTable): name = tables.Column( linkify=True ) + asns = tables.ManyToManyColumn( + linkify_item=True, + verbose_name='ASNs' + ) + asn_count = columns.LinkedCountColumn( + accessor=tables.A('asns__count'), + viewname='ipam:asn_list', + url_params={'provider_id': 'pk'}, + verbose_name='ASN Count' + ) circuit_count = tables.Column( accessor=Accessor('count_circuits'), verbose_name='Circuits' @@ -29,8 +39,8 @@ class ProviderTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = Provider fields = ( - 'pk', 'id', 'name', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'circuit_count', - 'comments', 'contacts', 'tags', 'created', 'last_updated', + 'pk', 'id', 'name', 'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asn_count', + 'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 830c7d9ca..02b489ac4 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -3,6 +3,7 @@ from django.urls import reverse from circuits.choices import * from circuits.models import * from dcim.models import Site +from ipam.models import ASN, RIR from utilities.testing import APITestCase, APIViewTestCases @@ -18,20 +19,6 @@ class AppTest(APITestCase): class ProviderTest(APIViewTestCases.APIViewTestCase): model = Provider brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url'] - create_data = [ - { - 'name': 'Provider 4', - 'slug': 'provider-4', - }, - { - 'name': 'Provider 5', - 'slug': 'provider-5', - }, - { - 'name': 'Provider 6', - 'slug': 'provider-6', - }, - ] bulk_update_data = { 'asn': 1234, } @@ -39,6 +26,12 @@ class ProviderTest(APIViewTestCases.APIViewTestCase): @classmethod def setUpTestData(cls): + rir = RIR.objects.create(name='RFC 6996', is_private=True) + asns = [ + ASN(asn=65000 + i, rir=rir) for i in range(8) + ] + ASN.objects.bulk_create(asns) + providers = ( Provider(name='Provider 1', slug='provider-1'), Provider(name='Provider 2', slug='provider-2'), @@ -46,6 +39,24 @@ class ProviderTest(APIViewTestCases.APIViewTestCase): ) Provider.objects.bulk_create(providers) + cls.create_data = [ + { + 'name': 'Provider 4', + 'slug': 'provider-4', + 'asns': [asns[0].pk, asns[1].pk], + }, + { + 'name': 'Provider 5', + 'slug': 'provider-5', + 'asns': [asns[2].pk, asns[3].pk], + }, + { + 'name': 'Provider 6', + 'slug': 'provider-6', + 'asns': [asns[4].pk, asns[5].pk], + }, + ] + class CircuitTypeTest(APIViewTestCases.APIViewTestCase): model = CircuitType diff --git a/netbox/circuits/tests/test_filtersets.py b/netbox/circuits/tests/test_filtersets.py index 20416c4e6..205236712 100644 --- a/netbox/circuits/tests/test_filtersets.py +++ b/netbox/circuits/tests/test_filtersets.py @@ -4,6 +4,7 @@ from circuits.choices import * from circuits.filtersets import * from circuits.models import * from dcim.models import Cable, Region, Site, SiteGroup +from ipam.models import ASN, RIR from tenancy.models import Tenant, TenantGroup from utilities.testing import ChangeLoggedFilterSetTests @@ -15,6 +16,14 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): @classmethod def setUpTestData(cls): + rir = RIR.objects.create(name='RFC 6996', is_private=True) + asns = ( + ASN(asn=64512, rir=rir), + ASN(asn=64513, rir=rir), + ASN(asn=64514, rir=rir), + ) + ASN.objects.bulk_create(asns) + providers = ( Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'), Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'), @@ -23,6 +32,9 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): Provider(name='Provider 5', slug='provider-5', asn=65005, account='5678'), ) Provider.objects.bulk_create(providers) + providers[0].asns.set([asns[0]]) + providers[1].asns.set([asns[1]]) + providers[2].asns.set([asns[2]]) regions = ( Region(name='Test Region 1', slug='test-region-1'), @@ -70,10 +82,15 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'slug': ['provider-1', 'provider-2']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) - def test_asn(self): + def test_asn(self): # Legacy field params = {'asn': ['65001', '65002']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_asn_id(self): # ASN object assignment + asns = ASN.objects.all()[:2] + params = {'asn_id': [asns[0].pk, asns[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_account(self): params = {'account': ['1234', '2345']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) diff --git a/netbox/circuits/tests/test_views.py b/netbox/circuits/tests/test_views.py index d8ad27d72..17c846c86 100644 --- a/netbox/circuits/tests/test_views.py +++ b/netbox/circuits/tests/test_views.py @@ -6,6 +6,7 @@ from django.urls import reverse from circuits.choices import * from circuits.models import * from dcim.models import Cable, Interface, Site +from ipam.models import ASN, RIR from utilities.testing import ViewTestCases, create_tags, create_test_device @@ -15,11 +16,21 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): @classmethod def setUpTestData(cls): - Provider.objects.bulk_create([ + rir = RIR.objects.create(name='RFC 6996', is_private=True) + asns = [ + ASN(asn=65000 + i, rir=rir) for i in range(8) + ] + ASN.objects.bulk_create(asns) + + providers = ( Provider(name='Provider 1', slug='provider-1', asn=65001), Provider(name='Provider 2', slug='provider-2', asn=65002), Provider(name='Provider 3', slug='provider-3', asn=65003), - ]) + ) + Provider.objects.bulk_create(providers) + providers[0].asns.set([asns[0], asns[1]]) + providers[1].asns.set([asns[2], asns[3]]) + providers[2].asns.set([asns[4], asns[5]]) tags = create_tags('Alpha', 'Bravo', 'Charlie') @@ -27,6 +38,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'name': 'Provider X', 'slug': 'provider-x', 'asn': 65123, + 'asns': [asns[6].pk, asns[7].pk], 'account': '1234', 'portal_url': 'http://example.com/portal', 'noc_contact': 'noc@example.com', diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index d6f8b347c..813c946a3 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -134,10 +134,10 @@ class SiteSerializer(NetBoxModelSerializer): class Meta: model = Site fields = [ - 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asns', - 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', - 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', - 'rack_count', 'virtualmachine_count', 'vlan_count', + 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', + 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags', + 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', + 'virtualmachine_count', 'vlan_count', ] diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 6054ee7bf..57e2fa820 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -7,7 +7,6 @@ from timezone_field import TimeZoneFormField from dcim.choices import * from dcim.constants import * from dcim.models import * -from extras.models import Tag from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm diff --git a/netbox/dcim/tables/sites.py b/netbox/dcim/tables/sites.py index d4d355474..84522480f 100644 --- a/netbox/dcim/tables/sites.py +++ b/netbox/dcim/tables/sites.py @@ -86,16 +86,16 @@ class SiteTable(NetBoxTable): group = tables.Column( linkify=True ) + asns = tables.ManyToManyColumn( + linkify_item=True, + verbose_name='ASNs' + ) asn_count = columns.LinkedCountColumn( accessor=tables.A('asns__count'), viewname='ipam:asn_list', url_params={'site_id': 'pk'}, verbose_name='ASN Count' ) - asns = tables.ManyToManyColumn( - linkify_item=True, - verbose_name='ASNs' - ) tenant = TenantColumn() comments = columns.MarkdownColumn() contacts = tables.ManyToManyColumn( diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index c26575f1f..3fa1bcc7e 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -24,12 +24,13 @@ class ASNSerializer(NetBoxModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') tenant = NestedTenantSerializer(required=False, allow_null=True) site_count = serializers.IntegerField(read_only=True) + provider_count = serializers.IntegerField(read_only=True) class Meta: model = ASN fields = [ - 'id', 'url', 'display', 'asn', 'site_count', 'rir', 'tenant', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', + 'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'site_count', 'provider_count', 'tags', + 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index c09cffa05..dcddec580 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -8,6 +8,7 @@ from rest_framework.response import Response from rest_framework.routers import APIRootView from rest_framework.views import APIView +from circuits.models import Provider from dcim.models import Site from ipam import filtersets from ipam.models import * @@ -32,7 +33,10 @@ class IPAMRootView(APIRootView): # class ASNViewSet(NetBoxModelViewSet): - queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(site_count=count_related(Site, 'asns')) + queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate( + site_count=count_related(Site, 'asns'), + provider_count=count_related(Provider, 'asns') + ) serializer_class = serializers.ASNSerializer filterset_class = filtersets.ASNFilterSet diff --git a/netbox/ipam/tables/ip.py b/netbox/ipam/tables/ip.py index ac25b113b..244bcee8e 100644 --- a/netbox/ipam/tables/ip.py +++ b/netbox/ipam/tables/ip.py @@ -113,6 +113,11 @@ class ASNTable(NetBoxTable): 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 = tables.ManyToManyColumn( linkify_item=True, verbose_name='Sites' @@ -125,10 +130,10 @@ class ASNTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = ASN fields = ( - 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'tenant', 'description', 'sites', 'tags', 'created', - 'last_updated', 'actions', + 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'description', 'sites', 'tags', + 'created', 'last_updated', 'actions', ) - default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'description', 'tenant') + default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant') # diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 9faa35ac6..336a6ea72 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -4,6 +4,8 @@ from django.db.models.expressions import RawSQL from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from circuits.models import Provider +from circuits.tables import ProviderTable from dcim.filtersets import InterfaceFilterSet from dcim.models import Interface, Site from dcim.tables import SiteTable @@ -206,6 +208,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView): class ASNListView(generic.ObjectListView): queryset = ASN.objects.annotate( site_count=count_related(Site, 'asns'), + provider_count=count_related(Provider, 'asns') ) filterset = filtersets.ASNFilterSet filterset_form = forms.ASNFilterForm @@ -216,13 +219,21 @@ class ASNView(generic.ObjectView): queryset = ASN.objects.all() def get_extra_context(self, request, instance): + # Gather assigned Sites sites = instance.sites.restrict(request.user, 'view') sites_table = SiteTable(sites) sites_table.configure(request) + # Gather assigned Providers + providers = instance.providers.restrict(request.user, 'view') + providers_table = ProviderTable(providers) + providers_table.configure(request) + return { 'sites_table': sites_table, - 'sites_count': sites.count() + 'sites_count': sites.count(), + 'providers_table': providers_table, + 'providers_count': providers.count(), } diff --git a/netbox/templates/circuits/provider.html b/netbox/templates/circuits/provider.html index 3fd275c7c..1bf63f2d5 100644 --- a/netbox/templates/circuits/provider.html +++ b/netbox/templates/circuits/provider.html @@ -16,14 +16,29 @@
-
- Provider -
+
Provider
- - + + + + + + diff --git a/netbox/templates/ipam/asn.html b/netbox/templates/ipam/asn.html index 6c092f598..7afe981e6 100644 --- a/netbox/templates/ipam/asn.html +++ b/netbox/templates/ipam/asn.html @@ -45,7 +45,17 @@ {% if sites_count %} {{ sites_count }} {% else %} - {{ sites_count }} + {{ ''|placeholder }} + {% endif %} + + + + + @@ -69,6 +79,13 @@ {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %} +
+
Providers
+
+ {% render_table providers_table 'inc/table.html' %} + {% include 'inc/paginator.html' with paginator=providers_table.paginator page=providers_table.page %} +
+
{% plugin_full_width_page object %}
ASN{{ object.asn|placeholder }}ASN + {% if object.asn %} +
+ +
+ {% endif %} + {{ object.asn|placeholder }} +
ASNs + {% for asn in object.asns.all %} + {{ asn|linkify }}{% if not forloop.last %}, {% endif %} + {% empty %} + {{ ''|placeholder }} + {% endfor %} +
Account
Providers + {% if providers_count %} + {{ providers_count }} + {% else %} + {{ ''|placeholder }} {% endif %}