Просмотр исходного кода

Closes #8496: Enable assigning multiple ASNs to a provider

jeremystretch 3 лет назад
Родитель
Сommit
bddc35bbc7

+ 6 - 1
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/<id>/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

+ 12 - 2
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',
         ]
 
 

+ 1 - 1
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

+ 6 - 0
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

+ 14 - 4
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',
     )
 
 

+ 7 - 1
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)
 

+ 9 - 3
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(

+ 19 - 0
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'),
+        ),
+    ]

+ 5 - 0
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,

+ 12 - 2
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')
 

+ 25 - 14
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

+ 18 - 1
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)

+ 14 - 2
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',

+ 4 - 4
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',
         ]
 
 

+ 0 - 1
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

+ 4 - 4
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(

+ 3 - 2
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',
         ]
 
 

+ 5 - 1
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
 

+ 8 - 3
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')
 
 
 #

+ 12 - 1
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(),
         }
 
 

+ 20 - 5
netbox/templates/circuits/provider.html

@@ -16,14 +16,29 @@
 <div class="row mb-3">
 	  <div class="col col-md-6">
         <div class="card">
-            <h5 class="card-header">
-                Provider
-            </h5>
+            <h5 class="card-header">Provider</h5>
             <div class="card-body">
                 <table class="table table-hover attr-table">
                     <tr>
-                        <th scope="row">ASN</th>
-                        <td>{{ object.asn|placeholder }}</td>
+                      <th scope="row">ASN</th>
+                      <td>
+                        {% if object.asn %}
+                          <div class="float-end text-warning">
+                            <i class="mdi mdi-alert" title="This field will be removed in a future release. Please migrate this data to ASN objects."></i>
+                          </div>
+                        {% endif %}
+                        {{ object.asn|placeholder }}
+                      </td>
+                    </tr>
+                    <tr>
+                      <th scope="row">ASNs</th>
+                      <td>
+                        {% for asn in object.asns.all %}
+                          {{ asn|linkify }}{% if not forloop.last %}, {% endif %}
+                        {% empty %}
+                          {{ ''|placeholder }}
+                        {% endfor %}
+                      </td>
                     </tr>
                     <tr>
                         <th scope="row">Account</th>

+ 18 - 1
netbox/templates/ipam/asn.html

@@ -45,7 +45,17 @@
                 {% if sites_count %}
                   <a href="{% url 'dcim:site_list' %}?asn_id={{ object.pk }}">{{ sites_count }}</a>
                 {% else %}
-                  {{ sites_count }}
+                  {{ ''|placeholder }}
+                {% endif %}
+              </td>
+            </tr>
+            <tr>
+              <td>Providers</td>
+              <td>
+                {% if providers_count %}
+                  <a href="{% url 'circuits:provider_list' %}?asn_id={{ object.pk }}">{{ providers_count }}</a>
+                {% else %}
+                  {{ ''|placeholder }}
                 {% endif %}
               </td>
             </tr>
@@ -69,6 +79,13 @@
           {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
         </div>
       </div>
+      <div class="card">
+        <h5 class="card-header">Providers</h5>
+        <div class="card-body table-responsive">
+          {% render_table providers_table 'inc/table.html' %}
+          {% include 'inc/paginator.html' with paginator=providers_table.paginator page=providers_table.page %}
+        </div>
+      </div>
       {% plugin_full_width_page object %}
     </div>
   </div>