Explorar o código

Closes #8496: Enable assigning multiple ASNs to a provider

jeremystretch %!s(int64=3) %!d(string=hai) anos
pai
achega
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
 * [#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
 * [#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
 * [#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
 * [#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
 * [#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
 * [#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/dcim/module-types/`
     * `/api/ipam/service-templates/`
     * `/api/ipam/service-templates/`
     * `/api/ipam/vlan-groups/<id>/available-vlans/`
     * `/api/ipam/vlan-groups/<id>/available-vlans/`
+* circuits.Provider
+    * Added `asns` field
 * circuits.ProviderNetwork
 * circuits.ProviderNetwork
     * Added `service_id` field
     * Added `service_id` field
 * dcim.ConsolePort
 * 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
     * Added `data_type` and `object_type` fields
 * extras.CustomLink
 * extras.CustomLink
     * Added `enabled` field
     * Added `enabled` field
+* ipam.ASN
+    * Added `provider_count` field
 * ipam.VLANGroup
 * ipam.VLANGroup
     * Added the `/availables-vlans/` endpoint
     * Added the `/availables-vlans/` endpoint
     * Added the `min_vid` and `max_vid` fields
     * Added the `min_vid` and `max_vid` fields
 * tenancy.Contact
 * tenancy.Contact
-    * Added the `link` field
+    * Added `link` field
 * virtualization.VMInterface
 * virtualization.VMInterface
     * Added `vrf` field
     * Added `vrf` field

+ 12 - 2
netbox/circuits/api/serializers.py

@@ -4,7 +4,9 @@ from circuits.choices import CircuitStatusChoices
 from circuits.models import *
 from circuits.models import *
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
 from dcim.api.nested_serializers import NestedCableSerializer, NestedSiteSerializer
 from dcim.api.serializers import LinkTerminationSerializer
 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 netbox.api.serializers import NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from tenancy.api.nested_serializers import NestedTenantSerializer
 from .nested_serializers import *
 from .nested_serializers import *
@@ -16,13 +18,21 @@ from .nested_serializers import *
 
 
 class ProviderSerializer(NetBoxModelSerializer):
 class ProviderSerializer(NetBoxModelSerializer):
     url = serializers.HyperlinkedIdentityField(view_name='circuits-api:provider-detail')
     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)
     circuit_count = serializers.IntegerField(read_only=True)
 
 
     class Meta:
     class Meta:
         model = Provider
         model = Provider
         fields = [
         fields = [
             'id', 'url', 'display', 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact',
             '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):
 class ProviderViewSet(NetBoxModelViewSet):
-    queryset = Provider.objects.prefetch_related('tags').annotate(
+    queryset = Provider.objects.prefetch_related('asns', 'tags').annotate(
         circuit_count=count_related(Circuit, 'provider')
         circuit_count=count_related(Circuit, 'provider')
     )
     )
     serializer_class = serializers.ProviderSerializer
     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.filtersets import CableTerminationFilterSet
 from dcim.models import Region, Site, SiteGroup
 from dcim.models import Region, Site, SiteGroup
+from ipam.models import ASN
 from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
 from netbox.filtersets import ChangeLoggedModelFilterSet, NetBoxModelFilterSet, OrganizationalModelFilterSet
 from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 from tenancy.filtersets import ContactModelFilterSet, TenancyFilterSet
 from utilities.filters import TreeNodeMultipleChoiceFilter
 from utilities.filters import TreeNodeMultipleChoiceFilter
@@ -56,6 +57,11 @@ class ProviderFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Site (slug)',
         label='Site (slug)',
     )
     )
+    asn_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='asns',
+        queryset=ASN.objects.all(),
+        label='ASN (ID)',
+    )
 
 
     class Meta:
     class Meta:
         model = Provider
         model = Provider

+ 14 - 4
netbox/circuits/forms/bulk_edit.py

@@ -1,10 +1,15 @@
 from django import forms
 from django import forms
+from django.utils.translation import gettext as _
 
 
 from circuits.choices import CircuitStatusChoices
 from circuits.choices import CircuitStatusChoices
 from circuits.models import *
 from circuits.models import *
+from ipam.models import ASN
 from netbox.forms import NetBoxModelBulkEditForm
 from netbox.forms import NetBoxModelBulkEditForm
 from tenancy.models import Tenant
 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__ = (
 __all__ = (
     'CircuitBulkEditForm',
     'CircuitBulkEditForm',
@@ -17,7 +22,12 @@ __all__ = (
 class ProviderBulkEditForm(NetBoxModelBulkEditForm):
 class ProviderBulkEditForm(NetBoxModelBulkEditForm):
     asn = forms.IntegerField(
     asn = forms.IntegerField(
         required=False,
         required=False,
-        label='ASN'
+        label='ASN (legacy)'
+    )
+    asns = DynamicModelMultipleChoiceField(
+        queryset=ASN.objects.all(),
+        label=_('ASNs'),
+        required=False
     )
     )
     account = forms.CharField(
     account = forms.CharField(
         max_length=30,
         max_length=30,
@@ -45,10 +55,10 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
 
 
     model = Provider
     model = Provider
     fieldsets = (
     fieldsets = (
-        (None, ('asn', 'account', 'portal_url', 'noc_contact', 'admin_contact')),
+        (None, ('asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact')),
     )
     )
     nullable_fields = (
     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.choices import CircuitStatusChoices
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Region, Site, SiteGroup
 from dcim.models import Region, Site, SiteGroup
+from ipam.models import ASN
 from netbox.forms import NetBoxModelFilterSetForm
 from netbox.forms import NetBoxModelFilterSetForm
 from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
 from tenancy.forms import TenancyFilterForm, ContactModelFilterForm
 from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField
 from utilities.forms import DynamicModelMultipleChoiceField, MultipleChoiceField, TagFilterField
@@ -45,7 +46,12 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
     )
     )
     asn = forms.IntegerField(
     asn = forms.IntegerField(
         required=False,
         required=False,
-        label=_('ASN')
+        label=_('ASN (legacy)')
+    )
+    asn_id = DynamicModelMultipleChoiceField(
+        queryset=ASN.objects.all(),
+        required=False,
+        label=_('ASNs')
     )
     )
     tag = TagFilterField(model)
     tag = TagFilterField(model)
 
 

+ 9 - 3
netbox/circuits/forms/models.py

@@ -1,8 +1,9 @@
 from django import forms
 from django import forms
+from django.utils.translation import gettext as _
 
 
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Region, Site, SiteGroup
 from dcim.models import Region, Site, SiteGroup
-from extras.models import Tag
+from ipam.models import ASN
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm
 from utilities.forms import (
 from utilities.forms import (
@@ -21,17 +22,22 @@ __all__ = (
 
 
 class ProviderForm(NetBoxModelForm):
 class ProviderForm(NetBoxModelForm):
     slug = SlugField()
     slug = SlugField()
+    asns = DynamicModelMultipleChoiceField(
+        queryset=ASN.objects.all(),
+        label=_('ASNs'),
+        required=False
+    )
     comments = CommentField()
     comments = CommentField()
 
 
     fieldsets = (
     fieldsets = (
-        ('Provider', ('name', 'slug', 'asn', 'tags')),
+        ('Provider', ('name', 'slug', 'asn', 'asns', 'tags')),
         ('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
         ('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')),
     )
     )
 
 
     class Meta:
     class Meta:
         model = Provider
         model = Provider
         fields = [
         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 = {
         widgets = {
             'noc_contact': SmallTextarea(
             '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',
         verbose_name='ASN',
         help_text='32-bit autonomous system number'
         help_text='32-bit autonomous system number'
     )
     )
+    asns = models.ManyToManyField(
+        to='ipam.ASN',
+        related_name='providers',
+        blank=True
+    )
     account = models.CharField(
     account = models.CharField(
         max_length=30,
         max_length=30,
         blank=True,
         blank=True,

+ 12 - 2
netbox/circuits/tables/providers.py

@@ -14,6 +14,16 @@ class ProviderTable(NetBoxTable):
     name = tables.Column(
     name = tables.Column(
         linkify=True
         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(
     circuit_count = tables.Column(
         accessor=Accessor('count_circuits'),
         accessor=Accessor('count_circuits'),
         verbose_name='Circuits'
         verbose_name='Circuits'
@@ -29,8 +39,8 @@ class ProviderTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = Provider
         model = Provider
         fields = (
         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')
         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.choices import *
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Site
 from dcim.models import Site
+from ipam.models import ASN, RIR
 from utilities.testing import APITestCase, APIViewTestCases
 from utilities.testing import APITestCase, APIViewTestCases
 
 
 
 
@@ -18,20 +19,6 @@ class AppTest(APITestCase):
 class ProviderTest(APIViewTestCases.APIViewTestCase):
 class ProviderTest(APIViewTestCases.APIViewTestCase):
     model = Provider
     model = Provider
     brief_fields = ['circuit_count', 'display', 'id', 'name', 'slug', 'url']
     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 = {
     bulk_update_data = {
         'asn': 1234,
         'asn': 1234,
     }
     }
@@ -39,6 +26,12 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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 = (
         providers = (
             Provider(name='Provider 1', slug='provider-1'),
             Provider(name='Provider 1', slug='provider-1'),
             Provider(name='Provider 2', slug='provider-2'),
             Provider(name='Provider 2', slug='provider-2'),
@@ -46,6 +39,24 @@ class ProviderTest(APIViewTestCases.APIViewTestCase):
         )
         )
         Provider.objects.bulk_create(providers)
         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):
 class CircuitTypeTest(APIViewTestCases.APIViewTestCase):
     model = CircuitType
     model = CircuitType

+ 18 - 1
netbox/circuits/tests/test_filtersets.py

@@ -4,6 +4,7 @@ from circuits.choices import *
 from circuits.filtersets import *
 from circuits.filtersets import *
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Cable, Region, Site, SiteGroup
 from dcim.models import Cable, Region, Site, SiteGroup
+from ipam.models import ASN, RIR
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.testing import ChangeLoggedFilterSetTests
 from utilities.testing import ChangeLoggedFilterSetTests
 
 
@@ -15,6 +16,14 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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 = (
         providers = (
             Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'),
             Provider(name='Provider 1', slug='provider-1', asn=65001, account='1234'),
             Provider(name='Provider 2', slug='provider-2', asn=65002, account='2345'),
             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(name='Provider 5', slug='provider-5', asn=65005, account='5678'),
         )
         )
         Provider.objects.bulk_create(providers)
         Provider.objects.bulk_create(providers)
+        providers[0].asns.set([asns[0]])
+        providers[1].asns.set([asns[1]])
+        providers[2].asns.set([asns[2]])
 
 
         regions = (
         regions = (
             Region(name='Test Region 1', slug='test-region-1'),
             Region(name='Test Region 1', slug='test-region-1'),
@@ -70,10 +82,15 @@ class ProviderTestCase(TestCase, ChangeLoggedFilterSetTests):
         params = {'slug': ['provider-1', 'provider-2']}
         params = {'slug': ['provider-1', 'provider-2']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 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']}
         params = {'asn': ['65001', '65002']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         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):
     def test_account(self):
         params = {'account': ['1234', '2345']}
         params = {'account': ['1234', '2345']}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         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.choices import *
 from circuits.models import *
 from circuits.models import *
 from dcim.models import Cable, Interface, Site
 from dcim.models import Cable, Interface, Site
+from ipam.models import ASN, RIR
 from utilities.testing import ViewTestCases, create_tags, create_test_device
 from utilities.testing import ViewTestCases, create_tags, create_test_device
 
 
 
 
@@ -15,11 +16,21 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
     @classmethod
     @classmethod
     def setUpTestData(cls):
     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 1', slug='provider-1', asn=65001),
             Provider(name='Provider 2', slug='provider-2', asn=65002),
             Provider(name='Provider 2', slug='provider-2', asn=65002),
             Provider(name='Provider 3', slug='provider-3', asn=65003),
             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')
         tags = create_tags('Alpha', 'Bravo', 'Charlie')
 
 
@@ -27,6 +38,7 @@ class ProviderTestCase(ViewTestCases.PrimaryObjectViewTestCase):
             'name': 'Provider X',
             'name': 'Provider X',
             'slug': 'provider-x',
             'slug': 'provider-x',
             'asn': 65123,
             'asn': 65123,
+            'asns': [asns[6].pk, asns[7].pk],
             'account': '1234',
             'account': '1234',
             'portal_url': 'http://example.com/portal',
             'portal_url': 'http://example.com/portal',
             'noc_contact': 'noc@example.com',
             'noc_contact': 'noc@example.com',

+ 4 - 4
netbox/dcim/api/serializers.py

@@ -134,10 +134,10 @@ class SiteSerializer(NetBoxModelSerializer):
     class Meta:
     class Meta:
         model = Site
         model = Site
         fields = [
         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.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.models import *
 from dcim.models import *
-from extras.models import Tag
 from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
 from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
 from netbox.forms import NetBoxModelForm
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
 from tenancy.forms import TenancyForm

+ 4 - 4
netbox/dcim/tables/sites.py

@@ -86,16 +86,16 @@ class SiteTable(NetBoxTable):
     group = tables.Column(
     group = tables.Column(
         linkify=True
         linkify=True
     )
     )
+    asns = tables.ManyToManyColumn(
+        linkify_item=True,
+        verbose_name='ASNs'
+    )
     asn_count = columns.LinkedCountColumn(
     asn_count = columns.LinkedCountColumn(
         accessor=tables.A('asns__count'),
         accessor=tables.A('asns__count'),
         viewname='ipam:asn_list',
         viewname='ipam:asn_list',
         url_params={'site_id': 'pk'},
         url_params={'site_id': 'pk'},
         verbose_name='ASN Count'
         verbose_name='ASN Count'
     )
     )
-    asns = tables.ManyToManyColumn(
-        linkify_item=True,
-        verbose_name='ASNs'
-    )
     tenant = TenantColumn()
     tenant = TenantColumn()
     comments = columns.MarkdownColumn()
     comments = columns.MarkdownColumn()
     contacts = tables.ManyToManyColumn(
     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')
     url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     tenant = NestedTenantSerializer(required=False, allow_null=True)
     site_count = serializers.IntegerField(read_only=True)
     site_count = serializers.IntegerField(read_only=True)
+    provider_count = serializers.IntegerField(read_only=True)
 
 
     class Meta:
     class Meta:
         model = ASN
         model = ASN
         fields = [
         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.routers import APIRootView
 from rest_framework.views import APIView
 from rest_framework.views import APIView
 
 
+from circuits.models import Provider
 from dcim.models import Site
 from dcim.models import Site
 from ipam import filtersets
 from ipam import filtersets
 from ipam.models import *
 from ipam.models import *
@@ -32,7 +33,10 @@ class IPAMRootView(APIRootView):
 #
 #
 
 
 class ASNViewSet(NetBoxModelViewSet):
 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
     serializer_class = serializers.ASNSerializer
     filterset_class = filtersets.ASNFilterSet
     filterset_class = filtersets.ASNFilterSet
 
 

+ 8 - 3
netbox/ipam/tables/ip.py

@@ -113,6 +113,11 @@ class ASNTable(NetBoxTable):
         url_params={'asn_id': 'pk'},
         url_params={'asn_id': 'pk'},
         verbose_name='Site Count'
         verbose_name='Site Count'
     )
     )
+    provider_count = columns.LinkedCountColumn(
+        viewname='circuits:provider_list',
+        url_params={'asn_id': 'pk'},
+        verbose_name='Provider Count'
+    )
     sites = tables.ManyToManyColumn(
     sites = tables.ManyToManyColumn(
         linkify_item=True,
         linkify_item=True,
         verbose_name='Sites'
         verbose_name='Sites'
@@ -125,10 +130,10 @@ class ASNTable(NetBoxTable):
     class Meta(NetBoxTable.Meta):
     class Meta(NetBoxTable.Meta):
         model = ASN
         model = ASN
         fields = (
         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.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse
 from django.urls import reverse
 
 
+from circuits.models import Provider
+from circuits.tables import ProviderTable
 from dcim.filtersets import InterfaceFilterSet
 from dcim.filtersets import InterfaceFilterSet
 from dcim.models import Interface, Site
 from dcim.models import Interface, Site
 from dcim.tables import SiteTable
 from dcim.tables import SiteTable
@@ -206,6 +208,7 @@ class RIRBulkDeleteView(generic.BulkDeleteView):
 class ASNListView(generic.ObjectListView):
 class ASNListView(generic.ObjectListView):
     queryset = ASN.objects.annotate(
     queryset = ASN.objects.annotate(
         site_count=count_related(Site, 'asns'),
         site_count=count_related(Site, 'asns'),
+        provider_count=count_related(Provider, 'asns')
     )
     )
     filterset = filtersets.ASNFilterSet
     filterset = filtersets.ASNFilterSet
     filterset_form = forms.ASNFilterForm
     filterset_form = forms.ASNFilterForm
@@ -216,13 +219,21 @@ class ASNView(generic.ObjectView):
     queryset = ASN.objects.all()
     queryset = ASN.objects.all()
 
 
     def get_extra_context(self, request, instance):
     def get_extra_context(self, request, instance):
+        # Gather assigned Sites
         sites = instance.sites.restrict(request.user, 'view')
         sites = instance.sites.restrict(request.user, 'view')
         sites_table = SiteTable(sites)
         sites_table = SiteTable(sites)
         sites_table.configure(request)
         sites_table.configure(request)
 
 
+        # Gather assigned Providers
+        providers = instance.providers.restrict(request.user, 'view')
+        providers_table = ProviderTable(providers)
+        providers_table.configure(request)
+
         return {
         return {
             'sites_table': sites_table,
             '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="row mb-3">
 	  <div class="col col-md-6">
 	  <div class="col col-md-6">
         <div class="card">
         <div class="card">
-            <h5 class="card-header">
-                Provider
-            </h5>
+            <h5 class="card-header">Provider</h5>
             <div class="card-body">
             <div class="card-body">
                 <table class="table table-hover attr-table">
                 <table class="table table-hover attr-table">
                     <tr>
                     <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>
                     <tr>
                     <tr>
                         <th scope="row">Account</th>
                         <th scope="row">Account</th>

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

@@ -45,7 +45,17 @@
                 {% if sites_count %}
                 {% if sites_count %}
                   <a href="{% url 'dcim:site_list' %}?asn_id={{ object.pk }}">{{ sites_count }}</a>
                   <a href="{% url 'dcim:site_list' %}?asn_id={{ object.pk }}">{{ sites_count }}</a>
                 {% else %}
                 {% 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 %}
                 {% endif %}
               </td>
               </td>
             </tr>
             </tr>
@@ -69,6 +79,13 @@
           {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
           {% include 'inc/paginator.html' with paginator=sites_table.paginator page=sites_table.page %}
         </div>
         </div>
       </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 %}
       {% plugin_full_width_page object %}
     </div>
     </div>
   </div>
   </div>