Initial work on #6732

This commit is contained in:
Daniel Sheppard 2021-10-24 23:42:47 -05:00
parent 8c058dcd45
commit a01068949c
29 changed files with 515 additions and 38 deletions

View File

@ -116,16 +116,18 @@ class SiteSerializer(PrimaryModelSerializer):
device_count = serializers.IntegerField(read_only=True)
prefix_count = serializers.IntegerField(read_only=True)
rack_count = serializers.IntegerField(read_only=True)
asn_count = serializers.IntegerField(read_only=True)
virtualmachine_count = serializers.IntegerField(read_only=True)
vlan_count = serializers.IntegerField(read_only=True)
class Meta:
model = Site
fields = [
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn',
'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asns',
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count',
'asn_count', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count',
'vlan_count',
]

View File

@ -16,7 +16,7 @@ from circuits.models import Circuit
from dcim import filtersets
from dcim.models import *
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
from ipam.models import Prefix, VLAN
from ipam.models import Prefix, VLAN, ASN
from netbox.api.authentication import IsAuthenticatedOrLoginNotRequired
from netbox.api.exceptions import ServiceUnavailable
from netbox.api.metadata import ContentTypeMetadata
@ -139,6 +139,7 @@ class SiteViewSet(CustomFieldModelViewSet):
queryset = Site.objects.prefetch_related(
'region', 'tenant', 'tags'
).annotate(
asn_count=count_related(ASN, 'sites'),
device_count=count_related(Device, 'site'),
rack_count=count_related(Rack, 'site'),
prefix_count=count_related(Prefix, 'site'),

View File

@ -3,6 +3,7 @@ from django.contrib.auth.models import User
from extras.filters import TagFilter
from extras.filtersets import LocalConfigContextFilterSet
from ipam.models import ASN
from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
)
@ -127,12 +128,23 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
to_field_name='slug',
label='Group (slug)',
)
asn_id = django_filters.ModelMultipleChoiceFilter(
field_name='asns',
queryset=ASN.objects.all(),
label='AS (ID)',
)
asn = django_filters.ModelMultipleChoiceFilter(
field_name='asns__asn',
queryset=ASN.objects.all(),
to_field_name='asn',
label='AS (Number)',
)
tag = TagFilter()
class Meta:
model = Site
fields = [
'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'id', 'name', 'slug', 'facility', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email',
]
@ -151,7 +163,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
Q(comments__icontains=value)
)
try:
qs_filter |= Q(asn=int(value.strip()))
qs_filter |= Q(asns=int(value.strip()))
except ValueError:
pass
return queryset.filter(qs_filter)

View File

@ -1,4 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.contrib.auth.models import User
from timezone_field import TimeZoneFormField
@ -6,8 +7,7 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
from ipam.models import VLAN
from ipam.models import VLAN, ASN
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField,
@ -110,11 +110,10 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
queryset=Tenant.objects.all(),
required=False
)
asn = forms.IntegerField(
min_value=BGP_ASN_MIN,
max_value=BGP_ASN_MAX,
required=False,
label='ASN'
asns = DynamicModelChoiceField(
queryset=ASN.objects.all(),
label=_('ASNs'),
required=False
)
description = forms.CharField(
max_length=100,
@ -128,7 +127,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
class Meta:
nullable_fields = [
'region', 'group', 'tenant', 'asn', 'description', 'time_zone',
'region', 'group', 'tenant', 'asns', 'description', 'time_zone',
]

View File

@ -94,7 +94,7 @@ class SiteCSVForm(CustomFieldModelCSVForm):
class Meta:
model = Site
fields = (
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description',
'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email', 'comments',
)

View File

@ -6,6 +6,7 @@ from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
from ipam.models import ASN
from tenancy.forms import TenancyFilterForm
from utilities.forms import (
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
@ -143,11 +144,12 @@ class SiteGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = Site
field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id']
field_order = ['q', 'status', 'region_id', 'tenant_group_id', 'tenant_id', 'asns']
field_groups = [
['q', 'tag'],
['status', 'region_id', 'group_id'],
['tenant_group_id', 'tenant_id'],
['asn_id']
]
q = forms.CharField(
required=False,
@ -171,6 +173,12 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
label=_('Site group'),
fetch_trigger='open'
)
asn_id = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
required=False,
label=_('ASNs'),
fetch_trigger='open'
)
tag = TagFilterField(model)

View File

@ -1,4 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from timezone_field import TimeZoneFormField
@ -8,7 +9,7 @@ from dcim.constants import *
from dcim.models import *
from extras.forms import CustomFieldModelForm
from extras.models import Tag
from ipam.models import IPAddress, VLAN, VLANGroup
from ipam.models import IPAddress, VLAN, VLANGroup, ASN
from tenancy.forms import TenancyForm
from utilities.forms import (
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
@ -101,6 +102,11 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
queryset=SiteGroup.objects.all(),
required=False
)
asns = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
label=_('ASNs'),
required=False
)
slug = SlugField()
time_zone = TimeZoneFormField(
choices=add_blank_choice(TimeZoneFormField().choices),
@ -116,13 +122,13 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class Meta:
model = Site
fields = [
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone',
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_phone', 'contact_email', 'comments', 'tags',
]
fieldsets = (
('Site', (
'name', 'slug', 'status', 'region', 'group', 'facility', 'asn', 'time_zone', 'description', 'tags',
'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
)),
('Tenancy', ('tenant_group', 'tenant')),
('Contact Info', (
@ -147,7 +153,6 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
help_texts = {
'name': "Full name of the site",
'facility': "Data center provider and facility (e.g. Equinix NY7)",
'asn': "BGP autonomous system number",
'time_zone': "Local time zone",
'description': "Short description (will appear in sites list)",
'physical_address': "Physical location of the building (e.g. for GPS)",

View File

@ -189,12 +189,6 @@ class Site(PrimaryModel):
blank=True,
help_text='Local facility ID or description'
)
asn = ASNField(
blank=True,
null=True,
verbose_name='ASN',
help_text='32-bit autonomous system number'
)
time_zone = TimeZoneField(
blank=True
)
@ -257,7 +251,7 @@ class Site(PrimaryModel):
objects = RestrictedQuerySet.as_manager()
clone_fields = [
'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address',
'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', 'physical_address',
'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone', 'contact_email',
]

View File

@ -75,6 +75,11 @@ class SiteTable(BaseTable):
group = tables.Column(
linkify=True
)
asn_count = LinkedCountColumn(
viewname='ipam:asn_list',
url_params={'site_id': 'pk'},
verbose_name='ASNs'
)
tenant = TenantColumn()
comments = MarkdownColumn()
tags = TagColumn(
@ -84,11 +89,11 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = (
'pk', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
'contact_email', 'comments', 'tags',
'pk', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'contact_phone', 'contact_email', 'comments', 'tags',
)
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'description')
default_columns = ('pk', 'name', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'description')
#

View File

@ -14,7 +14,7 @@ from django.views.generic import View
from circuits.models import Circuit
from extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectJournalView
from ipam.models import IPAddress, Prefix, Service, VLAN
from ipam.models import IPAddress, Prefix, Service, VLAN, ASN
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
from netbox.views import generic
from utilities.forms import ConfirmationForm
@ -310,6 +310,7 @@ class SiteView(generic.ObjectView):
def get_extra_context(self, request, instance):
stats = {
'asn_count': ASN.objects.restrict(request.user, 'view').filter(sites=instance).count(),
'rack_count': Rack.objects.restrict(request.user, 'view').filter(site=instance).count(),
'device_count': Device.objects.restrict(request.user, 'view').filter(site=instance).count(),
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(site=instance).count(),

View File

@ -5,6 +5,7 @@ from netbox.api import WritableNestedSerializer
__all__ = [
'NestedAggregateSerializer',
'NestedASNSerializer',
'NestedIPAddressSerializer',
'NestedIPRangeSerializer',
'NestedPrefixSerializer',
@ -18,6 +19,18 @@ __all__ = [
]
#
# ASNs
#
class NestedASNSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
class Meta:
model = models.ASN
fields = ['id', 'url', 'display', 'asn']
#
# VRFs
#

View File

@ -17,6 +17,24 @@ from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .nested_serializers import *
#
# ASNs
#
from ..models import ASN
class ASNSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
class Meta:
model = ASN
fields = [
'id', 'url', 'display', 'asn', 'site_count', 'rir', 'tenant', 'description', 'tags', 'custom_fields',
'created', 'last_updated',
]
#
# VRFs
#

View File

@ -5,6 +5,9 @@ from . import views
router = OrderedDefaultRouter()
router.APIRootView = views.IPAMRootView
# ASNs
router.register('asns', views.ASNViewSet)
# VRFs
router.register('vrfs', views.VRFViewSet)

View File

@ -1,11 +1,13 @@
from rest_framework.routers import APIRootView
from dcim.models import Site
from extras.api.views import CustomFieldModelViewSet
from ipam import filtersets
from ipam.models import *
from netbox.api.views import ModelViewSet
from utilities.utils import count_related
from . import mixins, serializers
from ..models import ASN
class IPAMRootView(APIRootView):
@ -16,6 +18,16 @@ class IPAMRootView(APIRootView):
return 'IPAM'
#
# ASNs
#
class ASNViewSet(CustomFieldModelViewSet):
queryset = ASN.objects.prefetch_related('tenant', 'rir').annotate(site_count=count_related(Site, 'asns'))
serializer_class = serializers.ASNSerializer
filterset_class = filtersets.ASNFilterSet
#
# VRFs
#

View File

@ -9,6 +9,7 @@ from dcim.models import Device, Interface, Region, Site, SiteGroup
from extras.filters import TagFilter
from netbox.filtersets import OrganizationalModelFilterSet, PrimaryModelFilterSet
from tenancy.filtersets import TenancyFilterSet
from tenancy.models import Tenant
from utilities.filters import (
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
)
@ -19,6 +20,7 @@ from .models import *
__all__ = (
'AggregateFilterSet',
'ASNFilterSet',
'IPAddressFilterSet',
'IPRangeFilterSet',
'PrefixFilterSet',
@ -31,6 +33,8 @@ __all__ = (
'VRFFilterSet',
)
from .models import ASN
class VRFFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
q = django_filters.CharFilter(
@ -174,6 +178,41 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
return queryset.none()
class ASNFilterSet(OrganizationalModelFilterSet, TenancyFilterSet):
rir_id = django_filters.ModelMultipleChoiceFilter(
queryset=RIR.objects.all(),
label='RIR (ID)',
)
rir = django_filters.ModelMultipleChoiceFilter(
field_name='rir__slug',
queryset=RIR.objects.all(),
to_field_name='slug',
label='RIR (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
field_name='sites',
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
field_name='sites__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = ASN
fields = ['id', 'asn']
def search(self, queryset, name, value):
if not value.strip():
return queryset
qs_filter = Q(Q(description__icontains=value) | Q(asn__icontains=value))
return queryset.filter(qs_filter)
class RoleFilterSet(OrganizationalModelFilterSet):
q = django_filters.CharFilter(
method='search',

View File

@ -5,14 +5,16 @@ from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
from ipam.choices import *
from ipam.constants import *
from ipam.models import *
from ipam.models import ASN
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, DatePicker, DynamicModelChoiceField, NumericArrayField,
StaticSelect,
StaticSelect, DynamicModelMultipleChoiceField,
)
__all__ = (
'AggregateBulkEditForm',
'ASNBulkEditForm',
'IPAddressBulkEditForm',
'IPRangeBulkEditForm',
'PrefixBulkEditForm',
@ -89,6 +91,38 @@ class RIRBulkEditForm(BootstrapMixin, CustomFieldModelBulkEditForm):
nullable_fields = ['is_private', 'description']
class ASNBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=ASN.objects.all(),
widget=forms.MultipleHiddenInput()
)
sites = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False
)
rir = DynamicModelChoiceField(
queryset=RIR.objects.all(),
required=False,
label='RIR'
)
tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(),
required=False
)
description = forms.CharField(
max_length=100,
required=False
)
class Meta:
nullable_fields = [
'date_added', 'description',
]
widgets = {
'date_added': DatePicker(),
}
class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=Aggregate.objects.all(),

View File

@ -6,12 +6,14 @@ from extras.forms import CustomFieldModelCSVForm
from ipam.choices import *
from ipam.constants import *
from ipam.models import *
from ipam.models import ASN
from tenancy.models import Tenant
from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField
from virtualization.models import VirtualMachine, VMInterface
__all__ = (
'AggregateCSVForm',
'ASNCSVForm',
'IPAddressCSVForm',
'IPRangeCSVForm',
'PrefixCSVForm',
@ -80,6 +82,31 @@ class AggregateCSVForm(CustomFieldModelCSVForm):
fields = ('prefix', 'rir', 'tenant', 'date_added', 'description')
class ASNCSVForm(CustomFieldModelCSVForm):
slug = SlugField()
rir = CSVModelChoiceField(
queryset=RIR.objects.all(),
to_field_name='name',
help_text='Assigned RIR'
)
sites = CSVModelChoiceField(
queryset=Site.objects.all(),
to_field_name='name',
help_text='Assigned site'
)
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text='Assigned tenant'
)
class Meta:
model = ASN
fields = ('asn', 'rir', 'tenant', 'description')
help_texts = {}
class RoleCSVForm(CustomFieldModelCSVForm):
slug = SlugField()

View File

@ -6,7 +6,9 @@ from extras.forms import CustomFieldModelFilterForm
from ipam.choices import *
from ipam.constants import *
from ipam.models import *
from ipam.models import ASN
from tenancy.forms import TenancyFilterForm
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect,
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
@ -14,6 +16,7 @@ from utilities.forms import (
__all__ = (
'AggregateFilterForm',
'ASNFilterForm',
'IPAddressFilterForm',
'IPRangeFilterForm',
'PrefixFilterForm',
@ -136,6 +139,33 @@ class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil
tag = TagFilterField(model)
class ASNFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
model = ASN
field_groups = [
['q'],
['rir_id'],
['tenant_group_id', 'tenant_id'],
['site_id'],
]
q = forms.CharField(
required=False,
widget=forms.TextInput(attrs={'placeholder': _('All Fields')}),
label=_('Search')
)
rir_id = DynamicModelMultipleChoiceField(
queryset=RIR.objects.all(),
required=False,
label=_('RIR'),
fetch_trigger='open'
)
site_id = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
required=False,
label=_('Site'),
fetch_trigger='open'
)
class RoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
model = Role
field_groups = [

View File

@ -6,6 +6,7 @@ from extras.forms import CustomFieldModelForm
from extras.models import Tag
from ipam.constants import *
from ipam.models import *
from ipam.models import ASN
from tenancy.forms import TenancyForm
from utilities.forms import (
BootstrapMixin, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
@ -15,6 +16,7 @@ from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInter
__all__ = (
'AggregateForm',
'ASNForm',
'IPAddressAssignForm',
'IPAddressBulkAddForm',
'IPAddressForm',
@ -118,6 +120,30 @@ class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
}
class ASNForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
rir = DynamicModelChoiceField(
queryset=RIR.objects.all(),
label='RIR',
)
class Meta:
model = ASN
fields = [
'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description'
]
fieldsets = (
('ASN', ('asn', 'rir', 'sites', 'description')),
('Tenancy', ('tenant_group', 'tenant')),
)
help_texts = {
'asn': "AS number",
'rir': "Regional Internet Registry responsible for this prefix",
}
widgets = {
'date_added': DatePicker(),
}
class RoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()

View File

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

View File

@ -8,6 +8,7 @@ from django.db.models import F, Q
from django.urls import reverse
from django.utils.functional import cached_property
from dcim.fields import ASNField
from dcim.models import Device
from extras.utils import extras_features
from netbox.models import OrganizationalModel, PrimaryModel
@ -23,6 +24,7 @@ from virtualization.models import VirtualMachine
__all__ = (
'Aggregate',
'ASN',
'IPAddress',
'IPRange',
'Prefix',
@ -69,6 +71,52 @@ class RIR(OrganizationalModel):
return reverse('ipam:rir', args=[self.pk])
class ASN(PrimaryModel):
asn = ASNField(
blank=True,
null=True,
verbose_name='ASN',
help_text='32-bit autonomous system number'
)
description = models.CharField(
max_length=200,
blank=True
)
rir = models.ForeignKey(
to='ipam.RIR',
on_delete=models.PROTECT,
related_name='asns',
blank=False,
null=False
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='asns',
blank=True,
null=True
)
sites = models.ManyToManyField(
to='dcim.Site',
related_name='asns',
blank=True
)
objects = RestrictedQuerySet.as_manager()
class Meta:
ordering = ['asn']
verbose_name = 'ASN'
verbose_name_plural = 'ASNs'
def __str__(self):
return f'AS{self.asn}'
def get_absolute_url(self):
return reverse('ipam:asn', args=[self.pk])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class Aggregate(PrimaryModel):
"""

View File

@ -2,6 +2,7 @@ import django_tables2 as tables
from django.utils.safestring import mark_safe
from django_tables2.utils import Accessor
from ipam.models import ASN
from tenancy.tables import TenantColumn
from utilities.tables import (
BaseTable, BooleanColumn, ButtonsColumn, ChoiceFieldColumn, LinkedCountColumn, TagColumn,
@ -11,6 +12,7 @@ from ipam.models import *
__all__ = (
'AggregateTable',
'ASNTable',
'InterfaceIPAddressTable',
'IPAddressAssignTable',
'IPAddressTable',
@ -93,6 +95,29 @@ class RIRTable(BaseTable):
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
#
# RIRs
#
class ASNTable(BaseTable):
pk = ToggleColumn()
asn = tables.Column(
linkify=True
)
site_count = LinkedCountColumn(
viewname='dcim:site_list',
url_params={'asn_id': 'pk'},
verbose_name='Sites'
)
actions = ButtonsColumn(ASN)
class Meta(BaseTable.Meta):
model = ASN
fields = ('pk', 'asn', 'rir', 'site_count', 'tenant', 'description', 'actions')
default_columns = ('pk', 'asn', 'rir', 'site_count', 'sites', 'tenant', 'actions')
#
# Aggregates
#

View File

@ -20,6 +20,38 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
class ASNTest(APIViewTestCases.APIViewTestCase):
model = ASN
brief_fields = ['display', 'id', 'name', 'prefix_count', 'rd', 'url']
create_data = [
{
'name': 'VRF 4',
'rd': '65000:4',
},
{
'name': 'VRF 5',
'rd': '65000:5',
},
{
'name': 'VRF 6',
'rd': '65000:6',
},
]
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
vrfs = (
VRF(name='VRF 1', rd='65000:1'),
VRF(name='VRF 2', rd='65000:2'),
VRF(name='VRF 3'), # No RD
)
VRF.objects.bulk_create(vrfs)
class VRFTest(APIViewTestCases.APIViewTestCase):
model = VRF
brief_fields = ['display', 'id', 'name', 'prefix_count', 'rd', 'url']

View File

@ -7,6 +7,18 @@ from .models import *
app_name = 'ipam'
urlpatterns = [
# ASNs
path('asns/', views.ASNListView.as_view(), name='asn_list'),
path('asns/add/', views.ASNEditView.as_view(), name='asn_add'),
path('asns/import/', views.ASNBulkImportView.as_view(), name='asn_import'),
path('asns/edit/', views.ASNBulkEditView.as_view(), name='asn_bulk_edit'),
path('asns/delete/', views.ASNBulkDeleteView.as_view(), name='asn_bulk_delete'),
path('asns/<int:pk>/', views.ASNView.as_view(), name='asn'),
path('asns/<int:pk>/edit/', views.ASNEditView.as_view(), name='asn_edit'),
path('asns/<int:pk>/delete/', views.ASNDeleteView.as_view(), name='asn_delete'),
path('asns/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='asn_changelog', kwargs={'model': ASN}),
path('asns/<int:pk>/journal/', ObjectJournalView.as_view(), name='asn_journal', kwargs={'model': ASN}),
# VRFs
path('vrfs/', views.VRFListView.as_view(), name='vrf_list'),
path('vrfs/add/', views.VRFEditView.as_view(), name='vrf_add'),

View File

@ -2,7 +2,8 @@ from django.db.models import Prefetch
from django.db.models.expressions import RawSQL
from django.shortcuts import get_object_or_404, redirect, render
from dcim.models import Device, Interface
from dcim.models import Device, Interface, Site
from dcim.tables import SiteTable
from netbox.views import generic
from utilities.forms import TableConfigForm
from utilities.tables import paginate_table
@ -11,6 +12,7 @@ from virtualization.models import VirtualMachine, VMInterface
from . import filtersets, forms, tables
from .constants import *
from .models import *
from .models import ASN
from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
@ -195,6 +197,65 @@ class RIRBulkDeleteView(generic.BulkDeleteView):
table = tables.RIRTable
#
# ASNs
#
class ASNListView(generic.ObjectListView):
queryset = ASN.objects.annotate(
site_count=count_related(Site, 'asns'),
)
filterset = filtersets.ASNFilterSet
filterset_form = forms.ASNFilterForm
table = tables.ASNTable
class ASNView(generic.ObjectView):
queryset = ASN.objects.all()
def get_extra_context(self, request, instance):
sites_table = SiteTable(
list(instance.sites.all()),
orderable=False
)
return {
'sites_table': sites_table,
}
class ASNEditView(generic.ObjectEditView):
queryset = ASN.objects.all()
model_form = forms.ASNForm
class ASNDeleteView(generic.ObjectDeleteView):
queryset = ASN.objects.all()
class ASNBulkImportView(generic.BulkImportView):
queryset = ASN.objects.all()
model_form = forms.ASNCSVForm
table = tables.ASNTable
class ASNBulkEditView(generic.BulkEditView):
queryset = ASN.objects.annotate(
site_count=count_related(Site, 'asns')
)
filterset = filtersets.ASNFilterSet
table = tables.ASNTable
form = forms.ASNBulkEditForm
class ASNBulkDeleteView(generic.BulkDeleteView):
queryset = ASN.objects.annotate(
site_count=count_related(Site, 'asns')
)
filterset = filtersets.ASNFilterSet
table = tables.ASNTable
#
# Aggregates
#

View File

@ -153,7 +153,6 @@ class BaseFilterSet(django_filters.FilterSet):
# The filter field has been explicity defined on the filterset class so we must manually
# create the new filter with the same type because there is no guarantee the defined type
# is the same as the default type for the field
resolve_field(field, lookup_expr) # Will raise FieldLookupError if the lookup is invalid
new_filter = type(existing_filter)(
field_name=field_name,
lookup_expr=lookup_expr,

View File

@ -214,6 +214,12 @@ IPAM_MENU = Menu(
get_model_item('ipam', 'role', 'Prefix & VLAN Roles'),
),
),
MenuGroup(
label='ASNs',
items=(
get_model_item('ipam', 'asn', 'ASNs'),
),
),
MenuGroup(
label='Aggregates',
items=(

View File

@ -80,10 +80,6 @@
<th scope="row">Description</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">AS Number</th>
<td>{{ object.asn|placeholder }}</td>
</tr>
<tr>
<th scope="row">Time Zone</th>
<td>
@ -216,6 +212,10 @@
<h2><a href="{% url 'virtualization:virtualmachine_list' %}?site_id={{ object.pk }}" class="btn {% if stats.vm_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vm_count }}</a></h2>
<p>Virtual Machines</p>
</div>
<div class="col col-md-4 text-center">
<h2><a href="{% url 'ipam:asn_list' %}?site_id={{ object.pk }}" class="btn {% if stats.asn_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.asn_count }}</a></h2>
<p>ASNs</p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,64 @@
{% extends 'generic/object.html' %}
{% load buttons %}
{% load helpers %}
{% load plugins %}
{% block breadcrumbs %}
{{ block.super }}
<li class="breadcrumb-item"><a href="{% url 'ipam:asn_list' %}?rir_id={{ object.rir.pk }}">{{ object.rir }}</a></li>
{% endblock %}
{% block content %}
<div class="row">
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
ASN
</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<td>AS Number</td>
<td>{{ object.asn }}</td>
</tr>
<tr>
<td>RIR</td>
<td>
<a href="{% url 'ipam:asn_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
</td>
</tr>
<tr>
<td>Tenant</td>
<td>
{% if object.tenant %}
{% if prefix.object.group %}
<a href="{{ object.tenant.group.get_absolute_url }}">{{ object.tenant.group }}</a> /
{% endif %}
<a href="{{ object.tenant.get_absolute_url }}">{{ object.tenant }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Description</td>
<td>{{ object.description|placeholder }}</td>
</tr>
</table>
</div>
</div>
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:asn_list' %}
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% include 'inc/panel_table.html' with table=sites_table heading='Sites' %}
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}