mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-16 12:12:53 -06:00
Initial work on #6732
This commit is contained in:
parent
8c058dcd45
commit
a01068949c
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
@ -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',
|
||||
)
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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)",
|
||||
|
@ -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',
|
||||
]
|
||||
|
||||
|
@ -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')
|
||||
|
||||
|
||||
#
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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',
|
||||
|
@ -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(),
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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 = [
|
||||
|
@ -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()
|
||||
|
||||
|
@ -4,6 +4,7 @@ from .vlans import *
|
||||
from .vrfs import *
|
||||
|
||||
__all__ = (
|
||||
'ASN',
|
||||
'Aggregate',
|
||||
'IPAddress',
|
||||
'IPRange',
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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']
|
||||
|
@ -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'),
|
||||
|
@ -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
|
||||
#
|
||||
|
@ -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,
|
||||
|
@ -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=(
|
||||
|
@ -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>
|
||||
|
64
netbox/templates/ipam/asn.html
Normal file
64
netbox/templates/ipam/asn.html
Normal 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 %}
|
Loading…
Reference in New Issue
Block a user