mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-20 02:06:42 -06:00
Merge pull request #7662 from netbox-community/6732-asn-model
Closes: #6732 - Add new ASN model
This commit is contained in:
commit
8305f6d1f5
@ -18,6 +18,10 @@
|
|||||||
{!models/ipam/vrf.md!}
|
{!models/ipam/vrf.md!}
|
||||||
{!models/ipam/routetarget.md!}
|
{!models/ipam/routetarget.md!}
|
||||||
|
|
||||||
__
|
---
|
||||||
|
|
||||||
{!models/ipam/fhrpgroup.md!}
|
{!models/ipam/fhrpgroup.md!}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{!models/ipam/asn.md!}
|
||||||
|
15
docs/models/ipam/asn.md
Normal file
15
docs/models/ipam/asn.md
Normal 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
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ from timezone_field.rest_framework import TimeZoneSerializerField
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer
|
from ipam.api.nested_serializers import NestedIPAddressSerializer, NestedVLANSerializer, NestedASNSerializer
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import (
|
||||||
@ -113,21 +113,24 @@ class SiteSerializer(PrimaryModelSerializer):
|
|||||||
region = NestedRegionSerializer(required=False, allow_null=True)
|
region = NestedRegionSerializer(required=False, allow_null=True)
|
||||||
group = NestedSiteGroupSerializer(required=False, allow_null=True)
|
group = NestedSiteGroupSerializer(required=False, allow_null=True)
|
||||||
tenant = NestedTenantSerializer(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)
|
time_zone = TimeZoneSerializerField(required=False)
|
||||||
circuit_count = serializers.IntegerField(read_only=True)
|
circuit_count = serializers.IntegerField(read_only=True)
|
||||||
device_count = serializers.IntegerField(read_only=True)
|
device_count = serializers.IntegerField(read_only=True)
|
||||||
prefix_count = serializers.IntegerField(read_only=True)
|
prefix_count = serializers.IntegerField(read_only=True)
|
||||||
rack_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)
|
virtualmachine_count = serializers.IntegerField(read_only=True)
|
||||||
vlan_count = serializers.IntegerField(read_only=True)
|
vlan_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
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',
|
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||||
'contact_phone', 'contact_email', 'comments', 'tags', 'custom_fields', 'created', 'last_updated',
|
'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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ from circuits.models import Circuit
|
|||||||
from dcim import filtersets
|
from dcim import filtersets
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.api.views import ConfigContextQuerySetMixin, CustomFieldModelViewSet
|
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.authentication import IsAuthenticatedOrLoginNotRequired
|
||||||
from netbox.api.exceptions import ServiceUnavailable
|
from netbox.api.exceptions import ServiceUnavailable
|
||||||
from netbox.api.metadata import ContentTypeMetadata
|
from netbox.api.metadata import ContentTypeMetadata
|
||||||
@ -139,6 +139,7 @@ class SiteViewSet(CustomFieldModelViewSet):
|
|||||||
queryset = Site.objects.prefetch_related(
|
queryset = Site.objects.prefetch_related(
|
||||||
'region', 'tenant', 'tags'
|
'region', 'tenant', 'tags'
|
||||||
).annotate(
|
).annotate(
|
||||||
|
asn_count=count_related(ASN, 'sites'),
|
||||||
device_count=count_related(Device, 'site'),
|
device_count=count_related(Device, 'site'),
|
||||||
rack_count=count_related(Rack, 'site'),
|
rack_count=count_related(Rack, 'site'),
|
||||||
prefix_count=count_related(Prefix, '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.filters import TagFilter
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
|
from ipam.models import ASN
|
||||||
from netbox.filtersets import (
|
from netbox.filtersets import (
|
||||||
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet,
|
||||||
)
|
)
|
||||||
@ -130,6 +131,17 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
to_field_name='slug',
|
to_field_name='slug',
|
||||||
label='Group (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()
|
tag = TagFilter()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -155,6 +167,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
qs_filter |= Q(asn=int(value.strip()))
|
qs_filter |= Q(asn=int(value.strip()))
|
||||||
|
qs_filter |= Q(asns__asn=int(value.strip()))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
return queryset.filter(qs_filter)
|
return queryset.filter(qs_filter)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
|
|
||||||
@ -6,8 +7,8 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
||||||
from ipam.constants import BGP_ASN_MAX, BGP_ASN_MIN
|
from ipam.constants import BGP_ASN_MIN, BGP_ASN_MAX
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN, ASN
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField,
|
add_blank_choice, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField,
|
||||||
@ -116,6 +117,11 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
|||||||
required=False,
|
required=False,
|
||||||
label='ASN'
|
label='ASN'
|
||||||
)
|
)
|
||||||
|
asns = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ASN.objects.all(),
|
||||||
|
label=_('ASNs'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
description = forms.CharField(
|
description = forms.CharField(
|
||||||
max_length=100,
|
max_length=100,
|
||||||
required=False
|
required=False
|
||||||
@ -128,7 +134,7 @@ class SiteBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEd
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
nullable_fields = [
|
nullable_fields = [
|
||||||
'region', 'group', 'tenant', 'asn', 'description', 'time_zone',
|
'region', 'group', 'tenant', 'asn', 'asns', 'description', 'time_zone',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ class SiteCSVForm(CustomFieldModelCSVForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = (
|
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',
|
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
||||||
'contact_email', 'comments',
|
'contact_email', 'comments',
|
||||||
)
|
)
|
||||||
|
@ -6,6 +6,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm
|
||||||
|
from ipam.models import ASN
|
||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
APISelectMultiple, add_blank_choice, BootstrapMixin, ColorField, DynamicModelMultipleChoiceField, StaticSelect,
|
||||||
@ -138,11 +139,12 @@ class SiteGroupFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
|||||||
|
|
||||||
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterForm):
|
||||||
model = Site
|
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 = [
|
field_groups = [
|
||||||
['q', 'tag'],
|
['q', 'tag'],
|
||||||
['status', 'region_id', 'group_id'],
|
['status', 'region_id', 'group_id'],
|
||||||
['tenant_group_id', 'tenant_id'],
|
['tenant_group_id', 'tenant_id'],
|
||||||
|
['asn_id']
|
||||||
]
|
]
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
required=False,
|
required=False,
|
||||||
@ -166,6 +168,12 @@ class SiteFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFilterFo
|
|||||||
label=_('Site group'),
|
label=_('Site group'),
|
||||||
fetch_trigger='open'
|
fetch_trigger='open'
|
||||||
)
|
)
|
||||||
|
asn_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ASN.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('ASNs'),
|
||||||
|
fetch_trigger='open'
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
@ -8,7 +9,7 @@ from dcim.constants import *
|
|||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from extras.forms import CustomFieldModelForm
|
from extras.forms import CustomFieldModelForm
|
||||||
from extras.models import Tag
|
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 tenancy.forms import TenancyForm
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
|
APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, DynamicModelChoiceField,
|
||||||
@ -110,6 +111,11 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
queryset=SiteGroup.objects.all(),
|
queryset=SiteGroup.objects.all(),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
asns = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=ASN.objects.all(),
|
||||||
|
label=_('ASNs'),
|
||||||
|
required=False
|
||||||
|
)
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
time_zone = TimeZoneFormField(
|
time_zone = TimeZoneFormField(
|
||||||
choices=add_blank_choice(TimeZoneFormField().choices),
|
choices=add_blank_choice(TimeZoneFormField().choices),
|
||||||
@ -125,13 +131,14 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Site
|
model = Site
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', 'time_zone',
|
'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', 'asns',
|
||||||
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||||
'contact_phone', 'contact_email', 'comments', 'tags',
|
'contact_phone', 'contact_email', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Site', (
|
('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')),
|
('Tenancy', ('tenant_group', 'tenant')),
|
||||||
('Contact Info', (
|
('Contact Info', (
|
||||||
@ -155,8 +162,8 @@ class SiteForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
|||||||
}
|
}
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'name': "Full name of the site",
|
'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)",
|
'facility': "Data center provider and facility (e.g. Equinix NY7)",
|
||||||
'asn': "BGP autonomous system number",
|
|
||||||
'time_zone': "Local time zone",
|
'time_zone': "Local time zone",
|
||||||
'description': "Short description (will appear in sites list)",
|
'description': "Short description (will appear in sites list)",
|
||||||
'physical_address': "Physical location of the building (e.g. for GPS)",
|
'physical_address': "Physical location of the building (e.g. for GPS)",
|
||||||
@ -791,7 +798,6 @@ class VirtualChassisForm(BootstrapMixin, CustomFieldModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceVCMembershipForm(forms.ModelForm):
|
class DeviceVCMembershipForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Device
|
model = Device
|
||||||
fields = [
|
fields = [
|
||||||
@ -887,7 +893,6 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
|
|||||||
|
|
||||||
|
|
||||||
class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsolePortTemplate
|
model = ConsolePortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
@ -899,7 +904,6 @@ class ConsolePortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ConsoleServerPortTemplate
|
model = ConsoleServerPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
@ -911,7 +915,6 @@ class ConsoleServerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
@ -923,7 +926,6 @@ class PowerPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = PowerOutletTemplate
|
model = PowerOutletTemplate
|
||||||
fields = [
|
fields = [
|
||||||
@ -934,7 +936,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit power_port choices to current DeviceType
|
# Limit power_port choices to current DeviceType
|
||||||
@ -945,7 +946,6 @@ class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = InterfaceTemplate
|
model = InterfaceTemplate
|
||||||
fields = [
|
fields = [
|
||||||
@ -958,7 +958,6 @@ class InterfaceTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = FrontPortTemplate
|
model = FrontPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
@ -970,7 +969,6 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
# Limit rear_port choices to current DeviceType
|
# Limit rear_port choices to current DeviceType
|
||||||
@ -981,7 +979,6 @@ class FrontPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = RearPortTemplate
|
model = RearPortTemplate
|
||||||
fields = [
|
fields = [
|
||||||
@ -994,7 +991,6 @@ class RearPortTemplateForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
|
|
||||||
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
class DeviceBayTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DeviceBayTemplate
|
model = DeviceBayTemplate
|
||||||
fields = [
|
fields = [
|
||||||
@ -1257,7 +1253,6 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, device_bay, *args, **kwargs):
|
def __init__(self, device_bay, *args, **kwargs):
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.fields['installed_device'].queryset = Device.objects.filter(
|
self.fields['installed_device'].queryset = Device.objects.filter(
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
|
import graphene
|
||||||
|
|
||||||
from dcim import filtersets, models
|
from dcim import filtersets, models
|
||||||
from extras.graphql.mixins import (
|
from extras.graphql.mixins import (
|
||||||
ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
|
ChangelogMixin, ConfigContextMixin, CustomFieldsMixin, ImageAttachmentsMixin, TagsMixin,
|
||||||
)
|
)
|
||||||
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
from ipam.graphql.mixins import IPAddressesMixin, VLANGroupsMixin
|
||||||
|
from netbox.graphql.scalars import BigInt
|
||||||
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType
|
from netbox.graphql.types import BaseObjectType, OrganizationalObjectType, PrimaryObjectType
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
@ -380,6 +383,7 @@ class RegionType(VLANGroupsMixin, OrganizationalObjectType):
|
|||||||
|
|
||||||
|
|
||||||
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
|
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, PrimaryObjectType):
|
||||||
|
asn = graphene.Field(BigInt)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Site
|
model = models.Site
|
||||||
|
19
netbox/dcim/migrations/0141_asn_model.py
Normal file
19
netbox/dcim/migrations/0141_asn_model.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -195,6 +195,11 @@ class Site(PrimaryModel):
|
|||||||
verbose_name='ASN',
|
verbose_name='ASN',
|
||||||
help_text='32-bit autonomous system number'
|
help_text='32-bit autonomous system number'
|
||||||
)
|
)
|
||||||
|
asns = models.ManyToManyField(
|
||||||
|
to='ipam.ASN',
|
||||||
|
related_name='sites',
|
||||||
|
blank=True
|
||||||
|
)
|
||||||
time_zone = TimeZoneField(
|
time_zone = TimeZoneField(
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
@ -81,6 +81,11 @@ class SiteTable(BaseTable):
|
|||||||
group = tables.Column(
|
group = tables.Column(
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
|
asn_count = LinkedCountColumn(
|
||||||
|
viewname='ipam:asn_list',
|
||||||
|
url_params={'site_id': 'pk'},
|
||||||
|
verbose_name='ASNs'
|
||||||
|
)
|
||||||
tenant = TenantColumn()
|
tenant = TenantColumn()
|
||||||
comments = MarkdownColumn()
|
comments = MarkdownColumn()
|
||||||
tags = TagColumn(
|
tags = TagColumn(
|
||||||
@ -90,11 +95,11 @@ class SiteTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Site
|
model = Site
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn', 'time_zone', 'description',
|
'pk', 'id', 'name', 'slug', 'status', 'facility', 'region', 'group', 'tenant', 'asn_count', 'time_zone',
|
||||||
'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name', 'contact_phone',
|
'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'contact_name',
|
||||||
'contact_email', 'comments', 'tags',
|
'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')
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -4,7 +4,7 @@ from django.test import TestCase
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.filtersets import *
|
from dcim.filtersets import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import IPAddress
|
from ipam.models import ASN, IPAddress, RIR
|
||||||
from tenancy.models import Tenant, TenantGroup
|
from tenancy.models import Tenant, TenantGroup
|
||||||
from utilities.choices import ColorChoices
|
from utilities.choices import ColorChoices
|
||||||
from utilities.testing import ChangeLoggedFilterSetTests
|
from utilities.testing import ChangeLoggedFilterSetTests
|
||||||
@ -149,6 +149,23 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
Site.objects.bulk_create(sites)
|
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):
|
def test_name(self):
|
||||||
params = {'name': ['Site 1', 'Site 2']}
|
params = {'name': ['Site 1', 'Site 2']}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
@ -165,6 +182,10 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'asn': [65001, 65002]}
|
params = {'asn': [65001, 65002]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_asns(self):
|
||||||
|
params = {'asns': [64512, 65002]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
def test_latitude(self):
|
def test_latitude(self):
|
||||||
params = {'latitude': [10, 20]}
|
params = {'latitude': [10, 20]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
@ -11,7 +11,7 @@ from netaddr import EUI
|
|||||||
from dcim.choices import *
|
from dcim.choices import *
|
||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import VLAN
|
from ipam.models import ASN, VLAN, RIR
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
from utilities.testing import ViewTestCases, create_tags, create_test_device
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
for group in groups:
|
for group in groups:
|
||||||
group.save()
|
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 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 2', slug='site-2', region=regions[0], group=groups[1]),
|
||||||
Site(name='Site 3', slug='site-3', 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')
|
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 = {
|
cls.form_data = {
|
||||||
'name': 'Site X',
|
'name': 'Site X',
|
||||||
'slug': 'site-x',
|
'slug': 'site-x',
|
||||||
@ -126,7 +153,6 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'group': groups[1].pk,
|
'group': groups[1].pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'facility': 'Facility X',
|
'facility': 'Facility X',
|
||||||
'asn': 65001,
|
|
||||||
'time_zone': pytz.UTC,
|
'time_zone': pytz.UTC,
|
||||||
'description': 'Site description',
|
'description': 'Site description',
|
||||||
'physical_address': '742 Evergreen Terrace, Springfield, USA',
|
'physical_address': '742 Evergreen Terrace, Springfield, USA',
|
||||||
@ -152,7 +178,6 @@ class SiteTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'region': regions[1].pk,
|
'region': regions[1].pk,
|
||||||
'group': groups[1].pk,
|
'group': groups[1].pk,
|
||||||
'tenant': None,
|
'tenant': None,
|
||||||
'asn': 65009,
|
|
||||||
'time_zone': pytz.timezone('US/Eastern'),
|
'time_zone': pytz.timezone('US/Eastern'),
|
||||||
'description': 'New description',
|
'description': 'New description',
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ from django.views.generic import View
|
|||||||
|
|
||||||
from circuits.models import Circuit
|
from circuits.models import Circuit
|
||||||
from extras.views import ObjectChangeLogView, ObjectConfigContextView, ObjectJournalView
|
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 ipam.tables import AssignedIPAddressesTable, InterfaceVLANTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
@ -332,9 +332,15 @@ class SiteView(generic.ObjectView):
|
|||||||
cumulative=True
|
cumulative=True
|
||||||
).restrict(request.user, 'view').filter(site=instance)
|
).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 {
|
return {
|
||||||
'stats': stats,
|
'stats': stats,
|
||||||
'locations': locations,
|
'locations': locations,
|
||||||
|
'asns': asns,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ from netbox.api import WritableNestedSerializer
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'NestedAggregateSerializer',
|
'NestedAggregateSerializer',
|
||||||
|
'NestedASNSerializer',
|
||||||
'NestedFHRPGroupSerializer',
|
'NestedFHRPGroupSerializer',
|
||||||
'NestedIPAddressSerializer',
|
'NestedIPAddressSerializer',
|
||||||
'NestedIPRangeSerializer',
|
'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
|
# VRFs
|
||||||
#
|
#
|
||||||
|
@ -16,6 +16,24 @@ from virtualization.api.nested_serializers import NestedVirtualMachineSerializer
|
|||||||
from .nested_serializers import *
|
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
|
# VRFs
|
||||||
#
|
#
|
||||||
|
@ -5,6 +5,9 @@ from . import views
|
|||||||
router = OrderedDefaultRouter()
|
router = OrderedDefaultRouter()
|
||||||
router.APIRootView = views.IPAMRootView
|
router.APIRootView = views.IPAMRootView
|
||||||
|
|
||||||
|
# ASNs
|
||||||
|
router.register('asns', views.ASNViewSet)
|
||||||
|
|
||||||
# VRFs
|
# VRFs
|
||||||
router.register('vrfs', views.VRFViewSet)
|
router.register('vrfs', views.VRFViewSet)
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
|
from dcim.models import Site
|
||||||
from extras.api.views import CustomFieldModelViewSet
|
from extras.api.views import CustomFieldModelViewSet
|
||||||
from ipam import filtersets
|
from ipam import filtersets
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
@ -16,6 +17,16 @@ class IPAMRootView(APIRootView):
|
|||||||
return 'IPAM'
|
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
|
# VRFs
|
||||||
#
|
#
|
||||||
|
@ -9,6 +9,7 @@ from dcim.models import Device, Interface, Region, Site, SiteGroup
|
|||||||
from extras.filters import TagFilter
|
from extras.filters import TagFilter
|
||||||
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, PrimaryModelFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter, NumericArrayFilter, TreeNodeMultipleChoiceFilter,
|
||||||
)
|
)
|
||||||
@ -19,6 +20,7 @@ from .models import *
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateFilterSet',
|
'AggregateFilterSet',
|
||||||
|
'ASNFilterSet',
|
||||||
'FHRPGroupAssignmentFilterSet',
|
'FHRPGroupAssignmentFilterSet',
|
||||||
'FHRPGroupFilterSet',
|
'FHRPGroupFilterSet',
|
||||||
'IPAddressFilterSet',
|
'IPAddressFilterSet',
|
||||||
@ -177,6 +179,41 @@ class AggregateFilterSet(PrimaryModelFilterSet, TenancyFilterSet):
|
|||||||
return queryset.none()
|
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):
|
class RoleFilterSet(OrganizationalModelFilterSet):
|
||||||
q = django_filters.CharFilter(
|
q = django_filters.CharFilter(
|
||||||
method='search',
|
method='search',
|
||||||
|
@ -5,14 +5,16 @@ from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm
|
|||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
|
from ipam.models import ASN
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, DatePicker, DynamicModelChoiceField, NumericArrayField,
|
add_blank_choice, BootstrapMixin, BulkEditNullBooleanSelect, DatePicker, DynamicModelChoiceField, NumericArrayField,
|
||||||
StaticSelect,
|
StaticSelect, DynamicModelMultipleChoiceField,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateBulkEditForm',
|
'AggregateBulkEditForm',
|
||||||
|
'ASNBulkEditForm',
|
||||||
'FHRPGroupBulkEditForm',
|
'FHRPGroupBulkEditForm',
|
||||||
'IPAddressBulkEditForm',
|
'IPAddressBulkEditForm',
|
||||||
'IPRangeBulkEditForm',
|
'IPRangeBulkEditForm',
|
||||||
@ -90,6 +92,38 @@ class RIRBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEdi
|
|||||||
nullable_fields = ['is_private', 'description']
|
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):
|
class AggregateBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldModelBulkEditForm):
|
||||||
pk = forms.ModelMultipleChoiceField(
|
pk = forms.ModelMultipleChoiceField(
|
||||||
queryset=Aggregate.objects.all(),
|
queryset=Aggregate.objects.all(),
|
||||||
|
@ -12,6 +12,7 @@ from virtualization.models import VirtualMachine, VMInterface
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateCSVForm',
|
'AggregateCSVForm',
|
||||||
|
'ASNCSVForm',
|
||||||
'FHRPGroupCSVForm',
|
'FHRPGroupCSVForm',
|
||||||
'IPAddressCSVForm',
|
'IPAddressCSVForm',
|
||||||
'IPRangeCSVForm',
|
'IPRangeCSVForm',
|
||||||
@ -81,6 +82,25 @@ class AggregateCSVForm(CustomFieldModelCSVForm):
|
|||||||
fields = ('prefix', 'rir', 'tenant', 'date_added', 'description')
|
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):
|
class RoleCSVForm(CustomFieldModelCSVForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
|
|
||||||
|
@ -6,7 +6,9 @@ from extras.forms import CustomFieldModelFilterForm
|
|||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
|
from ipam.models import ASN
|
||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
add_blank_choice, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect,
|
add_blank_choice, BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect,
|
||||||
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES,
|
||||||
@ -14,6 +16,7 @@ from utilities.forms import (
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateFilterForm',
|
'AggregateFilterForm',
|
||||||
|
'ASNFilterForm',
|
||||||
'FHRPGroupFilterForm',
|
'FHRPGroupFilterForm',
|
||||||
'IPAddressFilterForm',
|
'IPAddressFilterForm',
|
||||||
'IPRangeFilterForm',
|
'IPRangeFilterForm',
|
||||||
@ -134,6 +137,33 @@ class AggregateFilterForm(BootstrapMixin, TenancyFilterForm, CustomFieldModelFil
|
|||||||
tag = TagFilterField(model)
|
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):
|
class RoleFilterForm(BootstrapMixin, CustomFieldModelFilterForm):
|
||||||
model = Role
|
model = Role
|
||||||
q = forms.CharField(
|
q = forms.CharField(
|
||||||
|
@ -8,6 +8,7 @@ from ipam.choices import *
|
|||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.formfields import IPNetworkFormField
|
from ipam.formfields import IPNetworkFormField
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
|
from ipam.models import ASN
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
from utilities.exceptions import PermissionsViolation
|
from utilities.exceptions import PermissionsViolation
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
@ -18,6 +19,7 @@ from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInter
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateForm',
|
'AggregateForm',
|
||||||
|
'ASNForm',
|
||||||
'FHRPGroupForm',
|
'FHRPGroupForm',
|
||||||
'FHRPGroupAssignmentForm',
|
'FHRPGroupAssignmentForm',
|
||||||
'IPAddressAssignForm',
|
'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):
|
class RoleForm(BootstrapMixin, CustomFieldModelForm):
|
||||||
slug = SlugField()
|
slug = SlugField()
|
||||||
tags = DynamicModelMultipleChoiceField(
|
tags = DynamicModelMultipleChoiceField(
|
||||||
|
@ -5,6 +5,9 @@ from .types import *
|
|||||||
|
|
||||||
|
|
||||||
class IPAMQuery(graphene.ObjectType):
|
class IPAMQuery(graphene.ObjectType):
|
||||||
|
asn = ObjectField(ASNType)
|
||||||
|
asn_list = ObjectListField(ASNType)
|
||||||
|
|
||||||
aggregate = ObjectField(AggregateType)
|
aggregate = ObjectField(AggregateType)
|
||||||
aggregate_list = ObjectListField(AggregateType)
|
aggregate_list = ObjectListField(AggregateType)
|
||||||
|
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
|
import graphene
|
||||||
|
|
||||||
from ipam import filtersets, models
|
from ipam import filtersets, models
|
||||||
|
from netbox.graphql.scalars import BigInt
|
||||||
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
|
from netbox.graphql.types import OrganizationalObjectType, PrimaryObjectType
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ASNType',
|
||||||
'AggregateType',
|
'AggregateType',
|
||||||
'FHRPGroupType',
|
'FHRPGroupType',
|
||||||
'FHRPGroupAssignmentType',
|
'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 AggregateType(PrimaryObjectType):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
38
netbox/ipam/migrations/0053_asn_model.py
Normal file
38
netbox/ipam/migrations/0053_asn_model.py
Normal 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'],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -5,6 +5,7 @@ from .vlans import *
|
|||||||
from .vrfs import *
|
from .vrfs import *
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
'ASN',
|
||||||
'Aggregate',
|
'Aggregate',
|
||||||
'IPAddress',
|
'IPAddress',
|
||||||
'IPRange',
|
'IPRange',
|
||||||
|
@ -7,6 +7,7 @@ from django.db.models import F
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
|
|
||||||
|
from dcim.fields import ASNField
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
from extras.utils import extras_features
|
from extras.utils import extras_features
|
||||||
from netbox.models import OrganizationalModel, PrimaryModel
|
from netbox.models import OrganizationalModel, PrimaryModel
|
||||||
@ -23,6 +24,7 @@ from virtualization.models import VirtualMachine
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Aggregate',
|
'Aggregate',
|
||||||
|
'ASN',
|
||||||
'IPAddress',
|
'IPAddress',
|
||||||
'IPRange',
|
'IPRange',
|
||||||
'Prefix',
|
'Prefix',
|
||||||
@ -69,6 +71,49 @@ class RIR(OrganizationalModel):
|
|||||||
return reverse('ipam:rir', args=[self.pk])
|
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')
|
@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
|
||||||
class Aggregate(PrimaryModel):
|
class Aggregate(PrimaryModel):
|
||||||
"""
|
"""
|
||||||
|
@ -11,6 +11,7 @@ from ipam.models import *
|
|||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'AggregateTable',
|
'AggregateTable',
|
||||||
|
'ASNTable',
|
||||||
'AssignedIPAddressesTable',
|
'AssignedIPAddressesTable',
|
||||||
'IPAddressAssignTable',
|
'IPAddressAssignTable',
|
||||||
'IPAddressTable',
|
'IPAddressTable',
|
||||||
@ -96,6 +97,28 @@ class RIRTable(BaseTable):
|
|||||||
default_columns = ('pk', 'name', 'is_private', 'aggregate_count', 'description', 'actions')
|
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
|
# Aggregates
|
||||||
#
|
#
|
||||||
|
@ -7,6 +7,7 @@ from rest_framework import status
|
|||||||
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Site
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
|
from tenancy.models import Tenant
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, disable_warnings
|
from utilities.testing import APITestCase, APIViewTestCases, disable_warnings
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +21,58 @@ class AppTest(APITestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
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):
|
class VRFTest(APIViewTestCases.APIViewTestCase):
|
||||||
model = VRF
|
model = VRF
|
||||||
brief_fields = ['display', 'id', 'name', 'prefix_count', 'rd', 'url']
|
brief_fields = ['display', 'id', 'name', 'prefix_count', 'rd', 'url']
|
||||||
|
@ -9,6 +9,83 @@ from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMac
|
|||||||
from tenancy.models import Tenant, TenantGroup
|
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):
|
class VRFTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = VRF.objects.all()
|
queryset = VRF.objects.all()
|
||||||
filterset = VRFFilterSet
|
filterset = VRFFilterSet
|
||||||
|
@ -9,6 +9,61 @@ from tenancy.models import Tenant
|
|||||||
from utilities.testing import ViewTestCases, create_tags
|
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):
|
class VRFTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = VRF
|
model = VRF
|
||||||
|
|
||||||
|
@ -7,6 +7,18 @@ from .models import *
|
|||||||
app_name = 'ipam'
|
app_name = 'ipam'
|
||||||
urlpatterns = [
|
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
|
# VRFs
|
||||||
path('vrfs/', views.VRFListView.as_view(), name='vrf_list'),
|
path('vrfs/', views.VRFListView.as_view(), name='vrf_list'),
|
||||||
path('vrfs/add/', views.VRFEditView.as_view(), name='vrf_add'),
|
path('vrfs/add/', views.VRFEditView.as_view(), name='vrf_add'),
|
||||||
|
@ -5,7 +5,8 @@ from django.http import Http404
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.models import Device, Interface
|
from dcim.models import Device, Interface, Site
|
||||||
|
from dcim.tables import SiteTable
|
||||||
from netbox.views import generic
|
from netbox.views import generic
|
||||||
from utilities.tables import paginate_table
|
from utilities.tables import paginate_table
|
||||||
from utilities.utils import count_related
|
from utilities.utils import count_related
|
||||||
@ -13,6 +14,7 @@ from virtualization.models import VirtualMachine, VMInterface
|
|||||||
from . import filtersets, forms, tables
|
from . import filtersets, forms, tables
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import *
|
from .models import *
|
||||||
|
from .models import ASN
|
||||||
from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
|
from .utils import add_available_ipaddresses, add_available_prefixes, add_available_vlans
|
||||||
|
|
||||||
|
|
||||||
@ -197,6 +199,62 @@ class RIRBulkDeleteView(generic.BulkDeleteView):
|
|||||||
table = tables.RIRTable
|
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
|
# Aggregates
|
||||||
#
|
#
|
||||||
|
23
netbox/netbox/graphql/scalars.py
Normal file
23
netbox/netbox/graphql/scalars.py
Normal 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)
|
@ -229,6 +229,12 @@ IPAM_MENU = Menu(
|
|||||||
get_model_item('ipam', 'role', 'Prefix & VLAN Roles'),
|
get_model_item('ipam', 'role', 'Prefix & VLAN Roles'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
MenuGroup(
|
||||||
|
label='ASNs',
|
||||||
|
items=(
|
||||||
|
get_model_item('ipam', 'asn', 'ASNs'),
|
||||||
|
),
|
||||||
|
),
|
||||||
MenuGroup(
|
MenuGroup(
|
||||||
label='Aggregates',
|
label='Aggregates',
|
||||||
items=(
|
items=(
|
||||||
|
@ -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>
|
<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>
|
<p>Virtual Machines</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -256,6 +260,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</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' %}
|
{% include 'inc/panels/image_attachments.html' %}
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
</div>
|
</div>
|
||||||
|
77
netbox/templates/ipam/asn.html
Normal file
77
netbox/templates/ipam/asn.html
Normal 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 %}
|
@ -5,6 +5,9 @@ from django.test import TestCase
|
|||||||
from mptt.fields import TreeForeignKey
|
from mptt.fields import TreeForeignKey
|
||||||
from taggit.managers import TaggableManager
|
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.choices import *
|
||||||
from dcim.fields import MACAddressField
|
from dcim.fields import MACAddressField
|
||||||
from dcim.filtersets import DeviceFilterSet, SiteFilterSet
|
from dcim.filtersets import DeviceFilterSet, SiteFilterSet
|
||||||
@ -13,6 +16,7 @@ from dcim.models import (
|
|||||||
)
|
)
|
||||||
from extras.filters import TagFilter
|
from extras.filters import TagFilter
|
||||||
from extras.models import TaggedItem
|
from extras.models import TaggedItem
|
||||||
|
from ipam.models import RIR, ASN
|
||||||
from netbox.filtersets import BaseFilterSet
|
from netbox.filtersets import BaseFilterSet
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter, MultiValueNumberFilter,
|
MACAddressFilter, MultiValueCharFilter, MultiValueDateFilter, MultiValueDateTimeFilter, MultiValueNumberFilter,
|
||||||
@ -337,6 +341,8 @@ class DynamicFilterLookupExpressionTest(TestCase):
|
|||||||
device_filterset = DeviceFilterSet
|
device_filterset = DeviceFilterSet
|
||||||
site_queryset = Site.objects.all()
|
site_queryset = Site.objects.all()
|
||||||
site_filterset = SiteFilterSet
|
site_filterset = SiteFilterSet
|
||||||
|
circuit_queryset = Circuit.objects.all()
|
||||||
|
circuit_filterset = CircuitFilterSet
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
@ -384,6 +390,19 @@ class DynamicFilterLookupExpressionTest(TestCase):
|
|||||||
)
|
)
|
||||||
Site.objects.bulk_create(sites)
|
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 = (
|
racks = (
|
||||||
Rack(name='Rack 1', site=sites[0]),
|
Rack(name='Rack 1', site=sites[0]),
|
||||||
Rack(name='Rack 2', site=sites[1]),
|
Rack(name='Rack 2', site=sites[1]),
|
||||||
|
Loading…
Reference in New Issue
Block a user