Introduce ASNRange model

This commit is contained in:
jeremystretch 2023-02-25 14:52:42 -05:00
parent 1e1aac5c48
commit 7e7c9e981d
22 changed files with 432 additions and 74 deletions

View File

@ -7,6 +7,7 @@ from netbox.api.serializers import WritableNestedSerializer
__all__ = [ __all__ = [
'NestedAggregateSerializer', 'NestedAggregateSerializer',
'NestedASNSerializer', 'NestedASNSerializer',
'NestedASNRangeSerializer',
'NestedFHRPGroupSerializer', 'NestedFHRPGroupSerializer',
'NestedFHRPGroupAssignmentSerializer', 'NestedFHRPGroupAssignmentSerializer',
'NestedIPAddressSerializer', 'NestedIPAddressSerializer',
@ -25,6 +26,18 @@ __all__ = [
] ]
#
# ASN ranges
#
class NestedASNRangeSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
class Meta:
model = models.ASNRange
fields = ['id', 'url', 'display', 'name']
# #
# ASNs # ASNs
# #

View File

@ -15,12 +15,26 @@ from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .nested_serializers import * from .nested_serializers import *
#
# ASN ranges
#
class ASNRangeSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asnrange-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
asn_count = serializers.IntegerField(read_only=True)
class Meta:
model = ASNRange
fields = [
'id', 'url', 'display', 'name', 'slug', 'start', 'end', 'tenant', 'description', 'tags', 'custom_fields',
'created', 'last_updated', 'asn_count',
]
# #
# ASNs # ASNs
# #
from .nested_serializers import NestedL2VPNSerializer
from ..models.l2vpn import L2VPNTermination, L2VPN
class ASNSerializer(NetBoxModelSerializer): class ASNSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail') url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')

View File

@ -7,44 +7,22 @@ from . import views
router = NetBoxRouter() router = NetBoxRouter()
router.APIRootView = views.IPAMRootView router.APIRootView = views.IPAMRootView
# ASNs
router.register('asns', views.ASNViewSet) router.register('asns', views.ASNViewSet)
router.register('asn-ranges', views.ASNRangeViewSet)
# VRFs
router.register('vrfs', views.VRFViewSet) router.register('vrfs', views.VRFViewSet)
# Route targets
router.register('route-targets', views.RouteTargetViewSet) router.register('route-targets', views.RouteTargetViewSet)
# RIRs
router.register('rirs', views.RIRViewSet) router.register('rirs', views.RIRViewSet)
# Aggregates
router.register('aggregates', views.AggregateViewSet) router.register('aggregates', views.AggregateViewSet)
# Prefixes
router.register('roles', views.RoleViewSet) router.register('roles', views.RoleViewSet)
router.register('prefixes', views.PrefixViewSet) router.register('prefixes', views.PrefixViewSet)
# IP ranges
router.register('ip-ranges', views.IPRangeViewSet) router.register('ip-ranges', views.IPRangeViewSet)
# IP addresses
router.register('ip-addresses', views.IPAddressViewSet) router.register('ip-addresses', views.IPAddressViewSet)
# FHRP groups
router.register('fhrp-groups', views.FHRPGroupViewSet) router.register('fhrp-groups', views.FHRPGroupViewSet)
router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet) router.register('fhrp-group-assignments', views.FHRPGroupAssignmentViewSet)
# VLANs
router.register('vlan-groups', views.VLANGroupViewSet) router.register('vlan-groups', views.VLANGroupViewSet)
router.register('vlans', views.VLANViewSet) router.register('vlans', views.VLANViewSet)
# Services
router.register('service-templates', views.ServiceTemplateViewSet) router.register('service-templates', views.ServiceTemplateViewSet)
router.register('services', views.ServiceViewSet) router.register('services', views.ServiceViewSet)
# L2VPN
router.register('l2vpns', views.L2VPNViewSet) router.register('l2vpns', views.L2VPNViewSet)
router.register('l2vpn-terminations', views.L2VPNTerminationViewSet) router.register('l2vpn-terminations', views.L2VPNTerminationViewSet)

View File

@ -33,6 +33,14 @@ class IPAMRootView(APIRootView):
# Viewsets # Viewsets
# #
class ASNRangeViewSet(NetBoxModelViewSet):
queryset = ASNRange.objects.prefetch_related('tenant').annotate(
asn_count=count_related(ASN, 'range')
)
serializer_class = serializers.ASNRangeSerializer
filterset_class = filtersets.ASNRangeFilterSet
class ASNViewSet(NetBoxModelViewSet): class ASNViewSet(NetBoxModelViewSet):
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate( queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(
site_count=count_related(Site, 'asns'), site_count=count_related(Site, 'asns'),

View File

@ -20,6 +20,7 @@ from .models import *
__all__ = ( __all__ = (
'AggregateFilterSet', 'AggregateFilterSet',
'ASNFilterSet', 'ASNFilterSet',
'ASNRangeFilterSet',
'FHRPGroupAssignmentFilterSet', 'FHRPGroupAssignmentFilterSet',
'FHRPGroupFilterSet', 'FHRPGroupFilterSet',
'IPAddressFilterSet', 'IPAddressFilterSet',
@ -167,6 +168,19 @@ class AggregateFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
return queryset.none() return queryset.none()
class ASNRangeFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
class Meta:
model = ASNRange
fields = ['id', 'start', 'end', 'description']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(description__icontains=value)
return queryset.filter(qs_filter)
class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet): class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
rir_id = django_filters.ModelMultipleChoiceFilter( rir_id = django_filters.ModelMultipleChoiceFilter(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),

View File

@ -16,6 +16,7 @@ from utilities.forms import (
__all__ = ( __all__ = (
'AggregateBulkEditForm', 'AggregateBulkEditForm',
'ASNBulkEditForm', 'ASNBulkEditForm',
'ASNRangeBulkEditForm',
'FHRPGroupBulkEditForm', 'FHRPGroupBulkEditForm',
'IPAddressBulkEditForm', 'IPAddressBulkEditForm',
'IPRangeBulkEditForm', 'IPRangeBulkEditForm',
@ -97,6 +98,23 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm):
nullable_fields = ('is_private', 'description') nullable_fields = ('is_private', 'description')
class ASNRangeBulkEditForm(NetBoxModelBulkEditForm):
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
max_length=200,
required=False
)
model = ASNRange
fieldsets = (
(None, ('tenant', 'description')),
)
nullable_fields = ('date_added', 'description')
class ASNBulkEditForm(NetBoxModelBulkEditForm): class ASNBulkEditForm(NetBoxModelBulkEditForm):
sites = DynamicModelMultipleChoiceField( sites = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),

View File

@ -15,6 +15,7 @@ from virtualization.models import VirtualMachine, VMInterface
__all__ = ( __all__ = (
'AggregateImportForm', 'AggregateImportForm',
'ASNImportForm', 'ASNImportForm',
'ASNRangeImportForm',
'FHRPGroupImportForm', 'FHRPGroupImportForm',
'IPAddressImportForm', 'IPAddressImportForm',
'IPRangeImportForm', 'IPRangeImportForm',
@ -87,6 +88,19 @@ class AggregateImportForm(NetBoxModelImportForm):
fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', 'tags') fields = ('prefix', 'rir', 'tenant', 'date_added', 'description', 'comments', 'tags')
class ASNRangeImportForm(NetBoxModelImportForm):
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned tenant')
)
class Meta:
model = ASNRange
fields = ('name', 'slug', 'start', 'end', 'tenant', 'description', 'tags')
class ASNImportForm(NetBoxModelImportForm): class ASNImportForm(NetBoxModelImportForm):
rir = CSVModelChoiceField( rir = CSVModelChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),

View File

@ -17,6 +17,7 @@ from virtualization.models import VirtualMachine
__all__ = ( __all__ = (
'AggregateFilterForm', 'AggregateFilterForm',
'ASNFilterForm', 'ASNFilterForm',
'ASNRangeFilterForm',
'FHRPGroupFilterForm', 'FHRPGroupFilterForm',
'IPAddressFilterForm', 'IPAddressFilterForm',
'IPRangeFilterForm', 'IPRangeFilterForm',
@ -114,6 +115,22 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model) tag = TagFilterField(model)
class ASNRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASNRange
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Range', ('start', 'end')),
('Tenant', ('tenant_group_id', 'tenant_id')),
)
start = forms.IntegerField(
required=False
)
end = forms.IntegerField(
required=False
)
tag = TagFilterField(model)
class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = ASN model = ASN
fieldsets = ( fieldsets = (

View File

@ -20,6 +20,7 @@ from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInter
__all__ = ( __all__ = (
'AggregateForm', 'AggregateForm',
'ASNForm', 'ASNForm',
'ASNRangeForm',
'FHRPGroupForm', 'FHRPGroupForm',
'FHRPGroupAssignmentForm', 'FHRPGroupAssignmentForm',
'IPAddressAssignForm', 'IPAddressAssignForm',
@ -128,6 +129,20 @@ class AggregateForm(TenancyForm, NetBoxModelForm):
} }
class ASNRangeForm(TenancyForm, NetBoxModelForm):
slug = SlugField()
fieldsets = (
('ASN Range', ('name', 'slug', 'start', 'end', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
class Meta:
model = ASNRange
fields = [
'name', 'slug', 'start', 'end', 'tenant_group', 'tenant', 'description', 'tags'
]
class ASNForm(TenancyForm, NetBoxModelForm): class ASNForm(TenancyForm, NetBoxModelForm):
rir = DynamicModelChoiceField( rir = DynamicModelChoiceField(
queryset=RIR.objects.all(), queryset=RIR.objects.all(),

View File

@ -8,6 +8,9 @@ class IPAMQuery(graphene.ObjectType):
asn = ObjectField(ASNType) asn = ObjectField(ASNType)
asn_list = ObjectListField(ASNType) asn_list = ObjectListField(ASNType)
asnrange = ObjectField(ASNRangeType)
asnrange_list = ObjectListField(ASNRangeType)
aggregate = ObjectField(AggregateType) aggregate = ObjectField(AggregateType)
aggregate_list = ObjectListField(AggregateType) aggregate_list = ObjectListField(AggregateType)

View File

@ -1,6 +1,5 @@
import graphene import graphene
from graphene_django import DjangoObjectType
from extras.graphql.mixins import ContactsMixin from extras.graphql.mixins import ContactsMixin
from ipam import filtersets, models from ipam import filtersets, models
from netbox.graphql.scalars import BigInt from netbox.graphql.scalars import BigInt
@ -8,6 +7,7 @@ from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, NetBo
__all__ = ( __all__ = (
'ASNType', 'ASNType',
'ASNRangeType',
'AggregateType', 'AggregateType',
'FHRPGroupType', 'FHRPGroupType',
'FHRPGroupAssignmentType', 'FHRPGroupAssignmentType',
@ -36,6 +36,14 @@ class ASNType(NetBoxObjectType):
filterset_class = filtersets.ASNFilterSet filterset_class = filtersets.ASNFilterSet
class ASNRangeType(NetBoxObjectType):
class Meta:
model = models.ASNRange
fields = '__all__'
filterset_class = filtersets.ASNRangeFilterSet
class AggregateType(NetBoxObjectType): class AggregateType(NetBoxObjectType):
class Meta: class Meta:

View File

@ -0,0 +1,45 @@
# Generated by Django 4.1.7 on 2023-02-25 19:49
from django.db import migrations, models
import django.db.models.deletion
import ipam.fields
import taggit.managers
import utilities.json
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0009_standardize_description_comments'),
('extras', '0087_dashboard'),
('ipam', '0063_standardize_description_comments'),
]
operations = [
migrations.CreateModel(
name='ASNRange',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
('created', models.DateTimeField(auto_now_add=True, null=True)),
('last_updated', models.DateTimeField(auto_now=True, null=True)),
('custom_field_data', models.JSONField(blank=True, default=dict, encoder=utilities.json.CustomFieldJSONEncoder)),
('description', models.CharField(blank=True, max_length=200)),
('name', models.CharField(max_length=100, unique=True)),
('slug', models.SlugField(max_length=100, unique=True)),
('start', ipam.fields.ASNField()),
('end', ipam.fields.ASNField()),
('tags', taggit.managers.TaggableManager(through='extras.TaggedItem', to='extras.Tag')),
('tenant', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='asn_ranges', to='tenancy.tenant')),
],
options={
'verbose_name': 'ASN range',
'verbose_name_plural': 'ASN ranges',
'ordering': ('name',),
},
),
migrations.AddField(
model_name='asn',
name='range',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='ipam.asnrange'),
),
]

View File

@ -9,6 +9,7 @@ from .vlans import *
__all__ = ( __all__ = (
'ASN', 'ASN',
'ASNRange',
'Aggregate', 'Aggregate',
'IPAddress', 'IPAddress',
'IPRange', 'IPRange',

View File

@ -1,13 +1,14 @@
from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from ipam.fields import ASNField from ipam.fields import ASNField
from netbox.models import PrimaryModel from netbox.models import OrganizationalModel, PrimaryModel
__all__ = ( __all__ = (
'ASN', 'ASN',
'ASNRange',
) )
@ -16,6 +17,12 @@ class ASN(PrimaryModel):
An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have
one or more ASNs assigned to it. one or more ASNs assigned to it.
""" """
range = models.ForeignKey(
to='ipam.ASNRange',
on_delete=models.PROTECT,
blank=True,
null=True
)
asn = ASNField( asn = ASNField(
unique=True, unique=True,
verbose_name='ASN', verbose_name='ASN',
@ -68,3 +75,49 @@ class ASN(PrimaryModel):
return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})' return f'{self.asn} ({self.asn // 65536}.{self.asn % 65536})'
else: else:
return self.asn return self.asn
def clean(self):
if self.range and self.asn not in self.range.range:
raise ValidationError(f"ASN {self.asn} is outside of assigned range ({self.range})")
class ASNRange(OrganizationalModel):
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
start = ASNField()
end = ASNField()
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='asn_ranges',
blank=True,
null=True
)
class Meta:
ordering = ('name',)
verbose_name = 'ASN range'
verbose_name_plural = 'ASN ranges'
def __str__(self):
return f'ASNs {self.range_as_string()}'
def get_absolute_url(self):
return reverse('ipam:asnrange', args=[self.pk])
@property
def range(self):
return range(self.start, self.end + 1)
def range_as_string(self):
return f'{self.start}-{self.end}'
def clean(self):
if self.end <= self.start:
raise ValidationError(f"Starting ASN ({self.start}) must be lower than ending ASN ({self.end}).")

View File

@ -22,6 +22,14 @@ class ASNIndex(SearchIndex):
) )
@register_search
class ASNRangeIndex(SearchIndex):
model = models.ASNRange
fields = (
('description', 500),
)
@register_search @register_search
class FHRPGroupIndex(SearchIndex): class FHRPGroupIndex(SearchIndex):
model = models.FHRPGroup model = models.FHRPGroup

View File

@ -1,3 +1,4 @@
from .asn import *
from .fhrp import * from .fhrp import *
from .ip import * from .ip import *
from .l2vpn import * from .l2vpn import *

69
netbox/ipam/tables/asn.py Normal file
View File

@ -0,0 +1,69 @@
import django_tables2 as tables
from django.utils.translation import gettext as _
from ipam.models import *
from netbox.tables import NetBoxTable, columns
from tenancy.tables import TenancyColumnsMixin
__all__ = (
'ASNTable',
'ASNRangeTable',
)
class ASNRangeTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column(
linkify=True
)
tags = columns.TagColumn(
url_name='ipam:asnrange_list'
)
asn_count = columns.LinkedCountColumn(
viewname='ipam:asn_list',
url_params={'asn_id': 'pk'},
verbose_name=_('ASN Count')
)
class Meta(NetBoxTable.Meta):
model = ASNRange
fields = (
'pk', 'name', 'slug', 'start', 'end', 'asn_count', 'tenant', 'tenant_group', 'description', 'tags',
'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'name', 'start', 'end', 'tenant', 'asn_count', 'description')
class ASNTable(TenancyColumnsMixin, NetBoxTable):
asn = tables.Column(
linkify=True
)
asn_asdot = tables.Column(
accessor=tables.A('asn_asdot'),
linkify=True,
verbose_name=_('ASDOT')
)
site_count = columns.LinkedCountColumn(
viewname='dcim:site_list',
url_params={'asn_id': 'pk'},
verbose_name=_('Site Count')
)
provider_count = columns.LinkedCountColumn(
viewname='circuits:provider_list',
url_params={'asn_id': 'pk'},
verbose_name=_('Provider Count')
)
sites = columns.ManyToManyColumn(
linkify_item=True
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:asn_list'
)
class Meta(NetBoxTable.Meta):
model = ASN
fields = (
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description',
'comments', 'sites', 'tags', 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')

View File

@ -8,7 +8,6 @@ from tenancy.tables import TenancyColumnsMixin, TenantColumn
__all__ = ( __all__ = (
'AggregateTable', 'AggregateTable',
'ASNTable',
'AssignedIPAddressesTable', 'AssignedIPAddressesTable',
'IPAddressAssignTable', 'IPAddressAssignTable',
'IPAddressTable', 'IPAddressTable',
@ -93,47 +92,6 @@ class RIRTable(NetBoxTable):
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description') default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description')
#
# ASNs
#
class ASNTable(TenancyColumnsMixin, NetBoxTable):
asn = tables.Column(
linkify=True
)
asn_asdot = tables.Column(
accessor=tables.A('asn_asdot'),
linkify=True,
verbose_name='ASDOT'
)
site_count = columns.LinkedCountColumn(
viewname='dcim:site_list',
url_params={'asn_id': 'pk'},
verbose_name='Site Count'
)
provider_count = columns.LinkedCountColumn(
viewname='circuits:provider_list',
url_params={'asn_id': 'pk'},
verbose_name='Provider Count'
)
sites = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='Sites'
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='ipam:asn_list'
)
class Meta(NetBoxTable.Meta):
model = ASN
fields = (
'pk', 'asn', 'asn_asdot', 'rir', 'site_count', 'provider_count', 'tenant', 'tenant_group', 'description',
'comments', 'sites', 'tags', 'created', 'last_updated', 'actions',
)
default_columns = ('pk', 'asn', 'rir', 'site_count', 'provider_count', 'sites', 'description', 'tenant')
# #
# Aggregates # Aggregates
# #

View File

@ -6,6 +6,14 @@ from . import views
app_name = 'ipam' app_name = 'ipam'
urlpatterns = [ urlpatterns = [
# ASN ranges
path('asn-ranges/', views.ASNRangeListView.as_view(), name='asnrange_list'),
path('asn-ranges/add/', views.ASNRangeEditView.as_view(), name='asnrange_add'),
path('asn-ranges/import/', views.ASNRangeBulkImportView.as_view(), name='asnrange_import'),
path('asn-ranges/edit/', views.ASNRangeBulkEditView.as_view(), name='asnrange_bulk_edit'),
path('asn-ranges/delete/', views.ASNRangeBulkDeleteView.as_view(), name='asnrange_bulk_delete'),
path('asn-ranges/<int:pk>/', include(get_model_urls('ipam', 'asnrange'))),
# ASNs # ASNs
path('asns/', views.ASNListView.as_view(), name='asn_list'), path('asns/', views.ASNListView.as_view(), name='asn_list'),
path('asns/add/', views.ASNEditView.as_view(), name='asn_add'), path('asns/add/', views.ASNEditView.as_view(), name='asn_add'),

View File

@ -7,16 +7,15 @@ from django.utils.translation import gettext as _
from circuits.models import Provider from circuits.models import Provider
from dcim.filtersets import InterfaceFilterSet from dcim.filtersets import InterfaceFilterSet
from dcim.models import Interface, Site, Device from dcim.models import Interface, Site
from netbox.views import generic from netbox.views import generic
from utilities.utils import count_related from utilities.utils import count_related
from utilities.views import ViewTab, register_model_view from utilities.views import ViewTab, register_model_view
from virtualization.filtersets import VMInterfaceFilterSet from virtualization.filtersets import VMInterfaceFilterSet
from virtualization.models import VMInterface, VirtualMachine from virtualization.models import VMInterface
from . import filtersets, forms, tables from . import filtersets, forms, tables
from .constants import * from .constants import *
from .models import * from .models import *
from .models import ASN
from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable from .tables.l2vpn import L2VPNTable, L2VPNTerminationTable
from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans from .utils import add_requested_prefixes, add_available_ipaddresses, add_available_vlans
@ -195,6 +194,67 @@ class RIRBulkDeleteView(generic.BulkDeleteView):
table = tables.RIRTable table = tables.RIRTable
#
# ASN ranges
#
class ASNRangeListView(generic.ObjectListView):
queryset = ASNRange.objects.annotate(
asn_count=count_related(ASN, 'range'),
)
filterset = filtersets.ASNRangeFilterSet
filterset_form = forms.ASNRangeFilterForm
table = tables.ASNRangeTable
@register_model_view(ASNRange)
class ASNRangeView(generic.ObjectView):
queryset = ASNRange.objects.all()
def get_extra_context(self, request, instance):
related_models = (
(ASN.objects.restrict(request.user, 'view').filter(range=instance), 'asn_id'),
)
return {
'related_models': related_models,
}
@register_model_view(ASNRange, 'edit')
class ASNRangeEditView(generic.ObjectEditView):
queryset = ASNRange.objects.all()
form = forms.ASNRangeForm
@register_model_view(ASNRange, 'delete')
class ASNRangeDeleteView(generic.ObjectDeleteView):
queryset = ASNRange.objects.all()
class ASNRangeBulkImportView(generic.BulkImportView):
queryset = ASNRange.objects.all()
model_form = forms.ASNRangeImportForm
table = tables.ASNRangeTable
class ASNRangeBulkEditView(generic.BulkEditView):
queryset = ASNRange.objects.annotate(
site_count=count_related(Site, 'asns')
)
filterset = filtersets.ASNRangeFilterSet
table = tables.ASNRangeTable
form = forms.ASNRangeBulkEditForm
class ASNRangeBulkDeleteView(generic.BulkDeleteView):
queryset = ASNRange.objects.annotate(
site_count=count_related(Site, 'asns')
)
filterset = filtersets.ASNRangeFilterSet
table = tables.ASNRangeTable
# #
# ASNs # ASNs
# #

View File

@ -158,6 +158,7 @@ IPAM_MENU = Menu(
MenuGroup( MenuGroup(
label=_('ASNs'), label=_('ASNs'),
items=( items=(
get_model_item('ipam', 'asnrange', _('ASN Ranges')),
get_model_item('ipam', 'asn', _('ASNs')), get_model_item('ipam', 'asn', _('ASNs')),
), ),
), ),

View File

@ -0,0 +1,52 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% load render_table from django_tables2 %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">ASN Range</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<td>Name</td>
<td>{{ object.name }}</td>
</tr>
<tr>
<td>Range</td>
<td>{{ object.range_as_string }}</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
{% include 'inc/panels/tags.html' %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/related_objects.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock content %}