Merge pull request #7662 from netbox-community/6732-asn-model

Closes: #6732 - Add new ASN model
This commit is contained in:
Jeremy Stretch 2021-11-03 14:15:24 -04:00 committed by GitHub
commit 8305f6d1f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 902 additions and 39 deletions

View File

@ -18,6 +18,10 @@
{!models/ipam/vrf.md!}
{!models/ipam/routetarget.md!}
__
---
{!models/ipam/fhrpgroup.md!}
---
{!models/ipam/asn.md!}

15
docs/models/ipam/asn.md Normal file
View File

@ -0,0 +1,15 @@
# ASN
ASN is short for Autonomous System Number. This identifier is used in the BGP protocol to identify which "autonomous system" a particular prefix is originating and transiting through.
The AS number model within NetBox allows you to model some of this real-world relationship.
Within NetBox:
* AS numbers are globally unique
* Each AS number must be associated with a RIR (ARIN, RFC 6996, etc)
* Each AS number can be associated with many different sites
* Each site can have many different AS numbers
* Each AS number can be assigned to a single tenant

View File

@ -7,7 +7,7 @@ from timezone_field.rest_framework import TimeZoneSerializerField
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedASNSerializer
from ipam.models import VLAN
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
from netbox.api.serializers import (
@ -113,21 +113,24 @@ class SiteSerializer(PrimaryModelSerializer):
region = NestedRegionSerializer(required=False, allow_null=True)
group = NestedSiteGroupSerializer(required=False, allow_null=True)
tenant = NestedTenantSerializer(required=False, allow_null=True)
asns = NestedASNSerializer(many=True, required=False, allow_null=True)
time_zone = TimeZoneSerializerField(required=False)
circuit_count = serializers.IntegerField(read_only=True)
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', 'asn', '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

@ -15,7 +15,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,
)
@ -130,6 +131,17 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
to_field_name='slug',
label='Group (slug)',
)
asns_id = django_filters.ModelMultipleChoiceFilter(
field_name='asns',
queryset=ASN.objects.all(),
label='AS (ID)',
)
asns = django_filters.ModelMultipleChoiceFilter(
field_name='asns__asn',
queryset=ASN.objects.all(),
to_field_name='asn',
label='AS (Number)',
)
tag = TagFilter()
class Meta:
@ -155,6 +167,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
)
try:
qs_filter |= Q(asn=int(value.strip()))
qs_filter |= Q(asns__asn=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,8 @@ 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.constants import BGP_ASN_MIN, BGP_ASN_MAX
from ipam.models import VLAN, ASN
from tenancy.models import Tenant
from utilities.forms import (
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField,
@ -116,6 +117,11 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
required=False,
label='ASN'
)
asns = DynamicModelMultipleChoiceField(
queryset=ASN.objects.all(),
label=_('ASNs'),
required=False
)
description = forms.CharField(
max_length=100,
required=False
@ -128,7 +134,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
class Meta:
nullable_fields = [
'region', 'group', 'tenant', 'asn', 'description', 'time_zone',
'region', 'group', 'tenant', 'asn', 'asns', 'description', 'time_zone',
]

View File

@ -95,7 +95,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,
@ -138,11 +139,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', 'asn_id']
field_groups = [
['q', 'tag'],
['status', 'region_id', 'group_id'],
['tenant_group_id', 'tenant_id'],
['asn_id']
]
q = forms.CharField(
required=False,
@ -166,6 +168,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,
@ -110,6 +111,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),
@ -125,13 +131,14 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
class Meta:
model = Site
fields = [
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone',
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', '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', 'asn', 'asns', 'time_zone', 'description',
'tags',
)),
('Tenancy', ('tenant_group', 'tenant')),
('Contact Info', (
@ -155,8 +162,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
}
help_texts = {
'name': "Full name of the site",
'asn': "BGP autonomous system number. This field is depreciated in favour of the ASN model",
'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)",
@ -791,7 +798,6 @@ class VirtualChassisForm(BootstrapMixin, CustomFieldModelForm):
class DeviceVCMembershipForm(forms.ModelForm):
class Meta:
model = Device
fields = [
@ -887,7 +893,6 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsolePortTemplate
fields = [
@ -899,7 +904,6 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = ConsoleServerPortTemplate
fields = [
@ -911,7 +915,6 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = PowerPortTemplate
fields = [
@ -923,7 +926,6 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = PowerOutletTemplate
fields = [
@ -934,7 +936,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit power_port choices to current DeviceType
@ -945,7 +946,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = InterfaceTemplate
fields = [
@ -958,7 +958,6 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = FrontPortTemplate
fields = [
@ -970,7 +969,6 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Limit rear_port choices to current DeviceType
@ -981,7 +979,6 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = RearPortTemplate
fields = [
@ -994,7 +991,6 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
class Meta:
model = DeviceBayTemplate
fields = [
@ -1257,7 +1253,6 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
)
def __init__(self, device_bay, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['installed_device'].queryset = Device.objects.filter(

View File

@ -1,8 +1,11 @@
import graphene
from dcim import filtersets, models
from extras.graphql.mixins import (
ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
)
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType
__all__ = (
@ -380,6 +383,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
asn = graphene.Field(BigInt)
class Meta:
model = models.Site

View File

@ -0,0 +1,19 @@
# Generated by Django 3.2.8 on 2021-11-02 16:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ipam', '0053_asn_model'),
('dcim', '0140_wireless'),
]
operations = [
migrations.AddField(
model_name='site',
name='asns',
field=models.ManyToManyField(blank=True, related_name='sites', to='ipam.ASN'),
),
]

View File

@ -195,6 +195,11 @@ class Site(PrimaryModel):
verbose_name='ASN',
help_text='32-bit autonomous system number'
)
asns = models.ManyToManyField(
to='ipam.ASN',
related_name='sites',
blank=True
)
time_zone = TimeZoneField(
blank=True
)

View File

@ -81,6 +81,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(
@ -90,11 +95,11 @@ class SiteTable(BaseTable):
class Meta(BaseTable.Meta):
model = Site
fields = (
'pk', 'id', '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', 'id', '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

@ -4,7 +4,7 @@ from django.test import TestCase
from dcim.choices import *
from dcim.filtersets import *
from dcim.models import *
from ipam.models import IPAddress
from ipam.models import ASN, IPAddress, RIR
from tenancy.models import Tenant, TenantGroup
from utilities.choices import ColorChoices
from utilities.testing import ChangeLoggedFilterSetTests
@ -149,6 +149,23 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Site.objects.bulk_create(sites)
rir = RIR.objects.create(name='RFC 6996', is_private=True)
asns = (
ASN(asn=64512, rir=rir, tenant=tenants[0]),
ASN(asn=64513, rir=rir, tenant=tenants[0]),
ASN(asn=64514, rir=rir, tenant=tenants[0]),
ASN(asn=65001, rir=rir, tenant=tenants[0]),
ASN(asn=65002, rir=rir, tenant=tenants[0])
)
ASN.objects.bulk_create(asns)
asns[0].sites.set([sites[0]])
asns[1].sites.set([sites[1]])
asns[2].sites.set([sites[2]])
asns[3].sites.set([sites[2]])
asns[4].sites.set([sites[1]])
def test_name(self):
params = {'name': ['Site 1', 'Site 2']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
@ -165,6 +182,10 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'asn': [65001, 65002]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_asns(self):
params = {'asns': [64512, 65002]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_latitude(self):
params = {'latitude': [10, 20]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

View File

@ -11,7 +11,7 @@ from netaddr import EUI
from dcim.choices import *
from dcim.constants import *
from dcim.models import *
from ipam.models import VLAN
from ipam.models import ASN, VLAN, RIR
from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_tags, create_test_device
@ -110,7 +110,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
for group in groups:
group.save()
Site.objects.bulk_create([
sites = Site.objects.bulk_create([
Site(name='Site 1', slug='site-1', region=regions[0], group=groups[1]),
Site(name='Site 2', slug='site-2', region=regions[0], group=groups[1]),
Site(name='Site 3', slug='site-3', region=regions[0], group=groups[1]),
@ -118,6 +118,33 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
tags = create_tags('Alpha', 'Bravo', 'Charlie')
rir = RIR.objects.create(name='RFC 6996', is_private=True)
asns = [
ASN(asn=65000, rir=rir),
ASN(asn=65001, rir=rir),
ASN(asn=65002, rir=rir),
ASN(asn=65003, rir=rir),
ASN(asn=65004, rir=rir),
ASN(asn=65005, rir=rir),
ASN(asn=65006, rir=rir),
ASN(asn=65007, rir=rir),
ASN(asn=65008, rir=rir),
ASN(asn=65009, rir=rir),
ASN(asn=65010, rir=rir),
]
ASN.objects.bulk_create(asns)
asns[0].sites.set([sites[0]])
asns[2].sites.set([sites[0]])
asns[3].sites.set([sites[1]])
asns[4].sites.set([sites[2]])
asns[5].sites.set([sites[1]])
asns[6].sites.set([sites[2]])
asns[7].sites.set([sites[2]])
asns[8].sites.set([sites[2]])
asns[10].sites.set([sites[0]])
cls.form_data = {
'name': 'Site X',
'slug': 'site-x',
@ -126,7 +153,6 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'group': groups[1].pk,
'tenant': None,
'facility': 'Facility X',
'asn': 65001,
'time_zone': pytz.UTC,
'description': 'Site description',
'physical_address': '742 Evergreen Terrace, Springfield, USA',
@ -152,7 +178,6 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'region': regions[1].pk,
'group': groups[1].pk,
'tenant': None,
'asn': 65009,
'time_zone': pytz.timezone('US/Eastern'),
'description': 'New 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 ASN, IPAddress, Prefix, Service, VLAN
from ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
from netbox.views import generic
from utilities.forms import ConfirmationForm
@ -332,9 +332,15 @@ class SiteView(generic.ObjectView):
cumulative=True
).restrict(request.user, 'view').filter(site=instance)
asns = ASN.objects.restrict(request.user, 'view').filter(sites=instance)
asn_count = asns.count()
stats.update({'asn_count': asn_count})
return {
'stats': stats,
'locations': locations,
'asns': asns,
}

View File

@ -5,6 +5,7 @@ from netbox.api import WritableNestedSerializer
__all__ = [
'NestedAggregateSerializer',
'NestedASNSerializer',
'NestedFHRPGroupSerializer',
'NestedIPAddressSerializer',
'NestedIPRangeSerializer',
@ -19,6 +20,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

@ -16,6 +16,24 @@ from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
from .nested_serializers import *
#
# ASNs
#
class ASNSerializer(PrimaryModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='ipam-api:asn-detail')
tenant = NestedTenantSerializer(required=False, allow_null=True)
site_count = serializers.IntegerField(read_only=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,5 +1,6 @@
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 *
@ -16,6 +17,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 ChangeLoggedModelFilterSet, 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',
'FHRPGroupAssignmentFilterSet',
'FHRPGroupFilterSet',
'IPAddressFilterSet',
@ -177,6 +179,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',
'FHRPGroupBulkEditForm',
'IPAddressBulkEditForm',
'IPRangeBulkEditForm',
@ -90,6 +92,38 @@ class RIRBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEdi
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

@ -12,6 +12,7 @@ from virtualization.models import VirtualMachine, VMInterface
__all__ = (
'AggregateCSVForm',
'ASNCSVForm',
'FHRPGroupCSVForm',
'IPAddressCSVForm',
'IPRangeCSVForm',
@ -81,6 +82,25 @@ class AggregateCSVForm(CustomFieldModelCSVForm):
fields = ('prefix', 'rir', 'tenant', 'date_added', 'description')
class ASNCSVForm(CustomFieldModelCSVForm):
rir = CSVModelChoiceField(
queryset=RIR.objects.all(),
to_field_name='name',
help_text='Assigned RIR'
)
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',
'FHRPGroupFilterForm',
'IPAddressFilterForm',
'IPRangeFilterForm',
@ -134,6 +137,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
q = forms.CharField(

View File

@ -8,6 +8,7 @@ from ipam.choices import *
from ipam.constants import *
from ipam.formfields import IPNetworkFormField
from ipam.models import *
from ipam.models import ASN
from tenancy.forms import TenancyForm
from utilities.exceptions import PermissionsViolation
from utilities.forms import (
@ -18,6 +19,7 @@ from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInter
__all__ = (
'AggregateForm',
'ASNForm',
'FHRPGroupForm',
'FHRPGroupAssignmentForm',
'IPAddressAssignForm',
@ -127,6 +129,50 @@ class AggregateForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
}
class ASNForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
rir = DynamicModelChoiceField(
queryset=RIR.objects.all(),
label='RIR',
)
sites = DynamicModelMultipleChoiceField(
queryset=Site.objects.all(),
label='Sites',
required=False
)
tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(),
required=False
)
class Meta:
model = ASN
fields = [
'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'tags'
]
fieldsets = (
('ASN', ('asn', 'rir', 'sites', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
)
help_texts = {
'asn': "AS number",
'rir': "Regional Internet Registry responsible for this prefix",
}
widgets = {
'date_added': DatePicker(),
}
def __init__(self, data=None, instance=None, *args, **kwargs):
super().__init__(data=data, instance=instance, *args, **kwargs)
if self.instance and self.instance.pk is not None:
self.fields['sites'].initial = self.instance.sites.all().values_list('id', flat=True)
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
instance.sites.set(self.cleaned_data['sites'])
return instance
class RoleForm(BootstrapMixin, CustomFieldModelForm):
slug = SlugField()
tags = DynamicModelMultipleChoiceField(

View File

@ -5,6 +5,9 @@ from .types import *
class IPAMQuery(graphene.ObjectType):
asn = ObjectField(ASNType)
asn_list = ObjectListField(ASNType)
aggregate = ObjectField(AggregateType)
aggregate_list = ObjectListField(AggregateType)

View File

@ -1,7 +1,11 @@
import graphene
from ipam import filtersets, models
from netbox.graphql.scalars import BigInt
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
__all__ = (
'ASNType',
'AggregateType',
'FHRPGroupType',
'FHRPGroupAssignmentType',
@ -18,6 +22,15 @@ __all__ = (
)
class ASNType(PrimaryObjectType):
asn = graphene.Field(BigInt)
class Meta:
model = models.ASN
fields = '__all__'
filterset_class = filtersets.ASNFilterSet
class AggregateType(PrimaryObjectType):
class Meta:

View File

@ -0,0 +1,38 @@
# Generated by Django 3.2.8 on 2021-11-02 16:16
import dcim.fields
import django.core.serializers.json
from django.db import migrations, models
import django.db.models.deletion
import taggit.managers
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0004_extend_tag_support'),
('extras', '0064_configrevision'),
('ipam', '0052_fhrpgroup'),
]
operations = [
migrations.CreateModel(
name='ASN',
fields=[
('created', models.DateField(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=django.core.serializers.json.DjangoJSONEncoder)),
('id', models.BigAutoField(primary_key=True, serialize=False)),
('asn', dcim.fields.ASNField(unique=True)),
('description', models.CharField(blank=True, max_length=200)),
('rir', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='asns', to='ipam.rir')),
('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='asns', to='tenancy.tenant')),
],
options={
'verbose_name': 'ASN',
'verbose_name_plural': 'ASNs',
'ordering': ['asn'],
},
),
]

View File

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

View File

@ -7,6 +7,7 @@ from django.db.models import F
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,49 @@ class RIR(OrganizationalModel):
return reverse('ipam:rir', args=[self.pk])
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
class ASN(PrimaryModel):
asn = ASNField(
unique=True,
blank=False,
null=False,
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
)
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

@ -11,6 +11,7 @@ from ipam.models import *
__all__ = (
'AggregateTable',
'ASNTable',
'AssignedIPAddressesTable',
'IPAddressAssignTable',
'IPAddressTable',
@ -96,6 +97,28 @@ class RIRTable(BaseTable):
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
#
# ASNs
#
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

@ -7,6 +7,7 @@ from rest_framework import status
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
from ipam.choices import *
from ipam.models import *
from tenancy.models import Tenant
from utilities.testing import APITestCase, APIViewTestCases, disable_warnings
@ -20,6 +21,58 @@ class AppTest(APITestCase):
self.assertEqual(response.status_code, 200)
class ASNTest(APIViewTestCases.APIViewTestCase):
model = ASN
brief_fields = ['asn', 'display', 'id', 'url']
bulk_update_data = {
'description': 'New description',
}
@classmethod
def setUpTestData(cls):
rirs = [
RIR.objects.create(name='RFC 6996', slug='rfc-6996', description='Private Use', is_private=True),
RIR.objects.create(name='RFC 7300', slug='rfc-7300', description='IANA Use', is_private=True),
]
sites = [
Site.objects.create(name='Site 1', slug='site-1'),
Site.objects.create(name='Site 2', slug='site-2')
]
tenants = [
Tenant.objects.create(name='Tenant 1', slug='tenant-1'),
Tenant.objects.create(name='Tenant 2', slug='tenant-2'),
]
asns = (
ASN(asn=64513, rir=rirs[0], tenant=tenants[0]),
ASN(asn=65534, rir=rirs[0], tenant=tenants[1]),
ASN(asn=4200000000, rir=rirs[0], tenant=tenants[0]),
ASN(asn=4200002301, rir=rirs[1], tenant=tenants[1]),
)
ASN.objects.bulk_create(asns)
asns[0].sites.set([sites[0]])
asns[1].sites.set([sites[1]])
asns[2].sites.set([sites[0]])
asns[3].sites.set([sites[1]])
cls.create_data = [
{
'asn': 64512,
'rir': rirs[0].pk,
},
{
'asn': 65543,
'rir': rirs[0].pk,
},
{
'asn': 4294967294,
'rir': rirs[0].pk,
},
]
class VRFTest(APIViewTestCases.APIViewTestCase):
model = VRF
brief_fields = ['display', 'id', 'name', 'prefix_count', 'rd', 'url']

View File

@ -9,6 +9,83 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMac
from tenancy.models import Tenant, TenantGroup
class ASNTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = ASN.objects.all()
filterset = ASNFilterSet
@classmethod
def setUpTestData(cls):
rirs = [
RIR.objects.create(name='RFC 6996', slug='rfc-6996', description='Private Use', is_private=True),
RIR.objects.create(name='RFC 7300', slug='rfc-7300', description='IANA Use', is_private=True),
]
sites = [
Site.objects.create(name='Site 1', slug='site-1'),
Site.objects.create(name='Site 2', slug='site-2'),
Site.objects.create(name='Site 3', slug='site-3')
]
tenants = [
Tenant.objects.create(name='Tenant 1', slug='tenant-1'),
Tenant.objects.create(name='Tenant 2', slug='tenant-2'),
Tenant.objects.create(name='Tenant 3', slug='tenant-3'),
Tenant.objects.create(name='Tenant 4', slug='tenant-4'),
Tenant.objects.create(name='Tenant 5', slug='tenant-5'),
]
asns = (
ASN(asn=64512, rir=rirs[0], tenant=tenants[0]),
ASN(asn=64513, rir=rirs[0], tenant=tenants[0]),
ASN(asn=64514, rir=rirs[0], tenant=tenants[1]),
ASN(asn=64515, rir=rirs[0], tenant=tenants[2]),
ASN(asn=64516, rir=rirs[0], tenant=tenants[3]),
ASN(asn=65535, rir=rirs[1], tenant=tenants[4]),
ASN(asn=4200000000, rir=rirs[0], tenant=tenants[0]),
ASN(asn=4200000001, rir=rirs[0], tenant=tenants[1]),
ASN(asn=4200000002, rir=rirs[0], tenant=tenants[2]),
ASN(asn=4200000003, rir=rirs[0], tenant=tenants[3]),
ASN(asn=4200002301, rir=rirs[1], tenant=tenants[4]),
)
ASN.objects.bulk_create(asns)
asns[0].sites.set([sites[0]])
asns[1].sites.set([sites[0]])
asns[2].sites.set([sites[1]])
asns[3].sites.set([sites[2]])
asns[4].sites.set([sites[0]])
asns[5].sites.set([sites[1]])
asns[6].sites.set([sites[0]])
asns[7].sites.set([sites[1]])
asns[8].sites.set([sites[2]])
asns[9].sites.set([sites[0]])
asns[10].sites.set([sites[1]])
def test_asn(self):
params = {'asn': ['64512', '65535']}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 5)
def test_rir(self):
rirs = RIR.objects.all()[:1]
params = {'rir_id': [rirs[0].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
params = {'rir': [rirs[0].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
def test_site(self):
sites = Site.objects.all()[:2]
params = {'site_id': [sites[0].pk, sites[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 9)
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VRF.objects.all()
filterset = VRFFilterSet

View File

@ -9,6 +9,61 @@ from tenancy.models import Tenant
from utilities.testing import ViewTestCases, create_tags
class ASNTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = ASN
@classmethod
def setUpTestData(cls):
rirs = [
RIR.objects.create(name='RFC 6996', slug='rfc-6996', description='Private Use', is_private=True),
RIR.objects.create(name='RFC 7300', slug='rfc-7300', description='IANA Use', is_private=True),
]
sites = [
Site.objects.create(name='Site 1', slug='site-1'),
Site.objects.create(name='Site 2', slug='site-2')
]
tenants = [
Tenant.objects.create(name='Tenant 1', slug='tenant-1'),
Tenant.objects.create(name='Tenant 2', slug='tenant-2'),
]
asns = (
ASN(asn=64513, rir=rirs[0], tenant=tenants[0]),
ASN(asn=65535, rir=rirs[1], tenant=tenants[1]),
ASN(asn=4200000000, rir=rirs[0], tenant=tenants[0]),
ASN(asn=4200002301, rir=rirs[1], tenant=tenants[1]),
)
ASN.objects.bulk_create(asns)
asns[0].sites.set([sites[0]])
asns[1].sites.set([sites[1]])
asns[2].sites.set([sites[0]])
asns[3].sites.set([sites[1]])
tags = create_tags('Alpha', 'Bravo', 'Charlie')
cls.form_data = {
'asn': 64512,
'rir': rirs[0].pk,
'tenant': tenants[0].pk,
'site': sites[0].pk,
'description': 'A new ASN',
}
cls.csv_data = (
"asn,rir",
"64533,RFC 6996",
"64523,RFC 6996",
"4200000002,RFC 6996",
)
cls.bulk_edit_data = {
'rir': rirs[1].pk,
'description': 'Next description',
}
class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
model = VRF

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

@ -5,7 +5,8 @@ from django.http import Http404
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
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.tables import paginate_table
from utilities.utils import count_related
@ -13,6 +14,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
@ -197,6 +199,62 @@ 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 = instance.sites.restrict(request.user, 'view').all()
return {
'sites': sites,
}
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

@ -0,0 +1,23 @@
from graphene import Scalar
from graphql.language import ast
from graphql.type.scalars import MAX_INT, MIN_INT
class BigInt(Scalar):
"""
Handle any BigInts
"""
@staticmethod
def to_float(value):
num = int(value)
if num > MAX_INT or num < MIN_INT:
return float(num)
return num
serialize = to_float
parse_value = to_float
@staticmethod
def parse_literal(node):
if isinstance(node, ast.IntValue):
return BigInt.to_float(node.value)

View File

@ -229,6 +229,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

@ -215,6 +215,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>
@ -256,6 +260,20 @@
{% endif %}
</div>
</div>
<div class="card">
<h5 class="card-header">
ASNs
</h5>
<div class='card-body'>
{% if asns %}
{% for asn in asns %}
<a href="{{ asn.get_absolute_url }}"><span class="badge bg-primary">{{ asn }}</span></a>
{% endfor %}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
{% include 'inc/panels/image_attachments.html' %}
{% plugin_right_page object %}
</div>

View File

@ -0,0 +1,77 @@
{% 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>
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/tags.html' with tags=object.tags.all url='ipam:asn_list' %}
{% plugin_left_page object %}
</div>
<div class="col col-md-6">
<div class="card">
<h5 class="card-header">
Sites
</h5>
<div class='card-body'>
{% if sites %}
{% for site in sites %}
<a href="{{ site.get_absolute_url }}"><span class="badge bg-primary">{{ site }}</span></a>
{% endfor %}
{% else %}
<span class="text-muted">None</span>
{% endif %}
</div>
</div>
{% plugin_right_page object %}
</div>
</div>
<div class="row mb-3">
<div class="col col-md-12">
{% plugin_full_width_page object %}
</div>
</div>
{% endblock %}

View File

@ -5,6 +5,9 @@ from django.test import TestCase
from mptt.fields import TreeForeignKey
from taggit.managers import TaggableManager
from circuits.choices import CircuitStatusChoices
from circuits.filtersets import CircuitFilterSet
from circuits.models import Circuit, Provider, CircuitType
from dcim.choices import *
from dcim.fields import MACAddressField
from dcim.filtersets import DeviceFilterSet, SiteFilterSet
@ -13,6 +16,7 @@ from dcim.models import (
)
from extras.filters import TagFilter
from extras.models import TaggedItem
from ipam.models import RIR, ASN
from netbox.filtersets import BaseFilterSet
from utilities.filters import (
MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter, MultiValueNumberFilter,
@ -337,6 +341,8 @@ class DynamicFilterLookupExpressionTest(TestCase):
device_filterset = DeviceFilterSet
site_queryset = Site.objects.all()
site_filterset = SiteFilterSet
circuit_queryset = Circuit.objects.all()
circuit_filterset = CircuitFilterSet
@classmethod
def setUpTestData(cls):
@ -384,6 +390,19 @@ class DynamicFilterLookupExpressionTest(TestCase):
)
Site.objects.bulk_create(sites)
rir = RIR.objects.create(name='RFC 6996', is_private=True)
asns = [
ASN(asn=65001, rir=rir),
ASN(asn=65101, rir=rir),
ASN(asn=65201, rir=rir)
]
ASN.objects.bulk_create(asns)
asns[0].sites.add(sites[0])
asns[1].sites.add(sites[1])
asns[2].sites.add(sites[2])
racks = (
Rack(name='Rack 1', site=sites[0]),
Rack(name='Rack 2', site=sites[1]),