Closes #8496: Enable assigning multiple ASNs to a provider

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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