mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-29 11:56:25 -06:00
Merge branch 'feature' into 7848-rq-api
This commit is contained in:
commit
d3953a2f6d
@ -43,3 +43,7 @@ The security cipher used to apply wireless authentication. Options include:
|
|||||||
### Pre-Shared Key
|
### Pre-Shared Key
|
||||||
|
|
||||||
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
|
The security key configured on each client to grant access to the secured wireless LAN. This applies only to certain authentication types.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
The [region](../dcim/region.md), [site](../dcim/site.md), [site group](../dcim/sitegroup.md) or [location](../dcim/location.md) with which this wireless LAN is associated.
|
||||||
|
@ -21,6 +21,7 @@ def copy_site_assignments(apps, schema_editor):
|
|||||||
termination_id=models.F('provider_network_id')
|
termination_id=models.F('provider_network_id')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
22
netbox/circuits/migrations/0049_natural_ordering.py
Normal file
22
netbox/circuits/migrations/0049_natural_ordering.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('circuits', '0048_circuitterminations_cached_relations'),
|
||||||
|
('dcim', '0197_natural_sort_collation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='provider',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='providernetwork',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
]
|
@ -21,7 +21,8 @@ class Provider(ContactsMixin, PrimaryModel):
|
|||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True,
|
unique=True,
|
||||||
help_text=_('Full name of the provider')
|
help_text=_('Full name of the provider'),
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
@ -95,7 +96,8 @@ class ProviderNetwork(PrimaryModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
provider = models.ForeignKey(
|
provider = models.ForeignKey(
|
||||||
to='circuits.Provider',
|
to='circuits.Provider',
|
||||||
|
@ -341,7 +341,7 @@ class ProviderNetworkTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class TestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = CircuitTermination
|
model = CircuitTermination
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -76,10 +76,6 @@ class ChangeLogViewTest(ModelViewTestCase):
|
|||||||
self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
|
self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
|
||||||
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||||
|
|
||||||
# Check that private attributes were included in raw data but not display data
|
|
||||||
self.assertIn('_name', oc.postchange_data)
|
|
||||||
self.assertNotIn('_name', oc.postchange_data_clean)
|
|
||||||
|
|
||||||
def test_update_object(self):
|
def test_update_object(self):
|
||||||
site = Site(name='Site 1', slug='site-1')
|
site = Site(name='Site 1', slug='site-1')
|
||||||
site.save()
|
site.save()
|
||||||
@ -117,12 +113,6 @@ class ChangeLogViewTest(ModelViewTestCase):
|
|||||||
self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
|
self.assertEqual(oc.postchange_data['custom_fields']['cf2'], form_data['cf_cf2'])
|
||||||
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
|
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
|
||||||
|
|
||||||
# Check that private attributes were included in raw data but not display data
|
|
||||||
self.assertIn('_name', oc.prechange_data)
|
|
||||||
self.assertNotIn('_name', oc.prechange_data_clean)
|
|
||||||
self.assertIn('_name', oc.postchange_data)
|
|
||||||
self.assertNotIn('_name', oc.postchange_data_clean)
|
|
||||||
|
|
||||||
def test_delete_object(self):
|
def test_delete_object(self):
|
||||||
site = Site(
|
site = Site(
|
||||||
name='Site 1',
|
name='Site 1',
|
||||||
@ -153,10 +143,6 @@ class ChangeLogViewTest(ModelViewTestCase):
|
|||||||
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
|
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||||
self.assertEqual(oc.postchange_data, None)
|
self.assertEqual(oc.postchange_data, None)
|
||||||
|
|
||||||
# Check that private attributes were included in raw data but not display data
|
|
||||||
self.assertIn('_name', oc.prechange_data)
|
|
||||||
self.assertNotIn('_name', oc.prechange_data_clean)
|
|
||||||
|
|
||||||
def test_bulk_update_objects(self):
|
def test_bulk_update_objects(self):
|
||||||
sites = (
|
sites = (
|
||||||
Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE),
|
Site(name='Site 1', slug='site-1', status=SiteStatusChoices.STATUS_ACTIVE),
|
||||||
@ -353,10 +339,6 @@ class ChangeLogAPITest(APITestCase):
|
|||||||
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
|
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
|
||||||
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
self.assertEqual(oc.postchange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||||
|
|
||||||
# Check that private attributes were included in raw data but not display data
|
|
||||||
self.assertIn('_name', oc.postchange_data)
|
|
||||||
self.assertNotIn('_name', oc.postchange_data_clean)
|
|
||||||
|
|
||||||
def test_update_object(self):
|
def test_update_object(self):
|
||||||
site = Site(name='Site 1', slug='site-1')
|
site = Site(name='Site 1', slug='site-1')
|
||||||
site.save()
|
site.save()
|
||||||
@ -389,12 +371,6 @@ class ChangeLogAPITest(APITestCase):
|
|||||||
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
|
self.assertEqual(oc.postchange_data['custom_fields'], data['custom_fields'])
|
||||||
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
|
self.assertEqual(oc.postchange_data['tags'], ['Tag 3'])
|
||||||
|
|
||||||
# Check that private attributes were included in raw data but not display data
|
|
||||||
self.assertIn('_name', oc.prechange_data)
|
|
||||||
self.assertNotIn('_name', oc.prechange_data_clean)
|
|
||||||
self.assertIn('_name', oc.postchange_data)
|
|
||||||
self.assertNotIn('_name', oc.postchange_data_clean)
|
|
||||||
|
|
||||||
def test_delete_object(self):
|
def test_delete_object(self):
|
||||||
site = Site(
|
site = Site(
|
||||||
name='Site 1',
|
name='Site 1',
|
||||||
@ -423,10 +399,6 @@ class ChangeLogAPITest(APITestCase):
|
|||||||
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
|
self.assertEqual(oc.prechange_data['tags'], ['Tag 1', 'Tag 2'])
|
||||||
self.assertEqual(oc.postchange_data, None)
|
self.assertEqual(oc.postchange_data, None)
|
||||||
|
|
||||||
# Check that private attributes were included in raw data but not display data
|
|
||||||
self.assertIn('_name', oc.prechange_data)
|
|
||||||
self.assertNotIn('_name', oc.prechange_data_clean)
|
|
||||||
|
|
||||||
def test_bulk_create_objects(self):
|
def test_bulk_create_objects(self):
|
||||||
data = (
|
data = (
|
||||||
{
|
{
|
||||||
|
@ -21,7 +21,7 @@ __all__ = (
|
|||||||
class RegionSerializer(NestedGroupModelSerializer):
|
class RegionSerializer(NestedGroupModelSerializer):
|
||||||
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
parent = NestedRegionSerializer(required=False, allow_null=True, default=None)
|
||||||
site_count = serializers.IntegerField(read_only=True, default=0)
|
site_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
prefix_count = RelatedObjectCountField('_prefixes')
|
prefix_count = RelatedObjectCountField('prefix_set')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Region
|
model = Region
|
||||||
@ -35,7 +35,7 @@ class RegionSerializer(NestedGroupModelSerializer):
|
|||||||
class SiteGroupSerializer(NestedGroupModelSerializer):
|
class SiteGroupSerializer(NestedGroupModelSerializer):
|
||||||
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
parent = NestedSiteGroupSerializer(required=False, allow_null=True, default=None)
|
||||||
site_count = serializers.IntegerField(read_only=True, default=0)
|
site_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
prefix_count = RelatedObjectCountField('_prefixes')
|
prefix_count = RelatedObjectCountField('prefix_set')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SiteGroup
|
model = SiteGroup
|
||||||
@ -63,7 +63,7 @@ class SiteSerializer(NetBoxModelSerializer):
|
|||||||
# Related object counts
|
# Related object counts
|
||||||
circuit_count = RelatedObjectCountField('circuit_terminations')
|
circuit_count = RelatedObjectCountField('circuit_terminations')
|
||||||
device_count = RelatedObjectCountField('devices')
|
device_count = RelatedObjectCountField('devices')
|
||||||
prefix_count = RelatedObjectCountField('_prefixes')
|
prefix_count = RelatedObjectCountField('prefix_set')
|
||||||
rack_count = RelatedObjectCountField('racks')
|
rack_count = RelatedObjectCountField('racks')
|
||||||
vlan_count = RelatedObjectCountField('vlans')
|
vlan_count = RelatedObjectCountField('vlans')
|
||||||
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
virtualmachine_count = RelatedObjectCountField('virtual_machines')
|
||||||
@ -86,7 +86,7 @@ class LocationSerializer(NestedGroupModelSerializer):
|
|||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
rack_count = serializers.IntegerField(read_only=True, default=0)
|
rack_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
device_count = serializers.IntegerField(read_only=True, default=0)
|
device_count = serializers.IntegerField(read_only=True, default=0)
|
||||||
prefix_count = RelatedObjectCountField('_prefixes')
|
prefix_count = RelatedObjectCountField('prefix_set')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Location
|
model = Location
|
||||||
|
67
netbox/dcim/base_filtersets.py
Normal file
67
netbox/dcim/base_filtersets.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import django_filters
|
||||||
|
|
||||||
|
from django.utils.translation import gettext as _
|
||||||
|
from netbox.filtersets import BaseFilterSet
|
||||||
|
from utilities.filters import ContentTypeFilter, TreeNodeMultipleChoiceFilter
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ScopedFilterSet',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ScopedFilterSet(BaseFilterSet):
|
||||||
|
"""
|
||||||
|
Provides additional filtering functionality for location, site, etc.. for Scoped models.
|
||||||
|
"""
|
||||||
|
scope_type = ContentTypeFilter()
|
||||||
|
region_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
field_name='_region',
|
||||||
|
lookup_expr='in',
|
||||||
|
label=_('Region (ID)'),
|
||||||
|
)
|
||||||
|
region = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
field_name='_region',
|
||||||
|
lookup_expr='in',
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Region (slug)'),
|
||||||
|
)
|
||||||
|
site_group_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
field_name='_site_group',
|
||||||
|
lookup_expr='in',
|
||||||
|
label=_('Site group (ID)'),
|
||||||
|
)
|
||||||
|
site_group = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
field_name='_site_group',
|
||||||
|
lookup_expr='in',
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Site group (slug)'),
|
||||||
|
)
|
||||||
|
site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
field_name='_site',
|
||||||
|
label=_('Site (ID)'),
|
||||||
|
)
|
||||||
|
site = django_filters.ModelMultipleChoiceFilter(
|
||||||
|
field_name='_site__slug',
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Site (slug)'),
|
||||||
|
)
|
||||||
|
location_id = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
field_name='_location',
|
||||||
|
lookup_expr='in',
|
||||||
|
label=_('Location (ID)'),
|
||||||
|
)
|
||||||
|
location = TreeNodeMultipleChoiceFilter(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
field_name='_location',
|
||||||
|
lookup_expr='in',
|
||||||
|
to_field_name='slug',
|
||||||
|
label=_('Location (slug)'),
|
||||||
|
)
|
@ -73,7 +73,6 @@ __all__ = (
|
|||||||
'RearPortFilterSet',
|
'RearPortFilterSet',
|
||||||
'RearPortTemplateFilterSet',
|
'RearPortTemplateFilterSet',
|
||||||
'RegionFilterSet',
|
'RegionFilterSet',
|
||||||
'ScopedFilterSet',
|
|
||||||
'SiteFilterSet',
|
'SiteFilterSet',
|
||||||
'SiteGroupFilterSet',
|
'SiteGroupFilterSet',
|
||||||
'VirtualChassisFilterSet',
|
'VirtualChassisFilterSet',
|
||||||
@ -2345,60 +2344,3 @@ class InterfaceConnectionFilterSet(ConnectionFilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Interface
|
model = Interface
|
||||||
fields = tuple()
|
fields = tuple()
|
||||||
|
|
||||||
|
|
||||||
class ScopedFilterSet(BaseFilterSet):
|
|
||||||
"""
|
|
||||||
Provides additional filtering functionality for location, site, etc.. for Scoped models.
|
|
||||||
"""
|
|
||||||
scope_type = ContentTypeFilter()
|
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=Region.objects.all(),
|
|
||||||
field_name='_region',
|
|
||||||
lookup_expr='in',
|
|
||||||
label=_('Region (ID)'),
|
|
||||||
)
|
|
||||||
region = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=Region.objects.all(),
|
|
||||||
field_name='_region',
|
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Region (slug)'),
|
|
||||||
)
|
|
||||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
field_name='_site_group',
|
|
||||||
lookup_expr='in',
|
|
||||||
label=_('Site group (ID)'),
|
|
||||||
)
|
|
||||||
site_group = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
field_name='_site_group',
|
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Site group (slug)'),
|
|
||||||
)
|
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
field_name='_site',
|
|
||||||
label=_('Site (ID)'),
|
|
||||||
)
|
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='_site__slug',
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Site (slug)'),
|
|
||||||
)
|
|
||||||
location_id = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=Location.objects.all(),
|
|
||||||
field_name='_location',
|
|
||||||
lookup_expr='in',
|
|
||||||
label=_('Location (ID)'),
|
|
||||||
)
|
|
||||||
location = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=Location.objects.all(),
|
|
||||||
field_name='_location',
|
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Location (slug)'),
|
|
||||||
)
|
|
||||||
|
@ -76,7 +76,6 @@ class ComponentType(
|
|||||||
"""
|
"""
|
||||||
Base type for device/VM components
|
Base type for device/VM components
|
||||||
"""
|
"""
|
||||||
_name: str
|
|
||||||
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
|
device: Annotated["DeviceType", strawberry.lazy('dcim.graphql.types')]
|
||||||
|
|
||||||
|
|
||||||
@ -93,7 +92,6 @@ class ComponentTemplateType(
|
|||||||
"""
|
"""
|
||||||
Base type for device/VM components
|
Base type for device/VM components
|
||||||
"""
|
"""
|
||||||
_name: str
|
|
||||||
device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]
|
device_type: Annotated["DeviceTypeType", strawberry.lazy('dcim.graphql.types')]
|
||||||
|
|
||||||
|
|
||||||
@ -181,7 +179,7 @@ class ConsolePortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
|
|||||||
filters=ConsolePortTemplateFilter
|
filters=ConsolePortTemplateFilter
|
||||||
)
|
)
|
||||||
class ConsolePortTemplateType(ModularComponentTemplateType):
|
class ConsolePortTemplateType(ModularComponentTemplateType):
|
||||||
_name: str
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -199,7 +197,7 @@ class ConsoleServerPortType(ModularComponentType, CabledObjectMixin, PathEndpoin
|
|||||||
filters=ConsoleServerPortTemplateFilter
|
filters=ConsoleServerPortTemplateFilter
|
||||||
)
|
)
|
||||||
class ConsoleServerPortTemplateType(ModularComponentTemplateType):
|
class ConsoleServerPortTemplateType(ModularComponentTemplateType):
|
||||||
_name: str
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -208,7 +206,6 @@ class ConsoleServerPortTemplateType(ModularComponentTemplateType):
|
|||||||
filters=DeviceFilter
|
filters=DeviceFilter
|
||||||
)
|
)
|
||||||
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
class DeviceType(ConfigContextMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||||
_name: str
|
|
||||||
console_port_count: BigInt
|
console_port_count: BigInt
|
||||||
console_server_port_count: BigInt
|
console_server_port_count: BigInt
|
||||||
power_port_count: BigInt
|
power_port_count: BigInt
|
||||||
@ -273,7 +270,7 @@ class DeviceBayType(ComponentType):
|
|||||||
filters=DeviceBayTemplateFilter
|
filters=DeviceBayTemplateFilter
|
||||||
)
|
)
|
||||||
class DeviceBayTemplateType(ComponentTemplateType):
|
class DeviceBayTemplateType(ComponentTemplateType):
|
||||||
_name: str
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -282,7 +279,6 @@ class DeviceBayTemplateType(ComponentTemplateType):
|
|||||||
filters=InventoryItemTemplateFilter
|
filters=InventoryItemTemplateFilter
|
||||||
)
|
)
|
||||||
class InventoryItemTemplateType(ComponentTemplateType):
|
class InventoryItemTemplateType(ComponentTemplateType):
|
||||||
_name: str
|
|
||||||
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
|
role: Annotated["InventoryItemRoleType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
manufacturer: Annotated["ManufacturerType", strawberry.lazy('dcim.graphql.types')]
|
||||||
|
|
||||||
@ -366,7 +362,6 @@ class FrontPortType(ModularComponentType, CabledObjectMixin):
|
|||||||
filters=FrontPortTemplateFilter
|
filters=FrontPortTemplateFilter
|
||||||
)
|
)
|
||||||
class FrontPortTemplateType(ModularComponentTemplateType):
|
class FrontPortTemplateType(ModularComponentTemplateType):
|
||||||
_name: str
|
|
||||||
color: str
|
color: str
|
||||||
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
rear_port: Annotated["RearPortTemplateType", strawberry.lazy('dcim.graphql.types')]
|
||||||
|
|
||||||
@ -377,6 +372,7 @@ class FrontPortTemplateType(ModularComponentTemplateType):
|
|||||||
filters=InterfaceFilter
|
filters=InterfaceFilter
|
||||||
)
|
)
|
||||||
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
class InterfaceType(IPAddressesMixin, ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
||||||
|
_name: str
|
||||||
mac_address: str | None
|
mac_address: str | None
|
||||||
wwn: str | None
|
wwn: str | None
|
||||||
parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
parent: Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
@ -465,7 +461,7 @@ class LocationType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, Organi
|
|||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
||||||
return self._clusters.all()
|
return self.cluster_set.all()
|
||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||||
@ -527,7 +523,7 @@ class ModuleBayType(ModularComponentType):
|
|||||||
filters=ModuleBayTemplateFilter
|
filters=ModuleBayTemplateFilter
|
||||||
)
|
)
|
||||||
class ModuleBayTemplateType(ModularComponentTemplateType):
|
class ModuleBayTemplateType(ModularComponentTemplateType):
|
||||||
_name: str
|
pass
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -588,7 +584,6 @@ class PowerOutletType(ModularComponentType, CabledObjectMixin, PathEndpointMixin
|
|||||||
filters=PowerOutletTemplateFilter
|
filters=PowerOutletTemplateFilter
|
||||||
)
|
)
|
||||||
class PowerOutletTemplateType(ModularComponentTemplateType):
|
class PowerOutletTemplateType(ModularComponentTemplateType):
|
||||||
_name: str
|
|
||||||
power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None
|
power_port: Annotated["PowerPortTemplateType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
|
|
||||||
|
|
||||||
@ -620,8 +615,6 @@ class PowerPortType(ModularComponentType, CabledObjectMixin, PathEndpointMixin):
|
|||||||
filters=PowerPortTemplateFilter
|
filters=PowerPortTemplateFilter
|
||||||
)
|
)
|
||||||
class PowerPortTemplateType(ModularComponentTemplateType):
|
class PowerPortTemplateType(ModularComponentTemplateType):
|
||||||
_name: str
|
|
||||||
|
|
||||||
poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
poweroutlet_templates: List[Annotated["PowerOutletTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
|
||||||
@ -640,7 +633,6 @@ class RackTypeType(NetBoxObjectType):
|
|||||||
filters=RackFilter
|
filters=RackFilter
|
||||||
)
|
)
|
||||||
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
class RackType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||||
_name: str
|
|
||||||
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
|
site: Annotated["SiteType", strawberry.lazy('dcim.graphql.types')]
|
||||||
location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
|
location: Annotated["LocationType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
tenant: Annotated["TenantType", strawberry.lazy('tenancy.graphql.types')] | None
|
||||||
@ -693,7 +685,6 @@ class RearPortType(ModularComponentType, CabledObjectMixin):
|
|||||||
filters=RearPortTemplateFilter
|
filters=RearPortTemplateFilter
|
||||||
)
|
)
|
||||||
class RearPortTemplateType(ModularComponentTemplateType):
|
class RearPortTemplateType(ModularComponentTemplateType):
|
||||||
_name: str
|
|
||||||
color: str
|
color: str
|
||||||
|
|
||||||
frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
frontport_templates: List[Annotated["FrontPortTemplateType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
@ -716,7 +707,7 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
|||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
||||||
return self._clusters.all()
|
return self.cluster_set.all()
|
||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||||
@ -729,7 +720,6 @@ class RegionType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
|||||||
filters=SiteFilter
|
filters=SiteFilter
|
||||||
)
|
)
|
||||||
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObjectType):
|
||||||
_name: str
|
|
||||||
time_zone: str | None
|
time_zone: str | None
|
||||||
region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None
|
region: Annotated["RegionType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None
|
group: Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')] | None
|
||||||
@ -749,7 +739,7 @@ class SiteType(VLANGroupsMixin, ImageAttachmentsMixin, ContactsMixin, NetBoxObje
|
|||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
||||||
return self._clusters.all()
|
return self.cluster_set.all()
|
||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||||
@ -773,7 +763,7 @@ class SiteGroupType(VLANGroupsMixin, ContactsMixin, OrganizationalObjectType):
|
|||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
def clusters(self) -> List[Annotated["ClusterType", strawberry.lazy('virtualization.graphql.types')]]:
|
||||||
return self._clusters.all()
|
return self.cluster_set.all()
|
||||||
|
|
||||||
@strawberry_django.field
|
@strawberry_django.field
|
||||||
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
def circuit_terminations(self) -> List[Annotated["CircuitTerminationType", strawberry.lazy('circuits.graphql.types')]]:
|
||||||
|
17
netbox/dcim/migrations/0197_natural_sort_collation.py
Normal file
17
netbox/dcim/migrations/0197_natural_sort_collation.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.contrib.postgres.operations import CreateCollation
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0196_qinq_svlan'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
CreateCollation(
|
||||||
|
"natural_sort",
|
||||||
|
provider="icu",
|
||||||
|
locale="und-u-kn-true",
|
||||||
|
),
|
||||||
|
]
|
318
netbox/dcim/migrations/0198_natural_ordering.py
Normal file
318
netbox/dcim/migrations/0198_natural_ordering.py
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0197_natural_sort_collation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='site',
|
||||||
|
options={'ordering': ('name',)},
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='site',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='consoleport',
|
||||||
|
options={'ordering': ('device', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='consoleporttemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='consoleserverport',
|
||||||
|
options={'ordering': ('device', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='consoleserverporttemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='device',
|
||||||
|
options={'ordering': ('name', 'pk')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='devicebay',
|
||||||
|
options={'ordering': ('device', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='devicebaytemplate',
|
||||||
|
options={'ordering': ('device_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='frontport',
|
||||||
|
options={'ordering': ('device', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='frontporttemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='interfacetemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='inventoryitem',
|
||||||
|
options={'ordering': ('device__id', 'parent__id', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='inventoryitemtemplate',
|
||||||
|
options={'ordering': ('device_type__id', 'parent__id', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='modulebay',
|
||||||
|
options={'ordering': ('device', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='modulebaytemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='poweroutlet',
|
||||||
|
options={'ordering': ('device', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='poweroutlettemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='powerport',
|
||||||
|
options={'ordering': ('device', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='powerporttemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rack',
|
||||||
|
options={'ordering': ('site', 'location', 'name', 'pk')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rearport',
|
||||||
|
options={'ordering': ('device', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='rearporttemplate',
|
||||||
|
options={'ordering': ('device_type', 'module_type', 'name')},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='consoleporttemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='consoleserverporttemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='device',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='devicebaytemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='frontporttemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='inventoryitemtemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='modulebaytemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='poweroutlettemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='powerporttemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='rack',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='rearporttemplate',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='site',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleport',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleporttemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleserverport',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='consoleserverporttemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='device',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(blank=True, db_collation='natural_sort', max_length=64, null=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicebay',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='devicebaytemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='frontport',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='frontporttemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interface',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='interfacetemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitem',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='inventoryitemtemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulebay',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='modulebaytemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poweroutlet',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='poweroutlettemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerport',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerporttemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rack',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rearport',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='rearporttemplate',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerfeed',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='powerpanel',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualchassis',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualdevicecontext',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
]
|
@ -44,12 +44,8 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
|||||||
max_length=64,
|
max_length=64,
|
||||||
help_text=_(
|
help_text=_(
|
||||||
"{module} is accepted as a substitution for the module bay position when attached to a module type."
|
"{module} is accepted as a substitution for the module bay position when attached to a module type."
|
||||||
)
|
),
|
||||||
)
|
db_collation="natural_sort"
|
||||||
_name = NaturalOrderingField(
|
|
||||||
target_field='name',
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
)
|
||||||
label = models.CharField(
|
label = models.CharField(
|
||||||
verbose_name=_('label'),
|
verbose_name=_('label'),
|
||||||
@ -65,7 +61,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
ordering = ('device_type', '_name')
|
ordering = ('device_type', 'name')
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('device_type', 'name'),
|
fields=('device_type', 'name'),
|
||||||
@ -125,7 +121,7 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
ordering = ('device_type', 'module_type', '_name')
|
ordering = ('device_type', 'module_type', 'name')
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('device_type', 'name'),
|
fields=('device_type', 'name'),
|
||||||
@ -782,7 +778,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
|
|||||||
component_model = InventoryItem
|
component_model = InventoryItem
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('device_type__id', 'parent__id', '_name')
|
ordering = ('device_type__id', 'parent__id', 'name')
|
||||||
indexes = (
|
indexes = (
|
||||||
models.Index(fields=('component_type', 'component_id')),
|
models.Index(fields=('component_type', 'component_id')),
|
||||||
)
|
)
|
||||||
|
@ -50,12 +50,8 @@ class ComponentModel(NetBoxModel):
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=64
|
max_length=64,
|
||||||
)
|
db_collation="natural_sort"
|
||||||
_name = NaturalOrderingField(
|
|
||||||
target_field='name',
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
)
|
||||||
label = models.CharField(
|
label = models.CharField(
|
||||||
verbose_name=_('label'),
|
verbose_name=_('label'),
|
||||||
@ -71,7 +67,7 @@ class ComponentModel(NetBoxModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
ordering = ('device', '_name')
|
ordering = ('device', 'name')
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('device', 'name'),
|
fields=('device', 'name'),
|
||||||
@ -1301,7 +1297,7 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
|
|||||||
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
|
clone_fields = ('device', 'parent', 'role', 'manufacturer', 'status', 'part_id')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('device__id', 'parent__id', '_name')
|
ordering = ('device__id', 'parent__id', 'name')
|
||||||
indexes = (
|
indexes = (
|
||||||
models.Index(fields=('component_type', 'component_id')),
|
models.Index(fields=('component_type', 'component_id')),
|
||||||
)
|
)
|
||||||
|
@ -23,7 +23,7 @@ from netbox.config import ConfigItem
|
|||||||
from netbox.models import OrganizationalModel, PrimaryModel
|
from netbox.models import OrganizationalModel, PrimaryModel
|
||||||
from netbox.models.mixins import WeightMixin
|
from netbox.models.mixins import WeightMixin
|
||||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||||
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
|
from utilities.fields import ColorField, CounterCacheField
|
||||||
from utilities.tracking import TrackingModelMixin
|
from utilities.tracking import TrackingModelMixin
|
||||||
from .device_components import *
|
from .device_components import *
|
||||||
from .mixins import RenderConfigMixin
|
from .mixins import RenderConfigMixin
|
||||||
@ -582,13 +582,8 @@ class Device(
|
|||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=64,
|
max_length=64,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True,
|
||||||
)
|
db_collation="natural_sort"
|
||||||
_name = NaturalOrderingField(
|
|
||||||
target_field='name',
|
|
||||||
max_length=100,
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
)
|
||||||
serial = models.CharField(
|
serial = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -775,7 +770,7 @@ class Device(
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('_name', 'pk') # Name may be null
|
ordering = ('name', 'pk') # Name may be null
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
Lower('name'), 'site', 'tenant',
|
Lower('name'), 'site', 'tenant',
|
||||||
@ -1320,7 +1315,8 @@ class VirtualChassis(PrimaryModel):
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=64
|
max_length=64,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
domain = models.CharField(
|
domain = models.CharField(
|
||||||
verbose_name=_('domain'),
|
verbose_name=_('domain'),
|
||||||
@ -1382,7 +1378,8 @@ class VirtualDeviceContext(PrimaryModel):
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=64
|
max_length=64,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
verbose_name=_('status'),
|
verbose_name=_('status'),
|
||||||
|
@ -59,28 +59,24 @@ class CachedScopeMixin(models.Model):
|
|||||||
_location = models.ForeignKey(
|
_location = models.ForeignKey(
|
||||||
to='dcim.Location',
|
to='dcim.Location',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='_%(class)ss',
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_site = models.ForeignKey(
|
_site = models.ForeignKey(
|
||||||
to='dcim.Site',
|
to='dcim.Site',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='_%(class)ss',
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_region = models.ForeignKey(
|
_region = models.ForeignKey(
|
||||||
to='dcim.Region',
|
to='dcim.Region',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='_%(class)ss',
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
_site_group = models.ForeignKey(
|
_site_group = models.ForeignKey(
|
||||||
to='dcim.SiteGroup',
|
to='dcim.SiteGroup',
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='_%(class)ss',
|
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
@ -36,7 +36,8 @@ class PowerPanel(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
|
|
||||||
prerequisite_models = (
|
prerequisite_models = (
|
||||||
@ -86,7 +87,8 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
verbose_name=_('status'),
|
verbose_name=_('status'),
|
||||||
|
@ -19,7 +19,7 @@ from netbox.models.mixins import WeightMixin
|
|||||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||||
from utilities.conversion import to_grams
|
from utilities.conversion import to_grams
|
||||||
from utilities.data import array_to_string, drange
|
from utilities.data import array_to_string, drange
|
||||||
from utilities.fields import ColorField, NaturalOrderingField
|
from utilities.fields import ColorField
|
||||||
from .device_components import PowerPort
|
from .device_components import PowerPort
|
||||||
from .devices import Device, Module
|
from .devices import Device, Module
|
||||||
from .power import PowerFeed
|
from .power import PowerFeed
|
||||||
@ -255,12 +255,8 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
|
||||||
)
|
|
||||||
_name = NaturalOrderingField(
|
|
||||||
target_field='name',
|
|
||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
facility_id = models.CharField(
|
facility_id = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -340,7 +336,7 @@ class Rack(ContactsMixin, ImageAttachmentsMixin, RackBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique
|
ordering = ('site', 'location', 'name', 'pk') # (site, location, name) may be non-unique
|
||||||
constraints = (
|
constraints = (
|
||||||
# Name and facility_id must be unique *only* within a Location
|
# Name and facility_id must be unique *only* within a Location
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
|
@ -8,7 +8,6 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from netbox.models import NestedGroupModel, PrimaryModel
|
from netbox.models import NestedGroupModel, PrimaryModel
|
||||||
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
from netbox.models.features import ContactsMixin, ImageAttachmentsMixin
|
||||||
from utilities.fields import NaturalOrderingField
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Location',
|
'Location',
|
||||||
@ -143,12 +142,8 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
|||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True,
|
unique=True,
|
||||||
help_text=_("Full name of the site")
|
help_text=_("Full name of the site"),
|
||||||
)
|
db_collation="natural_sort"
|
||||||
_name = NaturalOrderingField(
|
|
||||||
target_field='name',
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
@ -245,7 +240,7 @@ class Site(ContactsMixin, ImageAttachmentsMixin, PrimaryModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('_name',)
|
ordering = ('name',)
|
||||||
verbose_name = _('site')
|
verbose_name = _('site')
|
||||||
verbose_name_plural = _('sites')
|
verbose_name_plural = _('sites')
|
||||||
|
|
||||||
|
@ -132,7 +132,6 @@ class PlatformTable(NetBoxTable):
|
|||||||
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
order_by=('_name',),
|
|
||||||
template_code=DEVICE_LINK,
|
template_code=DEVICE_LINK,
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
@ -288,7 +287,6 @@ class DeviceComponentTable(NetBoxTable):
|
|||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
linkify=True,
|
linkify=True,
|
||||||
order_by=('_name',)
|
|
||||||
)
|
)
|
||||||
device_status = columns.ChoiceFieldColumn(
|
device_status = columns.ChoiceFieldColumn(
|
||||||
accessor=tables.A('device__status'),
|
accessor=tables.A('device__status'),
|
||||||
@ -391,7 +389,6 @@ class DeviceConsolePortTable(ConsolePortTable):
|
|||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
@ -433,7 +430,6 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
template_code='<i class="mdi mdi-console-network-outline"></i> '
|
template_code='<i class="mdi mdi-console-network-outline"></i> '
|
||||||
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
@ -482,7 +478,6 @@ class DevicePowerPortTable(PowerPortTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
|
template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
|
||||||
'{{ value }}</a>',
|
'{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
@ -531,7 +526,6 @@ class DevicePowerOutletTable(PowerOutletTable):
|
|||||||
name = tables.TemplateColumn(
|
name = tables.TemplateColumn(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
@ -550,6 +544,11 @@ class DevicePowerOutletTable(PowerOutletTable):
|
|||||||
|
|
||||||
|
|
||||||
class BaseInterfaceTable(NetBoxTable):
|
class BaseInterfaceTable(NetBoxTable):
|
||||||
|
name = tables.Column(
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
linkify=True,
|
||||||
|
order_by=('_name',)
|
||||||
|
)
|
||||||
enabled = columns.BooleanColumn(
|
enabled = columns.BooleanColumn(
|
||||||
verbose_name=_('Enabled'),
|
verbose_name=_('Enabled'),
|
||||||
)
|
)
|
||||||
@ -597,7 +596,7 @@ class BaseInterfaceTable(NetBoxTable):
|
|||||||
return ",".join([str(obj) for obj in value.all()])
|
return ",".join([str(obj) for obj in value.all()])
|
||||||
|
|
||||||
|
|
||||||
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
|
class InterfaceTable(BaseInterfaceTable, ModularDeviceComponentTable, PathEndpointTable):
|
||||||
device = tables.Column(
|
device = tables.Column(
|
||||||
verbose_name=_('Device'),
|
verbose_name=_('Device'),
|
||||||
linkify={
|
linkify={
|
||||||
@ -736,7 +735,6 @@ class DeviceFrontPortTable(FrontPortTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
|
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
|
||||||
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
@ -783,7 +781,6 @@ class DeviceRearPortTable(RearPortTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
|
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
|
||||||
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
@ -846,7 +843,6 @@ class DeviceDeviceBayTable(DeviceBayTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
|
template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
|
||||||
'"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
'"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
@ -915,7 +911,6 @@ class DeviceModuleBayTable(ModuleBayTable):
|
|||||||
name = columns.MPTTColumn(
|
name = columns.MPTTColumn(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
linkify=True,
|
linkify=True,
|
||||||
order_by=Accessor('_name')
|
|
||||||
)
|
)
|
||||||
actions = columns.ActionsColumn(
|
actions = columns.ActionsColumn(
|
||||||
extra_buttons=MODULEBAY_BUTTONS
|
extra_buttons=MODULEBAY_BUTTONS
|
||||||
@ -982,7 +977,6 @@ class DeviceInventoryItemTable(InventoryItemTable):
|
|||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
|
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
|
||||||
'{{ value }}</a>',
|
'{{ value }}</a>',
|
||||||
order_by=Accessor('_name'),
|
|
||||||
attrs={'td': {'class': 'text-nowrap'}}
|
attrs={'td': {'class': 'text-nowrap'}}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -163,9 +163,7 @@ class ComponentTemplateTable(NetBoxTable):
|
|||||||
id = tables.Column(
|
id = tables.Column(
|
||||||
verbose_name=_('ID')
|
verbose_name=_('ID')
|
||||||
)
|
)
|
||||||
name = tables.Column(
|
name = tables.Column()
|
||||||
order_by=('_name',)
|
|
||||||
)
|
|
||||||
|
|
||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
exclude = ('id', )
|
exclude = ('id', )
|
||||||
@ -220,6 +218,10 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceTemplateTable(ComponentTemplateTable):
|
class InterfaceTemplateTable(ComponentTemplateTable):
|
||||||
|
name = tables.Column(
|
||||||
|
verbose_name=_('Name'),
|
||||||
|
order_by=('_name',)
|
||||||
|
)
|
||||||
enabled = columns.BooleanColumn(
|
enabled = columns.BooleanColumn(
|
||||||
verbose_name=_('Enabled'),
|
verbose_name=_('Enabled'),
|
||||||
)
|
)
|
||||||
|
@ -111,7 +111,6 @@ class RackTypeTable(NetBoxTable):
|
|||||||
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
order_by=('_name',),
|
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
location = tables.Column(
|
location = tables.Column(
|
||||||
|
@ -688,8 +688,7 @@ class RackElevationListView(generic.ObjectListView):
|
|||||||
sort = request.GET.get('sort', 'name')
|
sort = request.GET.get('sort', 'name')
|
||||||
if sort not in ORDERING_CHOICES:
|
if sort not in ORDERING_CHOICES:
|
||||||
sort = 'name'
|
sort = 'name'
|
||||||
sort_field = sort.replace("name", "_name") # Use natural ordering
|
racks = racks.order_by(sort)
|
||||||
racks = racks.order_by(sort_field)
|
|
||||||
|
|
||||||
# Pagination
|
# Pagination
|
||||||
per_page = get_paginate_count(request)
|
per_page = get_paginate_count(request)
|
||||||
@ -731,8 +730,8 @@ class RackView(GetRelatedModelsMixin, generic.ObjectView):
|
|||||||
peer_racks = peer_racks.filter(location=instance.location)
|
peer_racks = peer_racks.filter(location=instance.location)
|
||||||
else:
|
else:
|
||||||
peer_racks = peer_racks.filter(location__isnull=True)
|
peer_racks = peer_racks.filter(location__isnull=True)
|
||||||
next_rack = peer_racks.filter(_name__gt=instance._name).first()
|
next_rack = peer_racks.filter(name__gt=instance.name).first()
|
||||||
prev_rack = peer_racks.filter(_name__lt=instance._name).reverse().first()
|
prev_rack = peer_racks.filter(name__lt=instance.name).reverse().first()
|
||||||
|
|
||||||
# Determine any additional parameters to pass when embedding the rack elevations
|
# Determine any additional parameters to pass when embedding the rack elevations
|
||||||
svg_extra = '&'.join([
|
svg_extra = '&'.join([
|
||||||
|
@ -223,7 +223,7 @@ class EventRuleImportForm(NetBoxModelImportForm):
|
|||||||
from extras.scripts import get_module_and_script
|
from extras.scripts import get_module_and_script
|
||||||
module_name, script_name = action_object.split('.', 1)
|
module_name, script_name = action_object.split('.', 1)
|
||||||
try:
|
try:
|
||||||
module, script = get_module_and_script(module_name, script_name)
|
script = get_module_and_script(module_name, script_name)[1]
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise forms.ValidationError(_("Script {name} not found").format(name=action_object))
|
raise forms.ValidationError(_("Script {name} not found").format(name=action_object))
|
||||||
self.instance.action_object = script
|
self.instance.action_object = script
|
||||||
|
@ -38,7 +38,7 @@ class Command(BaseCommand):
|
|||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
module_name, script_name = script.split('.', 1)
|
module_name, script_name = script.split('.', 1)
|
||||||
module, script_obj = get_module_and_script(module_name, script_name)
|
script_obj = get_module_and_script(module_name, script_name)[1]
|
||||||
script = script_obj.python_class
|
script = script_obj.python_class
|
||||||
|
|
||||||
# Take user from command line if provided and exists, other
|
# Take user from command line if provided and exists, other
|
||||||
|
@ -30,7 +30,7 @@ def get_python_name(scriptmodule):
|
|||||||
"""
|
"""
|
||||||
Return the Python name of a ScriptModule's file on disk.
|
Return the Python name of a ScriptModule's file on disk.
|
||||||
"""
|
"""
|
||||||
path, filename = os.path.split(scriptmodule.file_path)
|
filename = os.path.split(scriptmodule.file_path)[0]
|
||||||
return os.path.splitext(filename)[0]
|
return os.path.splitext(filename)[0]
|
||||||
|
|
||||||
|
|
||||||
@ -128,7 +128,7 @@ def update_event_rules(apps, schema_editor):
|
|||||||
|
|
||||||
for eventrule in EventRule.objects.filter(action_object_type=scriptmodule_ct):
|
for eventrule in EventRule.objects.filter(action_object_type=scriptmodule_ct):
|
||||||
name = eventrule.action_parameters.get('script_name')
|
name = eventrule.action_parameters.get('script_name')
|
||||||
obj, created = Script.objects.get_or_create(
|
obj, __ = Script.objects.get_or_create(
|
||||||
module_id=eventrule.action_object_id,
|
module_id=eventrule.action_object_id,
|
||||||
name=name,
|
name=name,
|
||||||
defaults={'is_executable': False}
|
defaults={'is_executable': False}
|
||||||
|
@ -2,8 +2,9 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from drf_spectacular.utils import extend_schema_field
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.constants import LOCATION_SCOPE_TYPES
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS, PREFIX_SCOPE_TYPES
|
from ipam.constants import IPADDRESS_ASSIGNMENT_MODELS
|
||||||
from ipam.models import Aggregate, IPAddress, IPRange, Prefix
|
from ipam.models import Aggregate, IPAddress, IPRange, Prefix
|
||||||
from netbox.api.fields import ChoiceField, ContentTypeField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
from netbox.api.serializers import NetBoxModelSerializer
|
from netbox.api.serializers import NetBoxModelSerializer
|
||||||
@ -47,7 +48,7 @@ class PrefixSerializer(NetBoxModelSerializer):
|
|||||||
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
vrf = VRFSerializer(nested=True, required=False, allow_null=True)
|
||||||
scope_type = ContentTypeField(
|
scope_type = ContentTypeField(
|
||||||
queryset=ContentType.objects.filter(
|
queryset=ContentType.objects.filter(
|
||||||
model__in=PREFIX_SCOPE_TYPES
|
model__in=LOCATION_SCOPE_TYPES
|
||||||
),
|
),
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -18,7 +18,7 @@ class IPAMConfig(AppConfig):
|
|||||||
# Register denormalized fields
|
# Register denormalized fields
|
||||||
denormalized.register(Prefix, '_site', {
|
denormalized.register(Prefix, '_site', {
|
||||||
'_region': 'region',
|
'_region': 'region',
|
||||||
'_sitegroup': 'group',
|
'_site_group': 'group',
|
||||||
})
|
})
|
||||||
denormalized.register(Prefix, '_location', {
|
denormalized.register(Prefix, '_location', {
|
||||||
'_site': 'site',
|
'_site': 'site',
|
||||||
|
@ -23,11 +23,6 @@ VRF_RD_MAX_LENGTH = 21
|
|||||||
PREFIX_LENGTH_MIN = 1
|
PREFIX_LENGTH_MIN = 1
|
||||||
PREFIX_LENGTH_MAX = 127 # IPv6
|
PREFIX_LENGTH_MAX = 127 # IPv6
|
||||||
|
|
||||||
# models values for ContentTypes which may be Prefix scope types
|
|
||||||
PREFIX_SCOPE_TYPES = (
|
|
||||||
'region', 'sitegroup', 'site', 'location',
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# IPAddresses
|
# IPAddresses
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import django_filters
|
import django_filters
|
||||||
import netaddr
|
import netaddr
|
||||||
|
from dcim.base_filtersets import ScopedFilterSet
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
@ -9,7 +10,7 @@ from drf_spectacular.utils import extend_schema_field
|
|||||||
from netaddr.core import AddrFormatError
|
from netaddr.core import AddrFormatError
|
||||||
|
|
||||||
from circuits.models import Provider
|
from circuits.models import Provider
|
||||||
from dcim.models import Device, Interface, Location, Region, Site, SiteGroup
|
from dcim.models import Device, Interface, Region, Site, SiteGroup
|
||||||
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
from utilities.filters import (
|
from utilities.filters import (
|
||||||
@ -273,7 +274,7 @@ class RoleFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description', 'weight')
|
fields = ('id', 'name', 'slug', 'description', 'weight')
|
||||||
|
|
||||||
|
|
||||||
class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
class PrefixFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
|
||||||
family = django_filters.NumberFilter(
|
family = django_filters.NumberFilter(
|
||||||
field_name='prefix',
|
field_name='prefix',
|
||||||
lookup_expr='family'
|
lookup_expr='family'
|
||||||
@ -334,57 +335,6 @@ class PrefixFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
to_field_name='rd',
|
to_field_name='rd',
|
||||||
label=_('VRF (RD)'),
|
label=_('VRF (RD)'),
|
||||||
)
|
)
|
||||||
scope_type = ContentTypeFilter()
|
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=Region.objects.all(),
|
|
||||||
field_name='_region',
|
|
||||||
lookup_expr='in',
|
|
||||||
label=_('Region (ID)'),
|
|
||||||
)
|
|
||||||
region = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=Region.objects.all(),
|
|
||||||
field_name='_region',
|
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Region (slug)'),
|
|
||||||
)
|
|
||||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
field_name='_sitegroup',
|
|
||||||
lookup_expr='in',
|
|
||||||
label=_('Site group (ID)'),
|
|
||||||
)
|
|
||||||
site_group = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=SiteGroup.objects.all(),
|
|
||||||
field_name='_sitegroup',
|
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Site group (slug)'),
|
|
||||||
)
|
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
field_name='_site',
|
|
||||||
label=_('Site (ID)'),
|
|
||||||
)
|
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
|
||||||
field_name='_site__slug',
|
|
||||||
queryset=Site.objects.all(),
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Site (slug)'),
|
|
||||||
)
|
|
||||||
location_id = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=Location.objects.all(),
|
|
||||||
field_name='_location',
|
|
||||||
lookup_expr='in',
|
|
||||||
label=_('Location (ID)'),
|
|
||||||
)
|
|
||||||
location = TreeNodeMultipleChoiceFilter(
|
|
||||||
queryset=Location.objects.all(),
|
|
||||||
field_name='_location',
|
|
||||||
lookup_expr='in',
|
|
||||||
to_field_name='slug',
|
|
||||||
label=_('Location (slug)'),
|
|
||||||
)
|
|
||||||
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
vlan_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
label=_('VLAN (ID)'),
|
label=_('VLAN (ID)'),
|
||||||
|
@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from dcim.forms.mixins import ScopedBulkEditForm
|
||||||
from dcim.models import Region, Site, SiteGroup
|
from dcim.models import Region, Site, SiteGroup
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
@ -205,20 +206,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
nullable_fields = ('description',)
|
nullable_fields = ('description',)
|
||||||
|
|
||||||
|
|
||||||
class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
class PrefixBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
|
||||||
scope_type = ContentTypeChoiceField(
|
|
||||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
|
||||||
widget=HTMXSelect(method='post', attrs={'hx-select': '#form_fields'}),
|
|
||||||
required=False,
|
|
||||||
label=_('Scope type')
|
|
||||||
)
|
|
||||||
scope = DynamicModelChoiceField(
|
|
||||||
label=_('Scope'),
|
|
||||||
queryset=Site.objects.none(), # Initial queryset
|
|
||||||
required=False,
|
|
||||||
disabled=True,
|
|
||||||
selector=True
|
|
||||||
)
|
|
||||||
vlan_group = DynamicModelChoiceField(
|
vlan_group = DynamicModelChoiceField(
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -286,20 +274,6 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
'vlan', 'vrf', 'tenant', 'role', 'scope', 'description', 'comments',
|
'vlan', 'vrf', 'tenant', 'role', 'scope', 'description', 'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
if scope_type_id := get_field_value(self, 'scope_type'):
|
|
||||||
try:
|
|
||||||
scope_type = ContentType.objects.get(pk=scope_type_id)
|
|
||||||
model = scope_type.model_class()
|
|
||||||
self.fields['scope'].queryset = model.objects.all()
|
|
||||||
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
|
|
||||||
self.fields['scope'].disabled = False
|
|
||||||
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
|
class IPRangeBulkEditForm(NetBoxModelBulkEditForm):
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
|
@ -3,6 +3,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Site
|
from dcim.models import Device, Interface, Site
|
||||||
|
from dcim.forms.mixins import ScopedImportForm
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.models import *
|
from ipam.models import *
|
||||||
@ -154,7 +155,7 @@ class RoleImportForm(NetBoxModelImportForm):
|
|||||||
fields = ('name', 'slug', 'weight', 'description', 'tags')
|
fields = ('name', 'slug', 'weight', 'description', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class PrefixImportForm(NetBoxModelImportForm):
|
class PrefixImportForm(ScopedImportForm, NetBoxModelImportForm):
|
||||||
vrf = CSVModelChoiceField(
|
vrf = CSVModelChoiceField(
|
||||||
label=_('VRF'),
|
label=_('VRF'),
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
@ -169,11 +170,6 @@ class PrefixImportForm(NetBoxModelImportForm):
|
|||||||
to_field_name='name',
|
to_field_name='name',
|
||||||
help_text=_('Assigned tenant')
|
help_text=_('Assigned tenant')
|
||||||
)
|
)
|
||||||
scope_type = CSVContentTypeField(
|
|
||||||
queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
|
|
||||||
required=False,
|
|
||||||
label=_('Scope type (app & model)')
|
|
||||||
)
|
|
||||||
vlan_group = CSVModelChoiceField(
|
vlan_group = CSVModelChoiceField(
|
||||||
label=_('VLAN group'),
|
label=_('VLAN group'),
|
||||||
queryset=VLANGroup.objects.all(),
|
queryset=VLANGroup.objects.all(),
|
||||||
@ -208,7 +204,7 @@ class PrefixImportForm(NetBoxModelImportForm):
|
|||||||
'mark_utilized', 'description', 'comments', 'tags',
|
'mark_utilized', 'description', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
labels = {
|
labels = {
|
||||||
'scope_id': 'Scope ID',
|
'scope_id': _('Scope ID'),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, data=None, *args, **kwargs):
|
def __init__(self, data=None, *args, **kwargs):
|
||||||
|
@ -4,6 +4,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Site
|
from dcim.models import Device, Interface, Site
|
||||||
|
from dcim.forms.mixins import ScopedForm
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.formfields import IPNetworkFormField
|
from ipam.formfields import IPNetworkFormField
|
||||||
@ -197,25 +198,12 @@ class RoleForm(NetBoxModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class PrefixForm(TenancyForm, NetBoxModelForm):
|
class PrefixForm(TenancyForm, ScopedForm, NetBoxModelForm):
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
queryset=VRF.objects.all(),
|
queryset=VRF.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
label=_('VRF')
|
label=_('VRF')
|
||||||
)
|
)
|
||||||
scope_type = ContentTypeChoiceField(
|
|
||||||
queryset=ContentType.objects.filter(model__in=PREFIX_SCOPE_TYPES),
|
|
||||||
widget=HTMXSelect(),
|
|
||||||
required=False,
|
|
||||||
label=_('Scope type')
|
|
||||||
)
|
|
||||||
scope = DynamicModelChoiceField(
|
|
||||||
label=_('Scope'),
|
|
||||||
queryset=Site.objects.none(), # Initial queryset
|
|
||||||
required=False,
|
|
||||||
disabled=True,
|
|
||||||
selector=True
|
|
||||||
)
|
|
||||||
vlan = DynamicModelChoiceField(
|
vlan = DynamicModelChoiceField(
|
||||||
queryset=VLAN.objects.all(),
|
queryset=VLAN.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
@ -248,36 +236,6 @@ class PrefixForm(TenancyForm, NetBoxModelForm):
|
|||||||
'tenant', 'description', 'comments', 'tags',
|
'tenant', 'description', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
instance = kwargs.get('instance')
|
|
||||||
initial = kwargs.get('initial', {})
|
|
||||||
|
|
||||||
if instance is not None and instance.scope:
|
|
||||||
initial['scope'] = instance.scope
|
|
||||||
kwargs['initial'] = initial
|
|
||||||
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
if scope_type_id := get_field_value(self, 'scope_type'):
|
|
||||||
try:
|
|
||||||
scope_type = ContentType.objects.get(pk=scope_type_id)
|
|
||||||
model = scope_type.model_class()
|
|
||||||
self.fields['scope'].queryset = model.objects.all()
|
|
||||||
self.fields['scope'].widget.attrs['selector'] = model._meta.label_lower
|
|
||||||
self.fields['scope'].disabled = False
|
|
||||||
self.fields['scope'].label = _(bettertitle(model._meta.verbose_name))
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if self.instance and scope_type_id != self.instance.scope_type_id:
|
|
||||||
self.initial['scope'] = None
|
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
super().clean()
|
|
||||||
|
|
||||||
# Assign the selected scope (if any)
|
|
||||||
self.instance.scope = self.cleaned_data.get('scope')
|
|
||||||
|
|
||||||
|
|
||||||
class IPRangeForm(TenancyForm, NetBoxModelForm):
|
class IPRangeForm(TenancyForm, NetBoxModelForm):
|
||||||
vrf = DynamicModelChoiceField(
|
vrf = DynamicModelChoiceField(
|
||||||
|
@ -154,7 +154,7 @@ class IPRangeType(NetBoxObjectType):
|
|||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.Prefix,
|
models.Prefix,
|
||||||
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_sitegroup'),
|
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'),
|
||||||
filters=PrefixFilter
|
filters=PrefixFilter
|
||||||
)
|
)
|
||||||
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):
|
class PrefixType(NetBoxObjectType, BaseIPAddressFamilyType):
|
||||||
|
@ -108,8 +108,8 @@ class NetIn(Lookup):
|
|||||||
return self.rhs
|
return self.rhs
|
||||||
|
|
||||||
def as_sql(self, qn, connection):
|
def as_sql(self, qn, connection):
|
||||||
lhs, lhs_params = self.process_lhs(qn, connection)
|
lhs = self.process_lhs(qn, connection)[0]
|
||||||
rhs, rhs_params = self.process_rhs(qn, connection)
|
rhs_params = self.process_rhs(qn, connection)[1]
|
||||||
with_mask, without_mask = [], []
|
with_mask, without_mask = [], []
|
||||||
for address in rhs_params[0]:
|
for address in rhs_params[0]:
|
||||||
if '/' in address:
|
if '/' in address:
|
||||||
|
@ -11,11 +11,11 @@ def populate_denormalized_fields(apps, schema_editor):
|
|||||||
prefixes = Prefix.objects.filter(site__isnull=False).prefetch_related('site')
|
prefixes = Prefix.objects.filter(site__isnull=False).prefetch_related('site')
|
||||||
for prefix in prefixes:
|
for prefix in prefixes:
|
||||||
prefix._region_id = prefix.site.region_id
|
prefix._region_id = prefix.site.region_id
|
||||||
prefix._sitegroup_id = prefix.site.group_id
|
prefix._site_group_id = prefix.site.group_id
|
||||||
prefix._site_id = prefix.site_id
|
prefix._site_id = prefix.site_id
|
||||||
# Note: Location cannot be set prior to migration
|
# Note: Location cannot be set prior to migration
|
||||||
|
|
||||||
Prefix.objects.bulk_update(prefixes, ['_region', '_sitegroup', '_site'])
|
Prefix.objects.bulk_update(prefixes, ['_region', '_site_group', '_site'])
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@ -29,22 +29,22 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='prefix',
|
model_name='prefix',
|
||||||
name='_location',
|
name='_location',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.location'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='prefix',
|
model_name='prefix',
|
||||||
name='_region',
|
name='_region',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.region'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='prefix',
|
model_name='prefix',
|
||||||
name='_site',
|
name='_site',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.site'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='prefix',
|
model_name='prefix',
|
||||||
name='_sitegroup',
|
name='_site_group',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='_prefixes', to='dcim.sitegroup'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'),
|
||||||
),
|
),
|
||||||
|
|
||||||
# Populate denormalized FK values
|
# Populate denormalized FK values
|
||||||
|
32
netbox/ipam/migrations/0076_natural_ordering.py
Normal file
32
netbox/ipam/migrations/0076_natural_ordering.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ipam', '0075_vlan_qinq'),
|
||||||
|
('dcim', '0197_natural_sort_collation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='asnrange',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='routetarget',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=21, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vlangroup',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='vrf',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
]
|
@ -16,7 +16,8 @@ class ASNRange(OrganizationalModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import netaddr
|
import netaddr
|
||||||
from django.apps import apps
|
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@ -9,6 +8,7 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from core.models import ObjectType
|
from core.models import ObjectType
|
||||||
|
from dcim.models.mixins import CachedScopeMixin
|
||||||
from ipam.choices import *
|
from ipam.choices import *
|
||||||
from ipam.constants import *
|
from ipam.constants import *
|
||||||
from ipam.fields import IPNetworkField, IPAddressField
|
from ipam.fields import IPNetworkField, IPAddressField
|
||||||
@ -198,7 +198,7 @@ class Role(OrganizationalModel):
|
|||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
class Prefix(ContactsMixin, GetAvailablePrefixesMixin, CachedScopeMixin, PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be scoped to certain
|
A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be scoped to certain
|
||||||
areas and/or assigned to VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role.
|
areas and/or assigned to VRFs. A Prefix must be assigned a status and may optionally be assigned a used-define Role.
|
||||||
@ -208,22 +208,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
verbose_name=_('prefix'),
|
verbose_name=_('prefix'),
|
||||||
help_text=_('IPv4 or IPv6 network with mask')
|
help_text=_('IPv4 or IPv6 network with mask')
|
||||||
)
|
)
|
||||||
scope_type = models.ForeignKey(
|
|
||||||
to='contenttypes.ContentType',
|
|
||||||
on_delete=models.PROTECT,
|
|
||||||
limit_choices_to=Q(model__in=PREFIX_SCOPE_TYPES),
|
|
||||||
related_name='+',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
scope_id = models.PositiveBigIntegerField(
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
scope = GenericForeignKey(
|
|
||||||
ct_field='scope_type',
|
|
||||||
fk_field='scope_id'
|
|
||||||
)
|
|
||||||
vrf = models.ForeignKey(
|
vrf = models.ForeignKey(
|
||||||
to='ipam.VRF',
|
to='ipam.VRF',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -272,36 +256,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
help_text=_("Treat as fully utilized")
|
help_text=_("Treat as fully utilized")
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cached associations to enable efficient filtering
|
|
||||||
_location = models.ForeignKey(
|
|
||||||
to='dcim.Location',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='_prefixes',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
_site = models.ForeignKey(
|
|
||||||
to='dcim.Site',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='_prefixes',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
_region = models.ForeignKey(
|
|
||||||
to='dcim.Region',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='_prefixes',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
_sitegroup = models.ForeignKey(
|
|
||||||
to='dcim.SiteGroup',
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name='_prefixes',
|
|
||||||
blank=True,
|
|
||||||
null=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cached depth & child counts
|
# Cached depth & child counts
|
||||||
_depth = models.PositiveSmallIntegerField(
|
_depth = models.PositiveSmallIntegerField(
|
||||||
default=0,
|
default=0,
|
||||||
@ -368,25 +322,6 @@ class Prefix(ContactsMixin, GetAvailablePrefixesMixin, PrimaryModel):
|
|||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def cache_related_objects(self):
|
|
||||||
self._region = self._sitegroup = self._site = self._location = None
|
|
||||||
if self.scope_type:
|
|
||||||
scope_type = self.scope_type.model_class()
|
|
||||||
if scope_type == apps.get_model('dcim', 'region'):
|
|
||||||
self._region = self.scope
|
|
||||||
elif scope_type == apps.get_model('dcim', 'sitegroup'):
|
|
||||||
self._sitegroup = self.scope
|
|
||||||
elif scope_type == apps.get_model('dcim', 'site'):
|
|
||||||
self._region = self.scope.region
|
|
||||||
self._sitegroup = self.scope.group
|
|
||||||
self._site = self.scope
|
|
||||||
elif scope_type == apps.get_model('dcim', 'location'):
|
|
||||||
self._region = self.scope.site.region
|
|
||||||
self._sitegroup = self.scope.site.group
|
|
||||||
self._site = self.scope.site
|
|
||||||
self._location = self.scope
|
|
||||||
cache_related_objects.alters_data = True
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def family(self):
|
def family(self):
|
||||||
return self.prefix.version if self.prefix else None
|
return self.prefix.version if self.prefix else None
|
||||||
|
@ -35,7 +35,8 @@ class VLANGroup(OrganizationalModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
|
@ -18,7 +18,8 @@ class VRF(PrimaryModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
rd = models.CharField(
|
rd = models.CharField(
|
||||||
max_length=VRF_RD_MAX_LENGTH,
|
max_length=VRF_RD_MAX_LENGTH,
|
||||||
@ -74,7 +75,8 @@ class RouteTarget(PrimaryModel):
|
|||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=VRF_RD_MAX_LENGTH, # Same format options as VRF RD (RFC 4360 section 4)
|
max_length=VRF_RD_MAX_LENGTH, # Same format options as VRF RD (RFC 4360 section 4)
|
||||||
unique=True,
|
unique=True,
|
||||||
help_text=_('Route target value (formatted in accordance with RFC 4360)')
|
help_text=_('Route target value (formatted in accordance with RFC 4360)'),
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
tenant = models.ForeignKey(
|
tenant = models.ForeignKey(
|
||||||
to='tenancy.Tenant',
|
to='tenancy.Tenant',
|
||||||
|
@ -42,7 +42,7 @@ class PrefixOrderingTestCase(OrderingTestBase):
|
|||||||
"""
|
"""
|
||||||
This is a very basic test, which tests both prefixes without VRFs and prefixes with VRFs
|
This is a very basic test, which tests both prefixes without VRFs and prefixes with VRFs
|
||||||
"""
|
"""
|
||||||
vrf1, vrf2, vrf3 = list(VRF.objects.all())
|
vrf1, vrf2 = VRF.objects.all()[:2]
|
||||||
prefixes = (
|
prefixes = (
|
||||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/16')),
|
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/16')),
|
||||||
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/24')),
|
Prefix(status=PrefixStatusChoices.STATUS_ACTIVE, vrf=None, prefix=netaddr.IPNetwork('192.168.0.0/24')),
|
||||||
@ -106,7 +106,7 @@ class PrefixOrderingTestCase(OrderingTestBase):
|
|||||||
VRF A:10.1.1.0/24
|
VRF A:10.1.1.0/24
|
||||||
None: 192.168.0.0/16
|
None: 192.168.0.0/16
|
||||||
"""
|
"""
|
||||||
vrf1, vrf2, vrf3 = list(VRF.objects.all())
|
vrf1 = VRF.objects.first()
|
||||||
prefixes = [
|
prefixes = [
|
||||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('10.0.0.0/8')),
|
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('10.0.0.0/8')),
|
||||||
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('10.0.0.0/16')),
|
Prefix(status=PrefixStatusChoices.STATUS_CONTAINER, vrf=None, prefix=netaddr.IPNetwork('10.0.0.0/16')),
|
||||||
@ -130,7 +130,7 @@ class IPAddressOrderingTestCase(OrderingTestBase):
|
|||||||
"""
|
"""
|
||||||
This function tests ordering with the inclusion of vrfs
|
This function tests ordering with the inclusion of vrfs
|
||||||
"""
|
"""
|
||||||
vrf1, vrf2, vrf3 = list(VRF.objects.all())
|
vrf1, vrf2 = VRF.objects.all()[:2]
|
||||||
addresses = (
|
addresses = (
|
||||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrf1, address=netaddr.IPNetwork('10.0.0.1/24')),
|
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrf1, address=netaddr.IPNetwork('10.0.0.1/24')),
|
||||||
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrf1, address=netaddr.IPNetwork('10.0.1.1/24')),
|
IPAddress(status=IPAddressStatusChoices.STATUS_ACTIVE, vrf=vrf1, address=netaddr.IPNetwork('10.0.1.1/24')),
|
||||||
|
@ -107,7 +107,7 @@ class ObjectPermissionMixin:
|
|||||||
return perms
|
return perms
|
||||||
|
|
||||||
def has_perm(self, user_obj, perm, obj=None):
|
def has_perm(self, user_obj, perm, obj=None):
|
||||||
app_label, action, model_name = resolve_permission(perm)
|
app_label, __, model_name = resolve_permission(perm)
|
||||||
|
|
||||||
# Superusers implicitly have all permissions
|
# Superusers implicitly have all permissions
|
||||||
if user_obj.is_active and user_obj.is_superuser:
|
if user_obj.is_active and user_obj.is_superuser:
|
||||||
|
@ -213,7 +213,6 @@ class PluginTest(TestCase):
|
|||||||
self.assertEqual(get_plugin_config(plugin, 'bar'), None)
|
self.assertEqual(get_plugin_config(plugin, 'bar'), None)
|
||||||
self.assertEqual(get_plugin_config(plugin, 'bar', default=456), 456)
|
self.assertEqual(get_plugin_config(plugin, 'bar', default=456), 456)
|
||||||
|
|
||||||
|
|
||||||
def test_events_pipeline(self):
|
def test_events_pipeline(self):
|
||||||
"""
|
"""
|
||||||
Check that events pipeline is registered.
|
Check that events pipeline is registered.
|
||||||
|
@ -49,7 +49,7 @@ def handler_500(request, template_name=ERROR_500_TEMPLATE_NAME):
|
|||||||
template = loader.get_template(template_name)
|
template = loader.get_template(template_name)
|
||||||
except TemplateDoesNotExist:
|
except TemplateDoesNotExist:
|
||||||
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
|
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
|
||||||
type_, error, traceback = sys.exc_info()
|
type_, error = sys.exc_info()[:2]
|
||||||
|
|
||||||
return HttpResponseServerError(template.render({
|
return HttpResponseServerError(template.render({
|
||||||
'error': error,
|
'error': error,
|
||||||
|
@ -22,6 +22,14 @@
|
|||||||
<th scope="row">{% trans "Status" %}</th>
|
<th scope="row">{% trans "Status" %}</th>
|
||||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{% trans "Scope" %}</th>
|
||||||
|
{% if object.scope %}
|
||||||
|
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
|
||||||
|
{% else %}
|
||||||
|
<td>{{ ''|placeholder }}</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">{% trans "Description" %}</th>
|
<th scope="row">{% trans "Description" %}</th>
|
||||||
<td>{{ object.description|placeholder }}</td>
|
<td>{{ object.description|placeholder }}</td>
|
||||||
|
27
netbox/tenancy/migrations/0017_natural_ordering.py
Normal file
27
netbox/tenancy/migrations/0017_natural_ordering.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0016_charfield_null_choices'),
|
||||||
|
('dcim', '0197_natural_sort_collation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='contact',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tenant',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tenantgroup',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
]
|
@ -56,7 +56,8 @@ class Contact(PrimaryModel):
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
title = models.CharField(
|
title = models.CharField(
|
||||||
verbose_name=_('title'),
|
verbose_name=_('title'),
|
||||||
|
@ -18,7 +18,8 @@ class TenantGroup(NestedGroupModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
@ -39,7 +40,8 @@ class Tenant(ContactsMixin, PrimaryModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
|
@ -129,7 +129,7 @@ def get_annotations_for_serializer(serializer_class, fields_to_include=None):
|
|||||||
|
|
||||||
for field_name, field in serializer_class._declared_fields.items():
|
for field_name, field in serializer_class._declared_fields.items():
|
||||||
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
|
if field_name in fields_to_include and type(field) is RelatedObjectCountField:
|
||||||
related_field = model._meta.get_field(field.relation).field
|
related_field = getattr(model, field.relation).field
|
||||||
annotations[field_name] = count_related(related_field.model, related_field.name)
|
annotations[field_name] = count_related(related_field.model, related_field.name)
|
||||||
|
|
||||||
return annotations
|
return annotations
|
||||||
|
@ -49,7 +49,7 @@ def handle_rest_api_exception(request, *args, **kwargs):
|
|||||||
"""
|
"""
|
||||||
Handle exceptions and return a useful error message for REST API requests.
|
Handle exceptions and return a useful error message for REST API requests.
|
||||||
"""
|
"""
|
||||||
type_, error, traceback = sys.exc_info()
|
type_, error = sys.exc_info()[:2]
|
||||||
data = {
|
data = {
|
||||||
'error': str(error),
|
'error': str(error),
|
||||||
'exception': type_.__name__,
|
'exception': type_.__name__,
|
||||||
|
@ -5,7 +5,6 @@ from django.db import models
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from utilities.ordering import naturalize
|
|
||||||
from .forms.widgets import ColorSelect
|
from .forms.widgets import ColorSelect
|
||||||
from .validators import ColorValidator
|
from .validators import ColorValidator
|
||||||
|
|
||||||
@ -40,7 +39,7 @@ class NaturalOrderingField(models.CharField):
|
|||||||
"""
|
"""
|
||||||
description = "Stores a representation of its target field suitable for natural ordering"
|
description = "Stores a representation of its target field suitable for natural ordering"
|
||||||
|
|
||||||
def __init__(self, target_field, naturalize_function=naturalize, *args, **kwargs):
|
def __init__(self, target_field, naturalize_function, *args, **kwargs):
|
||||||
self.target_field = target_field
|
self.target_field = target_field
|
||||||
self.naturalize_function = naturalize_function
|
self.naturalize_function = naturalize_function
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
@ -83,7 +83,7 @@ class CountersTest(TestCase):
|
|||||||
|
|
||||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||||
def test_mptt_child_delete(self):
|
def test_mptt_child_delete(self):
|
||||||
device1, device2 = Device.objects.all()
|
device1 = Device.objects.first()
|
||||||
inventory_item1 = InventoryItem.objects.create(device=device1, name='Inventory Item 1')
|
inventory_item1 = InventoryItem.objects.create(device=device1, name='Inventory Item 1')
|
||||||
InventoryItem.objects.create(device=device1, name='Inventory Item 2', parent=inventory_item1)
|
InventoryItem.objects.create(device=device1, name='Inventory Item 2', parent=inventory_item1)
|
||||||
device1.refresh_from_db()
|
device1.refresh_from_db()
|
||||||
|
@ -59,6 +59,14 @@ class ClusterSerializer(NetBoxModelSerializer):
|
|||||||
)
|
)
|
||||||
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||||
scope = serializers.SerializerMethodField(read_only=True)
|
scope = serializers.SerializerMethodField(read_only=True)
|
||||||
|
allocated_vcpus = serializers.DecimalField(
|
||||||
|
read_only=True,
|
||||||
|
max_digits=8,
|
||||||
|
decimal_places=2,
|
||||||
|
|
||||||
|
)
|
||||||
|
allocated_memory = serializers.IntegerField(read_only=True)
|
||||||
|
allocated_disk = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
# Related object counts
|
# Related object counts
|
||||||
device_count = RelatedObjectCountField('devices')
|
device_count = RelatedObjectCountField('devices')
|
||||||
@ -69,7 +77,7 @@ class ClusterSerializer(NetBoxModelSerializer):
|
|||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'scope_id', 'scope',
|
'id', 'url', 'display_url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'scope_type', 'scope_id', 'scope',
|
||||||
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
|
||||||
'virtualmachine_count',
|
'virtualmachine_count', 'allocated_vcpus', 'allocated_memory', 'allocated_disk'
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count')
|
brief_fields = ('id', 'url', 'display', 'name', 'description', 'virtualmachine_count')
|
||||||
|
|
||||||
@ -80,5 +88,3 @@ class ClusterSerializer(NetBoxModelSerializer):
|
|||||||
serializer = get_serializer_for_model(obj.scope)
|
serializer = get_serializer_for_model(obj.scope)
|
||||||
context = {'request': self.context['request']}
|
context = {'request': self.context['request']}
|
||||||
return serializer(obj.scope, nested=True, context=context).data
|
return serializer(obj.scope, nested=True, context=context).data
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.db.models import Sum
|
||||||
from rest_framework.routers import APIRootView
|
from rest_framework.routers import APIRootView
|
||||||
|
|
||||||
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
|
from extras.api.mixins import ConfigContextQuerySetMixin, RenderConfigMixin
|
||||||
@ -33,7 +34,11 @@ class ClusterGroupViewSet(NetBoxModelViewSet):
|
|||||||
|
|
||||||
|
|
||||||
class ClusterViewSet(NetBoxModelViewSet):
|
class ClusterViewSet(NetBoxModelViewSet):
|
||||||
queryset = Cluster.objects.all()
|
queryset = Cluster.objects.prefetch_related('virtual_machines').annotate(
|
||||||
|
allocated_vcpus=Sum('virtual_machines__vcpus'),
|
||||||
|
allocated_memory=Sum('virtual_machines__memory'),
|
||||||
|
allocated_disk=Sum('virtual_machines__disk'),
|
||||||
|
)
|
||||||
serializer_class = serializers.ClusterSerializer
|
serializer_class = serializers.ClusterSerializer
|
||||||
filterset_class = filtersets.ClusterFilterSet
|
filterset_class = filtersets.ClusterFilterSet
|
||||||
|
|
||||||
|
@ -2,7 +2,8 @@ import django_filters
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from dcim.filtersets import CommonInterfaceFilterSet, ScopedFilterSet
|
from dcim.filtersets import CommonInterfaceFilterSet
|
||||||
|
from dcim.base_filtersets import ScopedFilterSet
|
||||||
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
|
||||||
from extras.filtersets import LocalConfigContextFilterSet
|
from extras.filtersets import LocalConfigContextFilterSet
|
||||||
from extras.models import ConfigTemplate
|
from extras.models import ConfigTemplate
|
||||||
|
@ -25,7 +25,6 @@ class ComponentType(NetBoxObjectType):
|
|||||||
"""
|
"""
|
||||||
Base type for device/VM components
|
Base type for device/VM components
|
||||||
"""
|
"""
|
||||||
_name: str
|
|
||||||
virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]
|
virtual_machine: Annotated["VirtualMachineType", strawberry.lazy('virtualization.graphql.types')]
|
||||||
|
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ class ClusterType(VLANGroupsMixin, NetBoxObjectType):
|
|||||||
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||||
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||||
], strawberry.union("ClusterScopeType")] | None:
|
], strawberry.union("ClusterScopeType")] | None:
|
||||||
return self.scope
|
return self.scope
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
@ -77,7 +76,6 @@ class ClusterTypeType(OrganizationalObjectType):
|
|||||||
filters=VirtualMachineFilter
|
filters=VirtualMachineFilter
|
||||||
)
|
)
|
||||||
class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
|
class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
|
||||||
_name: str
|
|
||||||
interface_count: BigInt
|
interface_count: BigInt
|
||||||
virtual_disk_count: BigInt
|
virtual_disk_count: BigInt
|
||||||
interface_count: BigInt
|
interface_count: BigInt
|
||||||
@ -102,6 +100,7 @@ class VirtualMachineType(ConfigContextMixin, ContactsMixin, NetBoxObjectType):
|
|||||||
filters=VMInterfaceFilter
|
filters=VMInterfaceFilter
|
||||||
)
|
)
|
||||||
class VMInterfaceType(IPAddressesMixin, ComponentType):
|
class VMInterfaceType(IPAddressesMixin, ComponentType):
|
||||||
|
_name: str
|
||||||
mac_address: str | None
|
mac_address: str | None
|
||||||
parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
parent: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||||
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
bridge: Annotated["VMInterfaceType", strawberry.lazy('virtualization.graphql.types')] | None
|
||||||
|
@ -3,17 +3,17 @@ from django.db import migrations, models
|
|||||||
|
|
||||||
|
|
||||||
def copy_site_assignments(apps, schema_editor):
|
def copy_site_assignments(apps, schema_editor):
|
||||||
"""
|
"""
|
||||||
Copy site ForeignKey values to the scope GFK.
|
Copy site ForeignKey values to the scope GFK.
|
||||||
"""
|
"""
|
||||||
ContentType = apps.get_model('contenttypes', 'ContentType')
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
Cluster = apps.get_model('virtualization', 'Cluster')
|
Cluster = apps.get_model('virtualization', 'Cluster')
|
||||||
Site = apps.get_model('dcim', 'Site')
|
Site = apps.get_model('dcim', 'Site')
|
||||||
|
|
||||||
Cluster.objects.filter(site__isnull=False).update(
|
Cluster.objects.filter(site__isnull=False).update(
|
||||||
scope_type=ContentType.objects.get_for_model(Site),
|
scope_type=ContentType.objects.get_for_model(Site),
|
||||||
scope_id=models.F('site_id')
|
scope_id=models.F('site_id')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-11-14 19:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0196_qinq_svlan'),
|
||||||
|
('virtualization', '0045_clusters_cached_relations'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='_location',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='_region',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='_site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='_site_group',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
43
netbox/virtualization/migrations/0047_natural_ordering.py
Normal file
43
netbox/virtualization/migrations/0047_natural_ordering.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('virtualization', '0046_alter_cluster__location_alter_cluster__region_and_more'),
|
||||||
|
('dcim', '0197_natural_sort_collation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='virtualmachine',
|
||||||
|
options={'ordering': ('name', 'pk')},
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='virtualdisk',
|
||||||
|
options={'ordering': ('virtual_machine', 'name')},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualdisk',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='virtualmachine',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=64),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100),
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='virtualdisk',
|
||||||
|
name='_name',
|
||||||
|
),
|
||||||
|
]
|
@ -50,7 +50,8 @@ class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100
|
max_length=100,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
type = models.ForeignKey(
|
type = models.ForeignKey(
|
||||||
verbose_name=_('type'),
|
verbose_name=_('type'),
|
||||||
|
@ -69,12 +69,8 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=64
|
max_length=64,
|
||||||
)
|
db_collation="natural_sort"
|
||||||
_name = NaturalOrderingField(
|
|
||||||
target_field='name',
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
@ -152,7 +148,7 @@ class VirtualMachine(ContactsMixin, ImageAttachmentsMixin, RenderConfigMixin, Co
|
|||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('_name', 'pk') # Name may be non-unique
|
ordering = ('name', 'pk') # Name may be non-unique
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
Lower('name'), 'cluster', 'tenant',
|
Lower('name'), 'cluster', 'tenant',
|
||||||
@ -273,13 +269,8 @@ class ComponentModel(NetBoxModel):
|
|||||||
)
|
)
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=64
|
max_length=64,
|
||||||
)
|
db_collation="natural_sort"
|
||||||
_name = NaturalOrderingField(
|
|
||||||
target_field='name',
|
|
||||||
naturalize_function=naturalize_interface,
|
|
||||||
max_length=100,
|
|
||||||
blank=True
|
|
||||||
)
|
)
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
verbose_name=_('description'),
|
verbose_name=_('description'),
|
||||||
@ -289,7 +280,6 @@ class ComponentModel(NetBoxModel):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
abstract = True
|
abstract = True
|
||||||
ordering = ('virtual_machine', CollateAsChar('_name'))
|
|
||||||
constraints = (
|
constraints = (
|
||||||
models.UniqueConstraint(
|
models.UniqueConstraint(
|
||||||
fields=('virtual_machine', 'name'),
|
fields=('virtual_machine', 'name'),
|
||||||
@ -311,10 +301,9 @@ class ComponentModel(NetBoxModel):
|
|||||||
|
|
||||||
|
|
||||||
class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
||||||
virtual_machine = models.ForeignKey(
|
name = models.CharField(
|
||||||
to='virtualization.VirtualMachine',
|
verbose_name=_('name'),
|
||||||
on_delete=models.CASCADE,
|
max_length=64,
|
||||||
related_name='interfaces' # Override ComponentModel
|
|
||||||
)
|
)
|
||||||
_name = NaturalOrderingField(
|
_name = NaturalOrderingField(
|
||||||
target_field='name',
|
target_field='name',
|
||||||
@ -322,6 +311,11 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
blank=True
|
blank=True
|
||||||
)
|
)
|
||||||
|
virtual_machine = models.ForeignKey(
|
||||||
|
to='virtualization.VirtualMachine',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='interfaces' # Override ComponentModel
|
||||||
|
)
|
||||||
ip_addresses = GenericRelation(
|
ip_addresses = GenericRelation(
|
||||||
to='ipam.IPAddress',
|
to='ipam.IPAddress',
|
||||||
content_type_field='assigned_object_type',
|
content_type_field='assigned_object_type',
|
||||||
@ -358,6 +352,7 @@ class VMInterface(ComponentModel, BaseInterface, TrackingModelMixin):
|
|||||||
class Meta(ComponentModel.Meta):
|
class Meta(ComponentModel.Meta):
|
||||||
verbose_name = _('interface')
|
verbose_name = _('interface')
|
||||||
verbose_name_plural = _('interfaces')
|
verbose_name_plural = _('interfaces')
|
||||||
|
ordering = ('virtual_machine', CollateAsChar('_name'))
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
@ -416,3 +411,4 @@ class VirtualDisk(ComponentModel, TrackingModelMixin):
|
|||||||
class Meta(ComponentModel.Meta):
|
class Meta(ComponentModel.Meta):
|
||||||
verbose_name = _('virtual disk')
|
verbose_name = _('virtual disk')
|
||||||
verbose_name_plural = _('virtual disks')
|
verbose_name_plural = _('virtual disks')
|
||||||
|
ordering = ('virtual_machine', 'name')
|
||||||
|
@ -53,7 +53,6 @@ VMINTERFACE_BUTTONS = """
|
|||||||
class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
|
||||||
name = tables.Column(
|
name = tables.Column(
|
||||||
verbose_name=_('Name'),
|
verbose_name=_('Name'),
|
||||||
order_by=('_name',),
|
|
||||||
linkify=True
|
linkify=True
|
||||||
)
|
)
|
||||||
status = columns.ChoiceFieldColumn(
|
status = columns.ChoiceFieldColumn(
|
||||||
|
47
netbox/vpn/migrations/0007_natural_ordering.py
Normal file
47
netbox/vpn/migrations/0007_natural_ordering.py
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('vpn', '0006_charfield_null_choices'),
|
||||||
|
('dcim', '0197_natural_sort_collation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ikepolicy',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ikeproposal',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipsecpolicy',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipsecprofile',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='ipsecproposal',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='l2vpn',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tunnel',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
]
|
@ -22,7 +22,8 @@ class IKEProposal(PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
authentication_method = models.CharField(
|
authentication_method = models.CharField(
|
||||||
verbose_name=('authentication method'),
|
verbose_name=('authentication method'),
|
||||||
@ -67,7 +68,8 @@ class IKEPolicy(PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
version = models.PositiveSmallIntegerField(
|
version = models.PositiveSmallIntegerField(
|
||||||
verbose_name=_('version'),
|
verbose_name=_('version'),
|
||||||
@ -125,7 +127,8 @@ class IPSecProposal(PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
encryption_algorithm = models.CharField(
|
encryption_algorithm = models.CharField(
|
||||||
verbose_name=_('encryption'),
|
verbose_name=_('encryption'),
|
||||||
@ -176,7 +179,8 @@ class IPSecPolicy(PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
proposals = models.ManyToManyField(
|
proposals = models.ManyToManyField(
|
||||||
to='vpn.IPSecProposal',
|
to='vpn.IPSecProposal',
|
||||||
@ -211,7 +215,8 @@ class IPSecProfile(PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
mode = models.CharField(
|
mode = models.CharField(
|
||||||
verbose_name=_('mode'),
|
verbose_name=_('mode'),
|
||||||
|
@ -20,7 +20,8 @@ class L2VPN(ContactsMixin, PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
|
@ -31,7 +31,8 @@ class Tunnel(PrimaryModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
status = models.CharField(
|
status = models.CharField(
|
||||||
verbose_name=_('status'),
|
verbose_name=_('status'),
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.constants import LOCATION_SCOPE_TYPES
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from drf_spectacular.utils import extend_schema_field
|
||||||
from ipam.api.serializers_.vlans import VLANSerializer
|
from ipam.api.serializers_.vlans import VLANSerializer
|
||||||
from netbox.api.fields import ChoiceField
|
from netbox.api.fields import ChoiceField, ContentTypeField
|
||||||
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
from netbox.api.serializers import NestedGroupModelSerializer, NetBoxModelSerializer
|
||||||
from tenancy.api.serializers_.tenants import TenantSerializer
|
from tenancy.api.serializers_.tenants import TenantSerializer
|
||||||
|
from utilities.api import get_serializer_for_model
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
from wireless.models import WirelessLAN, WirelessLANGroup
|
from wireless.models import WirelessLAN, WirelessLANGroup
|
||||||
from .nested import NestedWirelessLANGroupSerializer
|
from .nested import NestedWirelessLANGroupSerializer
|
||||||
@ -34,12 +38,30 @@ class WirelessLANSerializer(NetBoxModelSerializer):
|
|||||||
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
tenant = TenantSerializer(nested=True, required=False, allow_null=True)
|
||||||
auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
|
auth_type = ChoiceField(choices=WirelessAuthTypeChoices, required=False, allow_blank=True)
|
||||||
auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
|
auth_cipher = ChoiceField(choices=WirelessAuthCipherChoices, required=False, allow_blank=True)
|
||||||
|
scope_type = ContentTypeField(
|
||||||
|
queryset=ContentType.objects.filter(
|
||||||
|
model__in=LOCATION_SCOPE_TYPES
|
||||||
|
),
|
||||||
|
allow_null=True,
|
||||||
|
required=False,
|
||||||
|
default=None
|
||||||
|
)
|
||||||
|
scope_id = serializers.IntegerField(allow_null=True, required=False, default=None)
|
||||||
|
scope = serializers.SerializerMethodField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display_url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'tenant',
|
'id', 'url', 'display_url', 'display', 'ssid', 'description', 'group', 'status', 'vlan', 'scope_type', 'scope_id', 'scope',
|
||||||
'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields',
|
'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'custom_fields',
|
||||||
'created', 'last_updated',
|
'created', 'last_updated',
|
||||||
]
|
]
|
||||||
brief_fields = ('id', 'url', 'display', 'ssid', 'description')
|
brief_fields = ('id', 'url', 'display', 'ssid', 'description')
|
||||||
|
|
||||||
|
@extend_schema_field(serializers.JSONField(allow_null=True))
|
||||||
|
def get_scope(self, obj):
|
||||||
|
if obj.scope_id is None:
|
||||||
|
return None
|
||||||
|
serializer = get_serializer_for_model(obj.scope)
|
||||||
|
context = {'request': self.context['request']}
|
||||||
|
return serializer(obj.scope, nested=True, context=context).data
|
||||||
|
@ -2,6 +2,7 @@ import django_filters
|
|||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
|
||||||
from dcim.choices import LinkStatusChoices
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from dcim.base_filtersets import ScopedFilterSet
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
|
||||||
@ -43,7 +44,7 @@ class WirelessLANGroupFilterSet(OrganizationalModelFilterSet):
|
|||||||
fields = ('id', 'name', 'slug', 'description')
|
fields = ('id', 'name', 'slug', 'description')
|
||||||
|
|
||||||
|
|
||||||
class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
class WirelessLANFilterSet(NetBoxModelFilterSet, ScopedFilterSet, TenancyFilterSet):
|
||||||
group_id = TreeNodeMultipleChoiceFilter(
|
group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=WirelessLANGroup.objects.all(),
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
field_name='group',
|
field_name='group',
|
||||||
@ -74,7 +75,7 @@ class WirelessLANFilterSet(NetBoxModelFilterSet, TenancyFilterSet):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fields = ('id', 'ssid', 'auth_psk', 'description')
|
fields = ('id', 'ssid', 'auth_psk', 'scope_id', 'description')
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
if not value.strip():
|
if not value.strip():
|
||||||
|
@ -2,6 +2,7 @@ from django import forms
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.choices import LinkStatusChoices
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from dcim.forms.mixins import ScopedBulkEditForm
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.choices import *
|
from netbox.choices import *
|
||||||
from netbox.forms import NetBoxModelBulkEditForm
|
from netbox.forms import NetBoxModelBulkEditForm
|
||||||
@ -39,7 +40,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
nullable_fields = ('parent', 'description')
|
nullable_fields = ('parent', 'description')
|
||||||
|
|
||||||
|
|
||||||
class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
|
class WirelessLANBulkEditForm(ScopedBulkEditForm, NetBoxModelBulkEditForm):
|
||||||
status = forms.ChoiceField(
|
status = forms.ChoiceField(
|
||||||
label=_('Status'),
|
label=_('Status'),
|
||||||
choices=add_blank_choice(WirelessLANStatusChoices),
|
choices=add_blank_choice(WirelessLANStatusChoices),
|
||||||
@ -89,10 +90,11 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm):
|
|||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('group', 'ssid', 'status', 'vlan', 'tenant', 'description'),
|
FieldSet('group', 'ssid', 'status', 'vlan', 'tenant', 'description'),
|
||||||
|
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||||
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
||||||
)
|
)
|
||||||
nullable_fields = (
|
nullable_fields = (
|
||||||
'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'comments',
|
'ssid', 'group', 'vlan', 'tenant', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'scope', 'comments',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.choices import LinkStatusChoices
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from dcim.forms.mixins import ScopedImportForm
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.choices import *
|
from netbox.choices import *
|
||||||
@ -32,7 +33,7 @@ class WirelessLANGroupImportForm(NetBoxModelImportForm):
|
|||||||
fields = ('name', 'slug', 'parent', 'description', 'tags')
|
fields = ('name', 'slug', 'parent', 'description', 'tags')
|
||||||
|
|
||||||
|
|
||||||
class WirelessLANImportForm(NetBoxModelImportForm):
|
class WirelessLANImportForm(ScopedImportForm, NetBoxModelImportForm):
|
||||||
group = CSVModelChoiceField(
|
group = CSVModelChoiceField(
|
||||||
label=_('Group'),
|
label=_('Group'),
|
||||||
queryset=WirelessLANGroup.objects.all(),
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
@ -75,9 +76,12 @@ class WirelessLANImportForm(NetBoxModelImportForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fields = (
|
fields = (
|
||||||
'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'description',
|
'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'scope_type', 'scope_id',
|
||||||
'comments', 'tags',
|
'description', 'comments', 'tags',
|
||||||
)
|
)
|
||||||
|
labels = {
|
||||||
|
'scope_id': _('Scope ID'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class WirelessLinkImportForm(NetBoxModelImportForm):
|
class WirelessLinkImportForm(NetBoxModelImportForm):
|
||||||
|
@ -2,6 +2,7 @@ from django import forms
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.choices import LinkStatusChoices
|
from dcim.choices import LinkStatusChoices
|
||||||
|
from dcim.models import Location, Region, Site, SiteGroup
|
||||||
from netbox.choices import *
|
from netbox.choices import *
|
||||||
from netbox.forms import NetBoxModelFilterSetForm
|
from netbox.forms import NetBoxModelFilterSetForm
|
||||||
from tenancy.forms import TenancyFilterForm
|
from tenancy.forms import TenancyFilterForm
|
||||||
@ -33,6 +34,7 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('q', 'filter_id', 'tag'),
|
FieldSet('q', 'filter_id', 'tag'),
|
||||||
FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
|
FieldSet('ssid', 'group_id', 'status', name=_('Attributes')),
|
||||||
|
FieldSet('region_id', 'site_group_id', 'site_id', 'location_id', name=_('Scope')),
|
||||||
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
FieldSet('tenant_group_id', 'tenant_id', name=_('Tenant')),
|
||||||
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
||||||
)
|
)
|
||||||
@ -65,6 +67,31 @@ class WirelessLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
|
|||||||
label=_('Pre-shared key'),
|
label=_('Pre-shared key'),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
|
region_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Region.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Region')
|
||||||
|
)
|
||||||
|
site_group_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=SiteGroup.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Site group')
|
||||||
|
)
|
||||||
|
site_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Site.objects.all(),
|
||||||
|
required=False,
|
||||||
|
null_option='None',
|
||||||
|
query_params={
|
||||||
|
'region_id': '$region_id',
|
||||||
|
'site_group_id': '$site_group_id',
|
||||||
|
},
|
||||||
|
label=_('Site')
|
||||||
|
)
|
||||||
|
location_id = DynamicModelMultipleChoiceField(
|
||||||
|
queryset=Location.objects.all(),
|
||||||
|
required=False,
|
||||||
|
label=_('Location')
|
||||||
|
)
|
||||||
tag = TagFilterField(model)
|
tag = TagFilterField(model)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ from django.forms import PasswordInput
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from dcim.models import Device, Interface, Location, Site
|
from dcim.models import Device, Interface, Location, Site
|
||||||
|
from dcim.forms.mixins import ScopedForm
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.forms import NetBoxModelForm
|
from netbox.forms import NetBoxModelForm
|
||||||
from tenancy.forms import TenancyForm
|
from tenancy.forms import TenancyForm
|
||||||
@ -35,7 +36,7 @@ class WirelessLANGroupForm(NetBoxModelForm):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class WirelessLANForm(TenancyForm, NetBoxModelForm):
|
class WirelessLANForm(ScopedForm, TenancyForm, NetBoxModelForm):
|
||||||
group = DynamicModelChoiceField(
|
group = DynamicModelChoiceField(
|
||||||
label=_('Group'),
|
label=_('Group'),
|
||||||
queryset=WirelessLANGroup.objects.all(),
|
queryset=WirelessLANGroup.objects.all(),
|
||||||
@ -51,6 +52,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')),
|
FieldSet('ssid', 'group', 'vlan', 'status', 'description', 'tags', name=_('Wireless LAN')),
|
||||||
|
FieldSet('scope_type', 'scope', name=_('Scope')),
|
||||||
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
FieldSet('tenant_group', 'tenant', name=_('Tenancy')),
|
||||||
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
FieldSet('auth_type', 'auth_cipher', 'auth_psk', name=_('Authentication')),
|
||||||
)
|
)
|
||||||
@ -59,7 +61,7 @@ class WirelessLANForm(TenancyForm, NetBoxModelForm):
|
|||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fields = [
|
fields = [
|
||||||
'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
|
'ssid', 'group', 'status', 'vlan', 'tenant_group', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk',
|
||||||
'description', 'comments', 'tags',
|
'scope_type', 'description', 'comments', 'tags',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'auth_psk': PasswordInput(
|
'auth_psk': PasswordInput(
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from typing import Annotated, List
|
from typing import Annotated, List, Union
|
||||||
|
|
||||||
import strawberry
|
import strawberry
|
||||||
import strawberry_django
|
import strawberry_django
|
||||||
@ -28,7 +28,7 @@ class WirelessLANGroupType(OrganizationalObjectType):
|
|||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.WirelessLAN,
|
models.WirelessLAN,
|
||||||
fields='__all__',
|
exclude=('scope_type', 'scope_id', '_location', '_region', '_site', '_site_group'),
|
||||||
filters=WirelessLANFilter
|
filters=WirelessLANFilter
|
||||||
)
|
)
|
||||||
class WirelessLANType(NetBoxObjectType):
|
class WirelessLANType(NetBoxObjectType):
|
||||||
@ -38,6 +38,15 @@ class WirelessLANType(NetBoxObjectType):
|
|||||||
|
|
||||||
interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
interfaces: List[Annotated["InterfaceType", strawberry.lazy('dcim.graphql.types')]]
|
||||||
|
|
||||||
|
@strawberry_django.field
|
||||||
|
def scope(self) -> Annotated[Union[
|
||||||
|
Annotated["LocationType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
Annotated["RegionType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
Annotated["SiteGroupType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
Annotated["SiteType", strawberry.lazy('dcim.graphql.types')],
|
||||||
|
], strawberry.union("WirelessLANScopeType")] | None:
|
||||||
|
return self.scope
|
||||||
|
|
||||||
|
|
||||||
@strawberry_django.type(
|
@strawberry_django.type(
|
||||||
models.WirelessLink,
|
models.WirelessLink,
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-11-04 16:00
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('dcim', '0196_qinq_svlan'),
|
||||||
|
('wireless', '0010_charfield_null_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='_location',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='_%(class)ss',
|
||||||
|
to='dcim.location',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='_region',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='_%(class)ss',
|
||||||
|
to='dcim.region',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='_site',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='_%(class)ss',
|
||||||
|
to='dcim.site',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='_site_group',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='_%(class)ss',
|
||||||
|
to='dcim.sitegroup',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='scope_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='scope_type',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
limit_choices_to=models.Q(('model__in', ('region', 'sitegroup', 'site', 'location'))),
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name='+',
|
||||||
|
to='contenttypes.contenttype',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,41 @@
|
|||||||
|
# Generated by Django 5.0.9 on 2024-11-14 19:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0196_qinq_svlan'),
|
||||||
|
('wireless', '0011_wirelesslan__location_wirelesslan__region_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='_location',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.location'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='_region',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.region'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='_site',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.site'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wirelesslan',
|
||||||
|
name='_site_group',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='dcim.sitegroup'
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
17
netbox/wireless/migrations/0013_natural_ordering.py
Normal file
17
netbox/wireless/migrations/0013_natural_ordering.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('wireless', '0012_alter_wirelesslan__location_and_more'),
|
||||||
|
('dcim', '0197_natural_sort_collation'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='wirelesslangroup',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(db_collation='natural_sort', max_length=100, unique=True),
|
||||||
|
),
|
||||||
|
]
|
@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from dcim.choices import LinkStatusChoices
|
from dcim.choices import LinkStatusChoices
|
||||||
from dcim.constants import WIRELESS_IFACE_TYPES
|
from dcim.constants import WIRELESS_IFACE_TYPES
|
||||||
|
from dcim.models.mixins import CachedScopeMixin
|
||||||
from netbox.models import NestedGroupModel, PrimaryModel
|
from netbox.models import NestedGroupModel, PrimaryModel
|
||||||
from netbox.models.mixins import DistanceMixin
|
from netbox.models.mixins import DistanceMixin
|
||||||
from .choices import *
|
from .choices import *
|
||||||
@ -51,7 +52,8 @@ class WirelessLANGroup(NestedGroupModel):
|
|||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
unique=True
|
unique=True,
|
||||||
|
db_collation="natural_sort"
|
||||||
)
|
)
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
verbose_name=_('slug'),
|
verbose_name=_('slug'),
|
||||||
@ -71,7 +73,7 @@ class WirelessLANGroup(NestedGroupModel):
|
|||||||
verbose_name_plural = _('wireless LAN groups')
|
verbose_name_plural = _('wireless LAN groups')
|
||||||
|
|
||||||
|
|
||||||
class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
|
class WirelessLAN(WirelessAuthenticationBase, CachedScopeMixin, PrimaryModel):
|
||||||
"""
|
"""
|
||||||
A wireless network formed among an arbitrary number of access point and clients.
|
A wireless network formed among an arbitrary number of access point and clients.
|
||||||
"""
|
"""
|
||||||
@ -107,7 +109,7 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
|
|||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
|
||||||
clone_fields = ('ssid', 'group', 'tenant', 'description')
|
clone_fields = ('ssid', 'group', 'scope_type', 'scope_id', 'tenant', 'description')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ('ssid', 'pk')
|
ordering = ('ssid', 'pk')
|
||||||
|
@ -51,6 +51,13 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
status = columns.ChoiceFieldColumn(
|
status = columns.ChoiceFieldColumn(
|
||||||
verbose_name=_('Status'),
|
verbose_name=_('Status'),
|
||||||
)
|
)
|
||||||
|
scope_type = columns.ContentTypeColumn(
|
||||||
|
verbose_name=_('Scope Type'),
|
||||||
|
)
|
||||||
|
scope = tables.Column(
|
||||||
|
verbose_name=_('Scope'),
|
||||||
|
linkify=True
|
||||||
|
)
|
||||||
interface_count = tables.Column(
|
interface_count = tables.Column(
|
||||||
verbose_name=_('Interfaces')
|
verbose_name=_('Interfaces')
|
||||||
)
|
)
|
||||||
@ -65,7 +72,7 @@ class WirelessLANTable(TenancyColumnsMixin, NetBoxTable):
|
|||||||
model = WirelessLAN
|
model = WirelessLAN
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'ssid', 'group', 'status', 'tenant', 'tenant_group', 'vlan', 'interface_count', 'auth_type',
|
'pk', 'ssid', 'group', 'status', 'tenant', 'tenant_group', 'vlan', 'interface_count', 'auth_type',
|
||||||
'auth_cipher', 'auth_psk', 'description', 'comments', 'tags', 'created', 'last_updated',
|
'auth_cipher', 'auth_psk', 'scope', 'scope_type', 'description', 'comments', 'tags', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'ssid', 'group', 'status', 'description', 'vlan', 'auth_type', 'interface_count')
|
default_columns = ('pk', 'ssid', 'group', 'status', 'description', 'vlan', 'auth_type', 'interface_count')
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from dcim.choices import InterfaceTypeChoices
|
from dcim.choices import InterfaceTypeChoices
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface, Site
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
@ -53,6 +53,12 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
tenants = (
|
tenants = (
|
||||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||||
@ -94,6 +100,8 @@ class WirelessLANTest(APIViewTestCases.APIViewTestCase):
|
|||||||
'status': WirelessLANStatusChoices.STATUS_DISABLED,
|
'status': WirelessLANStatusChoices.STATUS_DISABLED,
|
||||||
'tenant': tenants[0].pk,
|
'tenant': tenants[0].pk,
|
||||||
'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE,
|
'auth_type': WirelessAuthTypeChoices.TYPE_WPA_ENTERPRISE,
|
||||||
|
'scope_type': 'dcim.site',
|
||||||
|
'scope_id': sites[1].pk,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
|
from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface, Location, Region, Site, SiteGroup
|
||||||
from ipam.models import VLAN
|
from ipam.models import VLAN
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
@ -110,6 +110,36 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
)
|
)
|
||||||
VLAN.objects.bulk_create(vlans)
|
VLAN.objects.bulk_create(vlans)
|
||||||
|
|
||||||
|
regions = (
|
||||||
|
Region(name='Test Region 1', slug='test-region-1'),
|
||||||
|
Region(name='Test Region 2', slug='test-region-2'),
|
||||||
|
Region(name='Test Region 3', slug='test-region-3'),
|
||||||
|
)
|
||||||
|
for r in regions:
|
||||||
|
r.save()
|
||||||
|
|
||||||
|
site_groups = (
|
||||||
|
SiteGroup(name='Site Group 1', slug='site-group-1'),
|
||||||
|
SiteGroup(name='Site Group 2', slug='site-group-2'),
|
||||||
|
SiteGroup(name='Site Group 3', slug='site-group-3'),
|
||||||
|
)
|
||||||
|
for site_group in site_groups:
|
||||||
|
site_group.save()
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Test Site 1', slug='test-site-1', region=regions[0], group=site_groups[0]),
|
||||||
|
Site(name='Test Site 2', slug='test-site-2', region=regions[1], group=site_groups[1]),
|
||||||
|
Site(name='Test Site 3', slug='test-site-3', region=regions[2], group=site_groups[2]),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
|
locations = (
|
||||||
|
Location(name='Location 1', slug='location-1', site=sites[0]),
|
||||||
|
Location(name='Location 2', slug='location-2', site=sites[2]),
|
||||||
|
)
|
||||||
|
for location in locations:
|
||||||
|
location.save()
|
||||||
|
|
||||||
tenants = (
|
tenants = (
|
||||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||||
@ -127,7 +157,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
|
auth_type=WirelessAuthTypeChoices.TYPE_OPEN,
|
||||||
auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
|
auth_cipher=WirelessAuthCipherChoices.CIPHER_AUTO,
|
||||||
auth_psk='PSK1',
|
auth_psk='PSK1',
|
||||||
description='foobar1'
|
description='foobar1',
|
||||||
|
scope=sites[0]
|
||||||
),
|
),
|
||||||
WirelessLAN(
|
WirelessLAN(
|
||||||
ssid='WLAN2',
|
ssid='WLAN2',
|
||||||
@ -138,7 +169,8 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
auth_type=WirelessAuthTypeChoices.TYPE_WEP,
|
auth_type=WirelessAuthTypeChoices.TYPE_WEP,
|
||||||
auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
|
auth_cipher=WirelessAuthCipherChoices.CIPHER_TKIP,
|
||||||
auth_psk='PSK2',
|
auth_psk='PSK2',
|
||||||
description='foobar2'
|
description='foobar2',
|
||||||
|
scope=locations[0]
|
||||||
),
|
),
|
||||||
WirelessLAN(
|
WirelessLAN(
|
||||||
ssid='WLAN3',
|
ssid='WLAN3',
|
||||||
@ -149,12 +181,14 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
|
auth_type=WirelessAuthTypeChoices.TYPE_WPA_PERSONAL,
|
||||||
auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
|
auth_cipher=WirelessAuthCipherChoices.CIPHER_AES,
|
||||||
auth_psk='PSK3',
|
auth_psk='PSK3',
|
||||||
description='foobar3'
|
description='foobar3',
|
||||||
|
scope=locations[1]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
WirelessLAN.objects.bulk_create(wireless_lans)
|
for wireless_lan in wireless_lans:
|
||||||
|
wireless_lan.save()
|
||||||
|
|
||||||
device = create_test_device('Device 1')
|
device = create_test_device('Device 1', site=sites[0])
|
||||||
interfaces = (
|
interfaces = (
|
||||||
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_80211N),
|
Interface(device=device, name='Interface 1', type=InterfaceTypeChoices.TYPE_80211N),
|
||||||
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_80211N),
|
Interface(device=device, name='Interface 2', type=InterfaceTypeChoices.TYPE_80211N),
|
||||||
@ -217,6 +251,38 @@ class WirelessLANTestCase(TestCase, ChangeLoggedFilterSetTests):
|
|||||||
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
params = {'interface_id': [interfaces[0].pk, interfaces[1].pk]}
|
||||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_region(self):
|
||||||
|
regions = Region.objects.all()[:2]
|
||||||
|
params = {'region_id': [regions[0].pk, regions[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'region': [regions[0].slug, regions[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_site_group(self):
|
||||||
|
site_groups = SiteGroup.objects.all()[:2]
|
||||||
|
params = {'site_group_id': [site_groups[0].pk, site_groups[1].pk]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
params = {'site_group': [site_groups[0].slug, site_groups[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
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(), 2)
|
||||||
|
params = {'site': [sites[0].slug, sites[1].slug]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
def test_location(self):
|
||||||
|
locations = Location.objects.all()[:1]
|
||||||
|
params = {'location_id': [locations[0].pk,]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
params = {'location': [locations[0].slug,]}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||||
|
|
||||||
|
def test_scope_type(self):
|
||||||
|
params = {'scope_type': 'dcim.location'}
|
||||||
|
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||||
|
|
||||||
|
|
||||||
class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
class WirelessLinkTestCase(TestCase, ChangeLoggedFilterSetTests):
|
||||||
queryset = WirelessLink.objects.all()
|
queryset = WirelessLink.objects.all()
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from wireless.choices import *
|
from wireless.choices import *
|
||||||
from wireless.models import *
|
from wireless.models import *
|
||||||
from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
|
from dcim.choices import InterfaceTypeChoices, LinkStatusChoices
|
||||||
from dcim.models import Interface
|
from dcim.models import Interface, Site
|
||||||
from netbox.choices import DistanceUnitChoices
|
from netbox.choices import DistanceUnitChoices
|
||||||
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
|
||||||
@ -56,6 +57,12 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
|
|
||||||
|
sites = (
|
||||||
|
Site(name='Site 1', slug='site-1'),
|
||||||
|
Site(name='Site 2', slug='site-2'),
|
||||||
|
)
|
||||||
|
Site.objects.bulk_create(sites)
|
||||||
|
|
||||||
tenants = (
|
tenants = (
|
||||||
Tenant(name='Tenant 1', slug='tenant-1'),
|
Tenant(name='Tenant 1', slug='tenant-1'),
|
||||||
Tenant(name='Tenant 2', slug='tenant-2'),
|
Tenant(name='Tenant 2', slug='tenant-2'),
|
||||||
@ -98,15 +105,17 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'ssid': 'WLAN2',
|
'ssid': 'WLAN2',
|
||||||
'group': groups[1].pk,
|
'group': groups[1].pk,
|
||||||
'status': WirelessLANStatusChoices.STATUS_DISABLED,
|
'status': WirelessLANStatusChoices.STATUS_DISABLED,
|
||||||
|
'scope_type': ContentType.objects.get_for_model(Site).pk,
|
||||||
|
'scope': sites[1].pk,
|
||||||
'tenant': tenants[1].pk,
|
'tenant': tenants[1].pk,
|
||||||
'tags': [t.pk for t in tags],
|
'tags': [t.pk for t in tags],
|
||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
"group,ssid,status,tenant",
|
"group,ssid,status,tenant,scope_type,scope_id",
|
||||||
f"Wireless LAN Group 2,WLAN4,{WirelessLANStatusChoices.STATUS_ACTIVE},{tenants[0].name}",
|
f"Wireless LAN Group 2,WLAN4,{WirelessLANStatusChoices.STATUS_ACTIVE},{tenants[0].name},,",
|
||||||
f"Wireless LAN Group 2,WLAN5,{WirelessLANStatusChoices.STATUS_DISABLED},{tenants[1].name}",
|
f"Wireless LAN Group 2,WLAN5,{WirelessLANStatusChoices.STATUS_DISABLED},{tenants[1].name},dcim.site,{sites[0].pk}",
|
||||||
f"Wireless LAN Group 2,WLAN6,{WirelessLANStatusChoices.STATUS_RESERVED},{tenants[2].name}",
|
f"Wireless LAN Group 2,WLAN6,{WirelessLANStatusChoices.STATUS_RESERVED},{tenants[2].name},dcim.site,{sites[1].pk}",
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.csv_update_data = (
|
cls.csv_update_data = (
|
||||||
|
Loading…
Reference in New Issue
Block a user