Closes #8496: Enable assigning multiple ASNs to a provider

This commit is contained in:
jeremystretch 2022-03-30 17:17:36 -04:00
parent cdacd2a951
commit bddc35bbc7
22 changed files with 222 additions and 53 deletions

View File

@ -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

View File

@ -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',
] ]

View File

@ -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

View File

@ -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

View File

@ -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',
) )

View File

@ -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)

View File

@ -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(

View File

@ -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'),
),
]

View File

@ -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,

View File

@ -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', 'pk', 'id', 'name', 'asn', 'asns', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'asn_count',
'comments', 'contacts', 'tags', 'created', 'last_updated', 'circuit_count', 'comments', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count') default_columns = ('pk', 'name', 'asn', 'account', 'circuit_count')

View File

@ -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

View File

@ -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)

View File

@ -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',

View File

@ -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', 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'asns', 'tags',
'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count',
'rack_count', 'virtualmachine_count', 'vlan_count', 'virtualmachine_count', 'vlan_count',
] ]

View File

@ -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

View File

@ -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(

View File

@ -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', 'id', 'url', 'display', 'asn', 'rir', 'tenant', 'description', 'site_count', 'provider_count', 'tags',
'created', 'last_updated', 'custom_fields', 'created', 'last_updated',
] ]

View File

@ -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

View File

@ -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', 'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'description', 'sites', 'tags',
'last_updated', 'actions', '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')
# #

View File

@ -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(),
} }

View File

@ -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"> <h5 class="card-header">Provider</h5>
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> <th scope="row">ASN</th>
<td>{{ object.asn|placeholder }}</td> <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>

View File

@ -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>