diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 5a0b266f8..a92968bc9 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -8,15 +8,18 @@ ### Breaking Changes * Automatic redirection of legacy slug-based URL paths has been removed. +* The `asn` field has been removed from the site model. Please use the ASN model introduced in NetBox v3.1 to track ASN assignments for sites. +* The `asn` query filter for sites now matches against the AS number of assigned ASNs. * The `contact_name`, `contact_phone`, and `contact_email` fields have been removed from the site model. Please use the new contact model introduced in NetBox v3.1 to store contact information for sites. ### Other Changes * [#7731](https://github.com/netbox-community/netbox/issues/7731) - Require Python 3.8 or later +* [#7743](https://github.com/netbox-community/netbox/issues/7743) - Remove legacy ASN field from site model * [#7748](https://github.com/netbox-community/netbox/issues/7748) - Remove legacy contact fields from site model * [#8031](https://github.com/netbox-community/netbox/issues/8031) - Remove automatic redirection of legacy slug-based URLs ### REST API Changes * dcim.Site - * Removed the `contact_name`, `contact_phone`, and `contact_email` fields + * Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 02c845091..113c71745 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -132,7 +132,7 @@ class SiteSerializer(PrimaryModelSerializer): class Meta: model = Site fields = [ - 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'asns', + 'id', 'url', 'display', 'name', 'slug', 'status', 'region', 'group', 'tenant', 'facility', 'asns', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'prefix_count', 'rack_count', 'virtualmachine_count', 'vlan_count', diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 134a555f4..990c55115 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -131,6 +131,12 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet): to_field_name='slug', label='Group (slug)', ) + asn = django_filters.ModelMultipleChoiceFilter( + field_name='asns__asn', + queryset=ASN.objects.all(), + to_field_name='asn', + label='AS (ID)', + ) asn_id = django_filters.ModelMultipleChoiceFilter( field_name='asns', queryset=ASN.objects.all(), @@ -141,7 +147,7 @@ class SiteFilterSet(PrimaryModelFilterSet, TenancyFilterSet): class Meta: model = Site fields = ( - 'id', 'name', 'slug', 'facility', 'asn', 'latitude', 'longitude', + 'id', 'name', 'slug', 'facility', 'latitude', 'longitude', ) def search(self, queryset, name, value): diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 9127b072f..a40396e98 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -111,12 +111,6 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): queryset=Tenant.objects.all(), required=False ) - asn = forms.IntegerField( - min_value=BGP_ASN_MIN, - max_value=BGP_ASN_MAX, - required=False, - label='ASN' - ) asns = DynamicModelMultipleChoiceField( queryset=ASN.objects.all(), label=_('ASNs'), @@ -134,7 +128,7 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): class Meta: nullable_fields = [ - 'region', 'group', 'tenant', 'asn', 'asns', 'description', 'time_zone', + 'region', 'group', 'tenant', 'asns', 'description', 'time_zone', ] diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 309b203b1..d16cf3dd1 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -130,15 +130,13 @@ class SiteForm(TenancyForm, CustomFieldModelForm): class Meta: model = Site - fields = [ - 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asn', 'asns', - 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', - 'tags', - ] + fields = ( + 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone', + 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags', + ) fieldsets = ( ('Site', ( - 'name', 'slug', 'status', 'region', 'group', 'facility', 'asn', 'asns', 'time_zone', 'description', - 'tags', + 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags', )), ('Tenancy', ('tenant_group', 'tenant')), ('Contact Info', ('physical_address', 'shipping_address', 'latitude', 'longitude')), @@ -159,7 +157,6 @@ class SiteForm(TenancyForm, CustomFieldModelForm): } help_texts = { 'name': "Full name of the site", - 'asn': "BGP autonomous system number. This field is depreciated in favour of the ASN model", 'facility': "Data center provider and facility (e.g. Equinix NY7)", 'time_zone': "Local time zone", 'description': "Short description (will appear in sites list)", diff --git a/netbox/dcim/migrations/0144_site_remove_deprecated_fields.py b/netbox/dcim/migrations/0144_site_remove_deprecated_fields.py index 14554d0a0..1dcf4d43c 100644 --- a/netbox/dcim/migrations/0144_site_remove_deprecated_fields.py +++ b/netbox/dcim/migrations/0144_site_remove_deprecated_fields.py @@ -8,6 +8,10 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RemoveField( + model_name='site', + name='asn', + ), migrations.RemoveField( model_name='site', name='contact_email', diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 5dd05734c..0be7e4617 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -248,12 +248,6 @@ class Site(PrimaryModel): blank=True, help_text='Local facility ID or description' ) - asn = ASNField( - blank=True, - null=True, - verbose_name='ASN', - help_text='32-bit autonomous system number' - ) asns = models.ManyToManyField( to='ipam.ASN', related_name='sites', @@ -307,7 +301,7 @@ class Site(PrimaryModel): ) clone_fields = [ - 'status', 'region', 'group', 'tenant', 'facility', 'asn', 'time_zone', 'description', 'physical_address', + 'status', 'region', 'group', 'tenant', 'facility', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', ] diff --git a/netbox/dcim/tests/test_filtersets.py b/netbox/dcim/tests/test_filtersets.py index 396355ebb..a187c8881 100644 --- a/netbox/dcim/tests/test_filtersets.py +++ b/netbox/dcim/tests/test_filtersets.py @@ -151,9 +151,9 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests): ASN.objects.bulk_create(asns) sites = ( - Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', asn=65001, latitude=10, longitude=10), - Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', asn=65002, latitude=20, longitude=20), - Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', asn=65003, latitude=30, longitude=30), + Site(name='Site 1', slug='site-1', region=regions[0], group=groups[0], tenant=tenants[0], status=SiteStatusChoices.STATUS_ACTIVE, facility='Facility 1', latitude=10, longitude=10), + Site(name='Site 2', slug='site-2', region=regions[1], group=groups[1], tenant=tenants[1], status=SiteStatusChoices.STATUS_PLANNED, facility='Facility 2', latitude=20, longitude=20), + Site(name='Site 3', slug='site-3', region=regions[2], group=groups[2], tenant=tenants[2], status=SiteStatusChoices.STATUS_RETIRED, facility='Facility 3', latitude=30, longitude=30), ) Site.objects.bulk_create(sites) sites[0].asns.set([asns[0]]) @@ -173,7 +173,7 @@ class SiteTestCase(TestCase, ChangeLoggedFilterSetTests): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_asn(self): - params = {'asn': [65001, 65002]} + params = {'asn': ['64512', '64513']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_asn_id(self): diff --git a/netbox/extras/tests/test_customvalidator.py b/netbox/extras/tests/test_customvalidator.py index 89857b615..ce3b572d1 100644 --- a/netbox/extras/tests/test_customvalidator.py +++ b/netbox/extras/tests/test_customvalidator.py @@ -2,6 +2,7 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.test import TestCase, override_settings +from circuits.models import Provider from dcim.models import Site from extras.validators import CustomValidator @@ -66,26 +67,26 @@ custom_validator = MyValidator() class CustomValidatorTest(TestCase): - @override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_validator]}) + @override_settings(CUSTOM_VALIDATORS={'circuits.provider': [min_validator]}) def test_configuration(self): - self.assertIn('dcim.site', settings.CUSTOM_VALIDATORS) - validator = settings.CUSTOM_VALIDATORS['dcim.site'][0] + self.assertIn('circuits.provider', settings.CUSTOM_VALIDATORS) + validator = settings.CUSTOM_VALIDATORS['circuits.provider'][0] self.assertIsInstance(validator, CustomValidator) - @override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_validator]}) + @override_settings(CUSTOM_VALIDATORS={'circuits.provider': [min_validator]}) def test_min(self): with self.assertRaises(ValidationError): - Site(name='abcdef123', slug='abcdefghijk', asn=1).clean() + Provider(name='Provider 1', slug='provider-1', asn=1).clean() - @override_settings(CUSTOM_VALIDATORS={'dcim.site': [max_validator]}) + @override_settings(CUSTOM_VALIDATORS={'circuits.provider': [max_validator]}) def test_max(self): with self.assertRaises(ValidationError): - Site(name='abcdef123', slug='abcdefghijk', asn=65535).clean() + Provider(name='Provider 1', slug='provider-1', asn=65535).clean() @override_settings(CUSTOM_VALIDATORS={'dcim.site': [min_length_validator]}) def test_min_length(self): with self.assertRaises(ValidationError): - Site(name='abc', slug='abc', asn=65000).clean() + Site(name='abc', slug='abc').clean() @override_settings(CUSTOM_VALIDATORS={'dcim.site': [max_length_validator]}) def test_max_length(self): diff --git a/netbox/templates/dcim/site.html b/netbox/templates/dcim/site.html index a8d39be40..539974b86 100644 --- a/netbox/templates/dcim/site.html +++ b/netbox/templates/dcim/site.html @@ -78,10 +78,6 @@ Description {{ object.description|placeholder }} - - AS Number - {{ object.asn|placeholder }} - Time Zone diff --git a/netbox/utilities/tests/test_filters.py b/netbox/utilities/tests/test_filters.py index 2616dbf36..5182722d1 100644 --- a/netbox/utilities/tests/test_filters.py +++ b/netbox/utilities/tests/test_filters.py @@ -5,9 +5,8 @@ from django.test import TestCase from mptt.fields import TreeForeignKey from taggit.managers import TaggableManager -from circuits.choices import CircuitStatusChoices -from circuits.filtersets import CircuitFilterSet -from circuits.models import Circuit, Provider, CircuitType +from circuits.filtersets import CircuitFilterSet, ProviderFilterSet +from circuits.models import Circuit, Provider from dcim.choices import * from dcim.fields import MACAddressField from dcim.filtersets import DeviceFilterSet, SiteFilterSet @@ -337,16 +336,16 @@ class DynamicFilterLookupExpressionTest(TestCase): """ Validate function of automatically generated filters using the Device model as an example. """ - device_queryset = Device.objects.all() - device_filterset = DeviceFilterSet - site_queryset = Site.objects.all() - site_filterset = SiteFilterSet - circuit_queryset = Circuit.objects.all() - circuit_filterset = CircuitFilterSet - @classmethod def setUpTestData(cls): + providers = ( + Provider(name='Provider 1', slug='provider-1', asn=65001), + Provider(name='Provider 2', slug='provider-2', asn=65101), + Provider(name='Provider 3', slug='provider-3', asn=65201), + ) + Provider.objects.bulk_create(providers) + manufacturers = ( Manufacturer(name='Manufacturer 1', slug='manufacturer-1'), Manufacturer(name='Manufacturer 2', slug='manufacturer-2'), @@ -384,9 +383,9 @@ class DynamicFilterLookupExpressionTest(TestCase): region.save() sites = ( - Site(name='Site 1', slug='abc-site-1', region=regions[0], asn=65001), - Site(name='Site 2', slug='def-site-2', region=regions[1], asn=65101), - Site(name='Site 3', slug='ghi-site-3', region=regions[2], asn=65201), + Site(name='Site 1', slug='abc-site-1', region=regions[0]), + Site(name='Site 2', slug='def-site-2', region=regions[1]), + Site(name='Site 3', slug='ghi-site-3', region=regions[2]), ) Site.objects.bulk_create(sites) @@ -429,112 +428,112 @@ class DynamicFilterLookupExpressionTest(TestCase): def test_site_name_negation(self): params = {'name__n': ['Site 1']} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2) def test_site_slug_icontains(self): params = {'slug__ic': ['-1']} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1) def test_site_slug_icontains_negation(self): params = {'slug__nic': ['-1']} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2) def test_site_slug_startswith(self): params = {'slug__isw': ['abc']} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1) def test_site_slug_startswith_negation(self): params = {'slug__nisw': ['abc']} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2) def test_site_slug_endswith(self): params = {'slug__iew': ['-1']} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 1) def test_site_slug_endswith_negation(self): params = {'slug__niew': ['-1']} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2) - def test_site_asn_lt(self): + def test_provider_asn_lt(self): params = {'asn__lt': [65101]} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1) + self.assertEqual(ProviderFilterSet(params, Provider.objects.all()).qs.count(), 1) - def test_site_asn_lte(self): + def test_provider_asn_lte(self): params = {'asn__lte': [65101]} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2) + self.assertEqual(ProviderFilterSet(params, Provider.objects.all()).qs.count(), 2) - def test_site_asn_gt(self): + def test_provider_asn_gt(self): params = {'asn__lt': [65101]} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 1) + self.assertEqual(ProviderFilterSet(params, Provider.objects.all()).qs.count(), 1) - def test_site_asn_gte(self): + def test_provider_asn_gte(self): params = {'asn__gte': [65101]} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2) + self.assertEqual(ProviderFilterSet(params, Provider.objects.all()).qs.count(), 2) def test_site_region_negation(self): params = {'region__n': ['region-1']} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2) def test_site_region_id_negation(self): params = {'region_id__n': [Region.objects.first().pk]} - self.assertEqual(SiteFilterSet(params, self.site_queryset).qs.count(), 2) + self.assertEqual(SiteFilterSet(params, Site.objects.all()).qs.count(), 2) def test_device_name_eq(self): params = {'name': ['Device 1']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1) def test_device_name_negation(self): params = {'name__n': ['Device 1']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 2) def test_device_name_startswith(self): params = {'name__isw': ['Device']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 3) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 3) def test_device_name_startswith_negation(self): params = {'name__nisw': ['Device 1']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 2) def test_device_name_endswith(self): params = {'name__iew': [' 1']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1) def test_device_name_endswith_negation(self): params = {'name__niew': [' 1']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 2) def test_device_name_icontains(self): params = {'name__ic': [' 2']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1) def test_device_name_icontains_negation(self): params = {'name__nic': [' ']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 0) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 0) def test_device_mac_address_negation(self): params = {'mac_address__n': ['00-00-00-00-00-01', 'aa-00-00-00-00-01']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 2) def test_device_mac_address_startswith(self): params = {'mac_address__isw': ['aa:']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1) def test_device_mac_address_startswith_negation(self): params = {'mac_address__nisw': ['aa:']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 2) def test_device_mac_address_endswith(self): params = {'mac_address__iew': [':02']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1) def test_device_mac_address_endswith_negation(self): params = {'mac_address__niew': [':02']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 2) def test_device_mac_address_icontains(self): params = {'mac_address__ic': ['aa:', 'bb']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 2) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 2) def test_device_mac_address_icontains_negation(self): params = {'mac_address__nic': ['aa:', 'bb']} - self.assertEqual(DeviceFilterSet(params, self.device_queryset).qs.count(), 1) + self.assertEqual(DeviceFilterSet(params, Device.objects.all()).qs.count(), 1)