mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 09:51:22 -06:00
Closes #8496: Enable assigning multiple ASNs to a provider
This commit is contained in:
parent
cdacd2a951
commit
bddc35bbc7
@ -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
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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
netbox/circuits/migrations/0035_provider_asns.py
Normal file
19
netbox/circuits/migrations/0035_provider_asns.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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,
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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',
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -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(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user