From 695ad47fe9e4f5fa3193fe4f08afd985d6eac4d6 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 19 Sep 2022 10:46:16 -0700 Subject: [PATCH 01/18] 10408 add error message if already exists --- netbox/netbox/views/generic/object_views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index a56a832b6..85604cd8f 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -3,7 +3,7 @@ from copy import deepcopy from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist -from django.db import transaction +from django.db import transaction, IntegrityError from django.db.models import ProtectedError from django.forms.widgets import HiddenInput from django.shortcuts import redirect, render @@ -421,7 +421,11 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): return redirect(return_url) - except (AbortRequest, PermissionsViolation) as e: + except IntegrityError: + form.add_error(None, f"{obj} already exists") + clear_webhooks.send(sender=self) + + except (IntegrityError, AbortRequest, PermissionsViolation) as e: logger.debug(e.message) form.add_error(None, e.message) clear_webhooks.send(sender=self) From 5d07f2c837a140b316598206fa76ead1792a3fde Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 20 Sep 2022 16:03:55 -0700 Subject: [PATCH 02/18] 9892 add FHRP group name --- netbox/ipam/migrations/0061_fhrpgroup_name.py | 18 ++++++++++++++++++ netbox/ipam/models/fhrp.py | 4 ++++ 2 files changed, 22 insertions(+) create mode 100644 netbox/ipam/migrations/0061_fhrpgroup_name.py diff --git a/netbox/ipam/migrations/0061_fhrpgroup_name.py b/netbox/ipam/migrations/0061_fhrpgroup_name.py new file mode 100644 index 000000000..7e232c18f --- /dev/null +++ b/netbox/ipam/migrations/0061_fhrpgroup_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.7 on 2022-09-20 23:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0060_alter_l2vpn_slug'), + ] + + operations = [ + migrations.AddField( + model_name='fhrpgroup', + name='name', + field=models.CharField(blank=True, max_length=100), + ), + ] diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py index 286251444..b022386c8 100644 --- a/netbox/ipam/models/fhrp.py +++ b/netbox/ipam/models/fhrp.py @@ -22,6 +22,10 @@ class FHRPGroup(NetBoxModel): group_id = models.PositiveSmallIntegerField( verbose_name='Group ID' ) + name = models.CharField( + max_length=100, + blank=True + ) protocol = models.CharField( max_length=50, choices=FHRPGroupProtocolChoices From a5421ae1707142c0ceb74399757df765a2a9f8d4 Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 20 Sep 2022 16:13:01 -0700 Subject: [PATCH 03/18] 9892 add FHRP group name --- netbox/ipam/filtersets.py | 2 +- netbox/ipam/tables/fhrp.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 3c0ab1ac8..1eb1ee863 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -653,7 +653,7 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet): class Meta: model = FHRPGroup - fields = ['id', 'group_id', 'auth_key'] + fields = ['id', 'group_id', 'name', 'auth_key'] def search(self, queryset, name, value): if not value.strip(): diff --git a/netbox/ipam/tables/fhrp.py b/netbox/ipam/tables/fhrp.py index f709bfeb2..dc4982e26 100644 --- a/netbox/ipam/tables/fhrp.py +++ b/netbox/ipam/tables/fhrp.py @@ -36,7 +36,7 @@ class FHRPGroupTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = FHRPGroup fields = ( - 'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count', + 'pk', 'name', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count') From 7735634649933e1c64d410a4fbc69f6bc8c975e6 Mon Sep 17 00:00:00 2001 From: Arthur Date: Thu, 22 Sep 2022 10:34:37 -0700 Subject: [PATCH 04/18] 10435 check if vm.cluster in qs --- netbox/ipam/querysets.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index 7edac2eff..b64ae04b8 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -81,30 +81,31 @@ class VLANQuerySet(RestrictedQuerySet): # Find all relevant VLANGroups q = Q() - if vm.cluster.site: - if vm.cluster.site.region: + if vm.cluster: + if vm.cluster.site: + if vm.cluster.site.region: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), + scope_id__in=vm.cluster.site.region.get_ancestors(include_self=True) + ) + if vm.cluster.site.group: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'), + scope_id__in=vm.cluster.site.group.get_ancestors(include_self=True) + ) q |= Q( - scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), - scope_id__in=vm.cluster.site.region.get_ancestors(include_self=True) + scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'), + scope_id=vm.cluster.site_id ) - if vm.cluster.site.group: + if vm.cluster.group: q |= Q( - scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'), - scope_id__in=vm.cluster.site.group.get_ancestors(include_self=True) + scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'), + scope_id=vm.cluster.group_id ) q |= Q( - scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'), - scope_id=vm.cluster.site_id + scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'), + scope_id=vm.cluster_id ) - if vm.cluster.group: - q |= Q( - scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'), - scope_id=vm.cluster.group_id - ) - q |= Q( - scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'), - scope_id=vm.cluster_id - ) vlan_groups = VLANGroup.objects.filter(q) # Return all applicable VLANs @@ -113,7 +114,7 @@ class VLANQuerySet(RestrictedQuerySet): Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs Q(group__isnull=True, site__isnull=True) # Global VLANs ) - if vm.cluster.site: + if vm.cluster and vm.cluster.site: q |= Q(site=vm.cluster.site) return self.filter(q) From c97d2d4fe9c9fdc4f348fc03facdae79c7f2894b Mon Sep 17 00:00:00 2001 From: "Artem I. Kotik" Date: Sat, 24 Sep 2022 15:49:23 +0400 Subject: [PATCH 05/18] Add widget for Airflow field in DeviceTypeForm --- netbox/dcim/forms/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 5728e7f2d..b33023ece 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -373,6 +373,7 @@ class DeviceTypeForm(NetBoxModelForm): 'front_image', 'rear_image', 'comments', 'tags', ] widgets = { + 'airflow': StaticSelect(), 'subdevice_role': StaticSelect(), 'front_image': ClearableFileInput(attrs={ 'accept': DEVICETYPE_IMAGE_FORMATS From 39129ecedfb5755ac1c1c827abf60f5904670d80 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 26 Sep 2022 06:17:02 -0700 Subject: [PATCH 06/18] 10407 fix documentation link to requests (#10409) * 10407 fix documentation link to requests * Append page heading to URL Co-authored-by: jeremystretch --- docs/configuration/system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration/system.md b/docs/configuration/system.md index 21607e566..93f8fa902 100644 --- a/docs/configuration/system.md +++ b/docs/configuration/system.md @@ -58,7 +58,7 @@ Email is sent from NetBox only for critical events or if configured for [logging Default: None -A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://2.python-requests.org/en/master/user/advanced/). For example: +A dictionary of HTTP proxies to use for outbound requests originating from NetBox (e.g. when sending webhook requests). Proxies should be specified by schema (HTTP and HTTPS) as per the [Python requests library documentation](https://requests.readthedocs.io/en/latest/user/advanced/#proxies). For example: ```python HTTP_PROXIES = { From 3ad337dd15b569a644e7f0a1a22fed4970ccf295 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 26 Sep 2022 10:08:54 -0400 Subject: [PATCH 07/18] Filter VLANs and VLANGroups by site or cluster site for VM --- netbox/ipam/querysets.py | 41 +++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/netbox/ipam/querysets.py b/netbox/ipam/querysets.py index b64ae04b8..9f4463f61 100644 --- a/netbox/ipam/querysets.py +++ b/netbox/ipam/querysets.py @@ -81,31 +81,34 @@ class VLANQuerySet(RestrictedQuerySet): # Find all relevant VLANGroups q = Q() + site = vm.site or vm.cluster.site if vm.cluster: - if vm.cluster.site: - if vm.cluster.site.region: - q |= Q( - scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), - scope_id__in=vm.cluster.site.region.get_ancestors(include_self=True) - ) - if vm.cluster.site.group: - q |= Q( - scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'), - scope_id__in=vm.cluster.site.group.get_ancestors(include_self=True) - ) - q |= Q( - scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'), - scope_id=vm.cluster.site_id - ) + # Add VLANGroups scoped to the assigned cluster (or its group) + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'), + scope_id=vm.cluster_id + ) if vm.cluster.group: q |= Q( scope_type=ContentType.objects.get_by_natural_key('virtualization', 'clustergroup'), scope_id=vm.cluster.group_id ) + if site: + # Add VLANGroups scoped to the assigned site (or its group or region) q |= Q( - scope_type=ContentType.objects.get_by_natural_key('virtualization', 'cluster'), - scope_id=vm.cluster_id + scope_type=ContentType.objects.get_by_natural_key('dcim', 'site'), + scope_id=site.pk ) + if site.region: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'region'), + scope_id__in=site.region.get_ancestors(include_self=True) + ) + if site.group: + q |= Q( + scope_type=ContentType.objects.get_by_natural_key('dcim', 'sitegroup'), + scope_id__in=site.group.get_ancestors(include_self=True) + ) vlan_groups = VLANGroup.objects.filter(q) # Return all applicable VLANs @@ -114,7 +117,7 @@ class VLANQuerySet(RestrictedQuerySet): Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs Q(group__isnull=True, site__isnull=True) # Global VLANs ) - if vm.cluster and vm.cluster.site: - q |= Q(site=vm.cluster.site) + if site: + q |= Q(site=site) return self.filter(q) From fd89ef04b6eac975bf7acf2cbada4810442c0bf3 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 26 Sep 2022 10:24:40 -0400 Subject: [PATCH 08/18] Revert "10408 add error message if already exists" --- netbox/netbox/views/generic/object_views.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/netbox/netbox/views/generic/object_views.py b/netbox/netbox/views/generic/object_views.py index 85604cd8f..a56a832b6 100644 --- a/netbox/netbox/views/generic/object_views.py +++ b/netbox/netbox/views/generic/object_views.py @@ -3,7 +3,7 @@ from copy import deepcopy from django.contrib import messages from django.core.exceptions import ObjectDoesNotExist -from django.db import transaction, IntegrityError +from django.db import transaction from django.db.models import ProtectedError from django.forms.widgets import HiddenInput from django.shortcuts import redirect, render @@ -421,11 +421,7 @@ class ObjectEditView(GetReturnURLMixin, BaseObjectView): return redirect(return_url) - except IntegrityError: - form.add_error(None, f"{obj} already exists") - clear_webhooks.send(sender=self) - - except (IntegrityError, AbortRequest, PermissionsViolation) as e: + except (AbortRequest, PermissionsViolation) as e: logger.debug(e.message) form.add_error(None, e.message) clear_webhooks.send(sender=self) From 96784640e371d12c0dcd97d15ceae4a45663f8c0 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 26 Sep 2022 10:27:35 -0400 Subject: [PATCH 09/18] Changelog for #10435, #10439 --- docs/release-notes/version-3.3.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index 71f5605f9..2955e17d5 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -2,13 +2,17 @@ ## v3.3.5 (FUTURE) +### Bug Fixes + +* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view +* [#10435](https://github.com/netbox-community/netbox/issues/10435) - Fix exception when filtering VLANs by virtual machine with no cluster assigned +* [#10439](https://github.com/netbox-community/netbox/issues/10439) - Fix form widget styling for DeviceType airflow field + --- ## v3.3.4 (2022-09-16) ### Bug Fixes - -* [#9497](https://github.com/netbox-community/netbox/issues/9497) - Adjust non-racked device filter on site and location detailed view * [#10383](https://github.com/netbox-community/netbox/issues/10383) - Fix assignment of component templates to module types via web UI * [#10387](https://github.com/netbox-community/netbox/issues/10387) - Fix `MultiValueDictKeyError` exception when editing a device interface From e11d5941222312cde96429b411d04c88db8f300c Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Sep 2022 11:39:43 -0700 Subject: [PATCH 10/18] 9892 add supporting tests, form fields --- docs/models/ipam/fhrpgroup.md | 8 ++++++++ netbox/ipam/api/serializers.py | 2 +- netbox/ipam/filtersets.py | 3 ++- netbox/ipam/forms/bulk_edit.py | 4 ++-- netbox/ipam/forms/bulk_import.py | 2 +- netbox/ipam/forms/filtersets.py | 5 ++++- netbox/ipam/forms/models.py | 4 ++-- netbox/ipam/tables/fhrp.py | 2 +- netbox/ipam/tests/test_api.py | 1 + netbox/ipam/tests/test_filtersets.py | 6 +++++- netbox/ipam/tests/test_views.py | 1 + netbox/templates/ipam/fhrpgroup.html | 4 ++++ netbox/templates/ipam/fhrpgroup_edit.html | 1 + 13 files changed, 33 insertions(+), 10 deletions(-) diff --git a/docs/models/ipam/fhrpgroup.md b/docs/models/ipam/fhrpgroup.md index 4da390310..ddb3010e2 100644 --- a/docs/models/ipam/fhrpgroup.md +++ b/docs/models/ipam/fhrpgroup.md @@ -19,6 +19,14 @@ The wire protocol employed by cooperating servers to maintain the virtual [IP ad The group's numeric identifier. +### Name + +An optional name for the FHRP group. + +### Description + +A brief description of the FHRP group. + ### Authentication Type The type of authentication employed by group nodes, if any. diff --git a/netbox/ipam/api/serializers.py b/netbox/ipam/api/serializers.py index fa8b563e9..8a8fc03d6 100644 --- a/netbox/ipam/api/serializers.py +++ b/netbox/ipam/api/serializers.py @@ -123,7 +123,7 @@ class FHRPGroupSerializer(NetBoxModelSerializer): class Meta: model = FHRPGroup fields = [ - 'id', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_addresses', + 'id', 'name', 'url', 'display', 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'tags', 'custom_fields', 'created', 'last_updated', ] diff --git a/netbox/ipam/filtersets.py b/netbox/ipam/filtersets.py index 1eb1ee863..360cf2a56 100644 --- a/netbox/ipam/filtersets.py +++ b/netbox/ipam/filtersets.py @@ -659,7 +659,8 @@ class FHRPGroupFilterSet(NetBoxModelFilterSet): if not value.strip(): return queryset return queryset.filter( - Q(description__icontains=value) + Q(description__icontains=value) | + Q(name__icontains=value) ) def filter_related_ip(self, queryset, name, value): diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 5f579b07f..7746b6625 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -328,10 +328,10 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): model = FHRPGroup fieldsets = ( - (None, ('protocol', 'group_id', 'description')), + (None, ('protocol', 'group_id', 'name', 'description')), ('Authentication', ('auth_type', 'auth_key')), ) - nullable_fields = ('auth_type', 'auth_key', 'description') + nullable_fields = ('auth_type', 'auth_key', 'name', 'description') class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 880d2722f..6a9dd91ac 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -326,7 +326,7 @@ class FHRPGroupCSVForm(NetBoxModelCSVForm): class Meta: model = FHRPGroup - fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'description') + fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description') class VLANGroupCSVForm(NetBoxModelCSVForm): diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index ecf63b49f..a2ff7085b 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -335,9 +335,12 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): model = FHRPGroup fieldsets = ( (None, ('q', 'tag')), - ('Attributes', ('protocol', 'group_id')), + ('Attributes', ('name', 'protocol', 'group_id')), ('Authentication', ('auth_type', 'auth_key')), ) + name = forms.CharField( + required=False + ) protocol = MultipleChoiceField( choices=FHRPGroupProtocolChoices, required=False diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 724812585..dea42065c 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -527,7 +527,7 @@ class FHRPGroupForm(NetBoxModelForm): ) fieldsets = ( - ('FHRP Group', ('protocol', 'group_id', 'description', 'tags')), + ('FHRP Group', ('protocol', 'group_id', 'name', 'description', 'tags')), ('Authentication', ('auth_type', 'auth_key')), ('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status')) ) @@ -535,7 +535,7 @@ class FHRPGroupForm(NetBoxModelForm): class Meta: model = FHRPGroup fields = ( - 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags', + 'protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags', ) def save(self, *args, **kwargs): diff --git a/netbox/ipam/tables/fhrp.py b/netbox/ipam/tables/fhrp.py index dc4982e26..cfbfb036b 100644 --- a/netbox/ipam/tables/fhrp.py +++ b/netbox/ipam/tables/fhrp.py @@ -36,7 +36,7 @@ class FHRPGroupTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = FHRPGroup fields = ( - 'pk', 'name', 'group_id', 'protocol', 'auth_type', 'auth_key', 'description', 'ip_addresses', 'member_count', + 'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'name', 'description', 'ip_addresses', 'member_count', 'tags', 'created', 'last_updated', ) default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count') diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 0fefb0162..1bbeb0487 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -552,6 +552,7 @@ class FHRPGroupTest(APIViewTestCases.APIViewTestCase): 'group_id': 200, 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, 'auth_key': 'foobarbaz999', + 'name': 'foobar-999' 'description': 'New description', } diff --git a/netbox/ipam/tests/test_filtersets.py b/netbox/ipam/tests/test_filtersets.py index 5c4113786..abb5a3cc3 100644 --- a/netbox/ipam/tests/test_filtersets.py +++ b/netbox/ipam/tests/test_filtersets.py @@ -932,7 +932,7 @@ class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests): fhrp_groups = ( FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP2, group_id=10, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_PLAINTEXT, auth_key='foo123'), - FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='bar456'), + FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_VRRP3, group_id=20, auth_type=FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, auth_key='bar456', name='bar123'), FHRPGroup(protocol=FHRPGroupProtocolChoices.PROTOCOL_HSRP, group_id=30), ) FHRPGroup.objects.bulk_create(fhrp_groups) @@ -956,6 +956,10 @@ class FHRPGroupTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'auth_key': ['foo123', 'bar456']} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_name(self): + params = {'name': ['bar123', ]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) + def test_related_ip(self): # Create some regular IPs to query for related IPs ipaddresses = ( diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index 27520229a..dc9b89141 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -524,6 +524,7 @@ class FHRPGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, 'auth_key': 'abc123def456', 'description': 'Blah blah blah', + 'name': 'test123 name' 'tags': [t.pk for t in tags], } diff --git a/netbox/templates/ipam/fhrpgroup.html b/netbox/templates/ipam/fhrpgroup.html index b4911ce44..89fc7083c 100644 --- a/netbox/templates/ipam/fhrpgroup.html +++ b/netbox/templates/ipam/fhrpgroup.html @@ -26,6 +26,10 @@ Group ID {{ object.group_id }} + + Name + {{ object.name|placeholder }} + Description {{ object.description|placeholder }} diff --git a/netbox/templates/ipam/fhrpgroup_edit.html b/netbox/templates/ipam/fhrpgroup_edit.html index 858d265ab..02816b440 100644 --- a/netbox/templates/ipam/fhrpgroup_edit.html +++ b/netbox/templates/ipam/fhrpgroup_edit.html @@ -8,6 +8,7 @@ {% render_field form.protocol %} {% render_field form.group_id %} + {% render_field form.name %} {% render_field form.description %} {% render_field form.tags %} From 3eeb31d5777a434a5e0250cd128a4fdb83039515 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Sep 2022 11:49:51 -0700 Subject: [PATCH 11/18] 9892 doh - fix tests --- netbox/ipam/tests/test_api.py | 2 +- netbox/ipam/tests/test_views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 1bbeb0487..ea6441650 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -552,7 +552,7 @@ class FHRPGroupTest(APIViewTestCases.APIViewTestCase): 'group_id': 200, 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, 'auth_key': 'foobarbaz999', - 'name': 'foobar-999' + 'name': 'foobar-999', 'description': 'New description', } diff --git a/netbox/ipam/tests/test_views.py b/netbox/ipam/tests/test_views.py index dc9b89141..5cc8fad24 100644 --- a/netbox/ipam/tests/test_views.py +++ b/netbox/ipam/tests/test_views.py @@ -524,7 +524,7 @@ class FHRPGroupTestCase(ViewTestCases.PrimaryObjectViewTestCase): 'auth_type': FHRPGroupAuthTypeChoices.AUTHENTICATION_MD5, 'auth_key': 'abc123def456', 'description': 'Blah blah blah', - 'name': 'test123 name' + 'name': 'test123 name', 'tags': [t.pk for t in tags], } From 5ce805db2e2051b96e174014bbf6d258f65a8433 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Sep 2022 12:56:16 -0700 Subject: [PATCH 12/18] 9892 doh - fix tests --- netbox/ipam/forms/bulk_edit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 7746b6625..67bcf83fb 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -321,6 +321,10 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): required=False, label='Authentication key' ) + name = forms.CharField( + max_length=100, + required=False + ) description = forms.CharField( max_length=200, required=False From 10cb3c2c3d3550baea4d22ec7c998e1497856571 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Sep 2022 13:54:49 -0700 Subject: [PATCH 13/18] 9892 add name to str --- netbox/ipam/models/fhrp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py index b022386c8..fe61a7fec 100644 --- a/netbox/ipam/models/fhrp.py +++ b/netbox/ipam/models/fhrp.py @@ -59,7 +59,7 @@ class FHRPGroup(NetBoxModel): verbose_name = 'FHRP group' def __str__(self): - name = f'{self.get_protocol_display()}: {self.group_id}' + name = f'{self.name} {self.get_protocol_display()}: {self.group_id}' # Append the first assigned IP addresses (if any) to serve as an additional identifier if self.pk: From 8103ad3b9e441bedf09f22278604b9d966caddc3 Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 26 Sep 2022 13:57:09 -0700 Subject: [PATCH 14/18] 9892 add name to str --- netbox/ipam/models/fhrp.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py index fe61a7fec..88e6e19d9 100644 --- a/netbox/ipam/models/fhrp.py +++ b/netbox/ipam/models/fhrp.py @@ -59,7 +59,11 @@ class FHRPGroup(NetBoxModel): verbose_name = 'FHRP group' def __str__(self): - name = f'{self.name} {self.get_protocol_display()}: {self.group_id}' + name = '' + if self.name: + name = f'{self.name} ' + + name += f'{self.get_protocol_display()}: {self.group_id}' # Append the first assigned IP addresses (if any) to serve as an additional identifier if self.pk: From 6016e1b15daade776be0d407c51ae53e707f7d6e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 27 Sep 2022 09:55:35 -0400 Subject: [PATCH 15/18] Changelog & cleanup for #9892 --- docs/models/ipam/fhrpgroup.md | 4 ---- docs/release-notes/version-3.4.md | 9 +++++++++ netbox/ipam/tables/fhrp.py | 8 +++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/models/ipam/fhrpgroup.md b/docs/models/ipam/fhrpgroup.md index ddb3010e2..de09fee29 100644 --- a/docs/models/ipam/fhrpgroup.md +++ b/docs/models/ipam/fhrpgroup.md @@ -23,10 +23,6 @@ The group's numeric identifier. An optional name for the FHRP group. -### Description - -A brief description of the FHRP group. - ### Authentication Type The type of authentication employed by group nodes, if any. diff --git a/docs/release-notes/version-3.4.md b/docs/release-notes/version-3.4.md index 39c44f38e..257ffd625 100644 --- a/docs/release-notes/version-3.4.md +++ b/docs/release-notes/version-3.4.md @@ -3,6 +3,10 @@ !!! warning "PostgreSQL 11 Required" NetBox v3.4 requires PostgreSQL 11 or later. +### Enhancements + +* [#9892](https://github.com/netbox-community/netbox/issues/9892) - Add optional `name` field for FHRP groups + ### Plugins API * [#10314](https://github.com/netbox-community/netbox/issues/10314) - Move `clone()` method from NetBoxModel to CloningMixin @@ -10,3 +14,8 @@ ### Other Changes * [#10358](https://github.com/netbox-community/netbox/issues/10358) - Raise minimum required PostgreSQL version from 10 to 11 + +### REST API Changes + +* ipam.FHRPGroup + * Added optional `name` field diff --git a/netbox/ipam/tables/fhrp.py b/netbox/ipam/tables/fhrp.py index cfbfb036b..beffdd232 100644 --- a/netbox/ipam/tables/fhrp.py +++ b/netbox/ipam/tables/fhrp.py @@ -36,10 +36,12 @@ class FHRPGroupTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = FHRPGroup fields = ( - 'pk', 'group_id', 'protocol', 'auth_type', 'auth_key', 'name', 'description', 'ip_addresses', 'member_count', - 'tags', 'created', 'last_updated', + 'pk', 'group_id', 'protocol', 'name', 'auth_type', 'auth_key', 'description', 'ip_addresses', + 'member_count', 'tags', 'created', 'last_updated', + ) + default_columns = ( + 'pk', 'group_id', 'protocol', 'name', 'auth_type', 'description', 'ip_addresses', 'member_count', ) - default_columns = ('pk', 'group_id', 'protocol', 'auth_type', 'description', 'ip_addresses', 'member_count') class FHRPGroupAssignmentTable(NetBoxTable): From ec6457bcd386dd08061203959fe0c27d79bb9ca7 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 14 Sep 2022 09:16:25 -0400 Subject: [PATCH 16/18] Remove custom validate_unique() methods --- .../migrations/0162_unique_constraints.py | 81 +++++++++++++++++++ netbox/dcim/models/devices.py | 40 +++++---- netbox/dcim/models/sites.py | 60 +++----------- netbox/dcim/tests/test_models.py | 6 +- .../migrations/0033_unique_constraints.py | 25 ++++++ netbox/virtualization/models.py | 30 +++---- 6 files changed, 153 insertions(+), 89 deletions(-) create mode 100644 netbox/dcim/migrations/0162_unique_constraints.py create mode 100644 netbox/virtualization/migrations/0033_unique_constraints.py diff --git a/netbox/dcim/migrations/0162_unique_constraints.py b/netbox/dcim/migrations/0162_unique_constraints.py new file mode 100644 index 000000000..08c113f50 --- /dev/null +++ b/netbox/dcim/migrations/0162_unique_constraints.py @@ -0,0 +1,81 @@ +# Generated by Django 4.1.1 on 2022-09-14 20:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0161_cabling_cleanup'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='location', + name='dcim_location_name', + ), + migrations.RemoveConstraint( + model_name='location', + name='dcim_location_slug', + ), + migrations.RemoveConstraint( + model_name='region', + name='dcim_region_name', + ), + migrations.RemoveConstraint( + model_name='region', + name='dcim_region_slug', + ), + migrations.RemoveConstraint( + model_name='sitegroup', + name='dcim_sitegroup_name', + ), + migrations.RemoveConstraint( + model_name='sitegroup', + name='dcim_sitegroup_slug', + ), + migrations.AlterUniqueTogether( + name='device', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint(fields=('name', 'site', 'tenant'), name='dcim_device_unique_name_site_tenant'), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint(condition=models.Q(('tenant__isnull', True)), fields=('name', 'site'), name='dcim_device_unique_name_site', violation_error_message='Device name must be unique per site.'), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint(fields=('rack', 'position', 'face'), name='dcim_device_unique_rack_position_face'), + ), + migrations.AddConstraint( + model_name='device', + constraint=models.UniqueConstraint(fields=('virtual_chassis', 'vc_position'), name='dcim_device_unique_virtual_chassis_vc_position'), + ), + migrations.AddConstraint( + model_name='location', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('site', 'name'), name='dcim_location_name', violation_error_message='A location with this name already exists within the specified site.'), + ), + migrations.AddConstraint( + model_name='location', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('site', 'slug'), name='dcim_location_slug', violation_error_message='A location with this slug already exists within the specified site.'), + ), + migrations.AddConstraint( + model_name='region', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='dcim_region_name', violation_error_message='A top-level region with this name already exists.'), + ), + migrations.AddConstraint( + model_name='region', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('slug',), name='dcim_region_slug', violation_error_message='A top-level region with this slug already exists.'), + ), + migrations.AddConstraint( + model_name='sitegroup', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='dcim_sitegroup_name', violation_error_message='A top-level site group with this name already exists.'), + ), + migrations.AddConstraint( + model_name='sitegroup', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('slug',), name='dcim_sitegroup_slug', violation_error_message='A top-level site group with this slug already exists.'), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 7858960a1..eb21e532b 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -651,10 +651,25 @@ class Device(NetBoxModel, ConfigContextModel): class Meta: ordering = ('_name', 'pk') # Name may be null - unique_together = ( - ('site', 'tenant', 'name'), # See validate_unique below - ('rack', 'position', 'face'), - ('virtual_chassis', 'vc_position'), + constraints = ( + models.UniqueConstraint( + name='dcim_device_unique_name_site_tenant', + fields=('name', 'site', 'tenant') + ), + models.UniqueConstraint( + name='dcim_device_unique_name_site', + fields=('name', 'site'), + condition=Q(tenant__isnull=True), + violation_error_message="Device name must be unique per site." + ), + models.UniqueConstraint( + name='dcim_device_unique_rack_position_face', + fields=('rack', 'position', 'face') + ), + models.UniqueConstraint( + name='dcim_device_unique_virtual_chassis_vc_position', + fields=('virtual_chassis', 'vc_position') + ), ) def __str__(self): @@ -679,23 +694,6 @@ class Device(NetBoxModel, ConfigContextModel): def get_absolute_url(self): return reverse('dcim:device', args=[self.pk]) - def validate_unique(self, exclude=None): - - # Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary - # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation - # of the uniqueness constraint without manual intervention. - if self.name and hasattr(self, 'site') and self.tenant is None: - if Device.objects.exclude(pk=self.pk).filter( - name=self.name, - site=self.site, - tenant__isnull=True - ): - raise ValidationError({ - 'name': 'A device with this name already exists.' - }) - - super().validate_unique(exclude) - def clean(self): super().clean() diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index f5c8e6d9d..90f855741 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -67,7 +67,8 @@ class Region(NestedGroupModel): models.UniqueConstraint( fields=('name',), name='dcim_region_name', - condition=Q(parent=None) + condition=Q(parent__isnull=True), + violation_error_message="A top-level region with this name already exists." ), models.UniqueConstraint( fields=('parent', 'slug'), @@ -76,24 +77,11 @@ class Region(NestedGroupModel): models.UniqueConstraint( fields=('slug',), name='dcim_region_slug', - condition=Q(parent=None) + condition=Q(parent__isnull=True), + violation_error_message="A top-level region with this slug already exists." ), ) - def validate_unique(self, exclude=None): - if self.parent is None: - regions = Region.objects.exclude(pk=self.pk) - if regions.filter(name=self.name, parent__isnull=True).exists(): - raise ValidationError({ - 'name': 'A region with this name already exists.' - }) - if regions.filter(slug=self.slug, parent__isnull=True).exists(): - raise ValidationError({ - 'name': 'A region with this slug already exists.' - }) - - super().validate_unique(exclude=exclude) - def get_absolute_url(self): return reverse('dcim:region', args=[self.pk]) @@ -153,7 +141,8 @@ class SiteGroup(NestedGroupModel): models.UniqueConstraint( fields=('name',), name='dcim_sitegroup_name', - condition=Q(parent=None) + condition=Q(parent__isnull=True), + violation_error_message="A top-level site group with this name already exists." ), models.UniqueConstraint( fields=('parent', 'slug'), @@ -162,24 +151,11 @@ class SiteGroup(NestedGroupModel): models.UniqueConstraint( fields=('slug',), name='dcim_sitegroup_slug', - condition=Q(parent=None) + condition=Q(parent__isnull=True), + violation_error_message="A top-level site group with this slug already exists." ), ) - def validate_unique(self, exclude=None): - if self.parent is None: - site_groups = SiteGroup.objects.exclude(pk=self.pk) - if site_groups.filter(name=self.name, parent__isnull=True).exists(): - raise ValidationError({ - 'name': 'A site group with this name already exists.' - }) - if site_groups.filter(slug=self.slug, parent__isnull=True).exists(): - raise ValidationError({ - 'name': 'A site group with this slug already exists.' - }) - - super().validate_unique(exclude=exclude) - def get_absolute_url(self): return reverse('dcim:sitegroup', args=[self.pk]) @@ -384,7 +360,8 @@ class Location(NestedGroupModel): models.UniqueConstraint( fields=('site', 'name'), name='dcim_location_name', - condition=Q(parent=None) + condition=Q(parent__isnull=True), + violation_error_message="A location with this name already exists within the specified site." ), models.UniqueConstraint( fields=('site', 'parent', 'slug'), @@ -393,24 +370,11 @@ class Location(NestedGroupModel): models.UniqueConstraint( fields=('site', 'slug'), name='dcim_location_slug', - condition=Q(parent=None) + condition=Q(parent__isnull=True), + violation_error_message="A location with this slug already exists within the specified site." ), ) - def validate_unique(self, exclude=None): - if self.parent is None: - locations = Location.objects.exclude(pk=self.pk) - if locations.filter(name=self.name, site=self.site, parent__isnull=True).exists(): - raise ValidationError({ - "name": f"A location with this name in site {self.site} already exists." - }) - if locations.filter(slug=self.slug, site=self.site, parent__isnull=True).exists(): - raise ValidationError({ - "name": f"A location with this slug in site {self.site} already exists." - }) - - super().validate_unique(exclude=exclude) - @classmethod def get_prerequisite_models(cls): return [Site, ] diff --git a/netbox/dcim/tests/test_models.py b/netbox/dcim/tests/test_models.py index 0e02b0de5..acde02ecd 100644 --- a/netbox/dcim/tests/test_models.py +++ b/netbox/dcim/tests/test_models.py @@ -384,7 +384,7 @@ class DeviceTestCase(TestCase): site=self.site, device_type=self.device_type, device_role=self.device_role, - name='' + name=None ) device1.save() @@ -392,12 +392,12 @@ class DeviceTestCase(TestCase): site=device1.site, device_type=device1.device_type, device_role=device1.device_role, - name='' + name=None ) device2.full_clean() device2.save() - self.assertEqual(Device.objects.filter(name='').count(), 2) + self.assertEqual(Device.objects.filter(name__isnull=True).count(), 2) def test_device_duplicate_names(self): diff --git a/netbox/virtualization/migrations/0033_unique_constraints.py b/netbox/virtualization/migrations/0033_unique_constraints.py new file mode 100644 index 000000000..fe02881b0 --- /dev/null +++ b/netbox/virtualization/migrations/0033_unique_constraints.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.1 on 2022-09-14 20:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0032_virtualmachine_update_sites'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='virtualmachine', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='virtualmachine', + constraint=models.UniqueConstraint(fields=('name', 'cluster', 'tenant'), name='virtualization_virtualmachine_unique_name_cluster_tenant'), + ), + migrations.AddConstraint( + model_name='virtualmachine', + constraint=models.UniqueConstraint(condition=models.Q(('tenant__isnull', True)), fields=('name', 'cluster'), name='virtualization_virtualmachine_unique_name_cluster', violation_error_message='Virtual machine name must be unique per site.'), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 4acbe6daf..1b0a6ba06 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -2,6 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator from django.db import models +from django.db.models import Q from django.urls import reverse from dcim.models import BaseInterface, Device @@ -309,9 +310,18 @@ class VirtualMachine(NetBoxModel, ConfigContextModel): class Meta: ordering = ('_name', 'pk') # Name may be non-unique - unique_together = [ - ['cluster', 'tenant', 'name'] - ] + constraints = ( + models.UniqueConstraint( + name='virtualization_virtualmachine_unique_name_cluster_tenant', + fields=('name', 'cluster', 'tenant') + ), + models.UniqueConstraint( + name='virtualization_virtualmachine_unique_name_cluster', + fields=('name', 'cluster'), + condition=Q(tenant__isnull=True), + violation_error_message="Virtual machine name must be unique per site." + ), + ) def __str__(self): return self.name @@ -323,20 +333,6 @@ class VirtualMachine(NetBoxModel, ConfigContextModel): def get_absolute_url(self): return reverse('virtualization:virtualmachine', args=[self.pk]) - def validate_unique(self, exclude=None): - - # Check for a duplicate name on a VM assigned to the same Cluster and no Tenant. This is necessary - # because Django does not consider two NULL fields to be equal, and thus will not trigger a violation - # of the uniqueness constraint without manual intervention. - if self.tenant is None and VirtualMachine.objects.exclude(pk=self.pk).filter( - name=self.name, cluster=self.cluster, tenant__isnull=True - ): - raise ValidationError({ - 'name': 'A virtual machine with this name already exists in the assigned cluster.' - }) - - super().validate_unique(exclude) - def clean(self): super().clean() From f51415cf2c581b63bad0807b5d32f4d5c0231af9 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 27 Sep 2022 15:35:24 -0400 Subject: [PATCH 17/18] Replace unique_together with UniqueConstraints --- .../migrations/0039_unique_constraints.py | 39 +++ netbox/circuits/models/circuits.py | 14 +- netbox/circuits/models/providers.py | 3 +- .../migrations/0162_unique_constraints.py | 246 +++++++++++++++++- .../dcim/models/device_component_templates.py | 95 +++---- netbox/dcim/models/device_components.py | 62 ++--- netbox/dcim/models/devices.py | 21 +- netbox/dcim/models/power.py | 14 +- netbox/dcim/models/racks.py | 17 +- .../migrations/0078_unique_constraints.py | 27 ++ netbox/extras/models/models.py | 16 +- .../migrations/0062_unique_constraints.py | 43 +++ netbox/ipam/models/fhrp.py | 7 +- netbox/ipam/models/vlans.py | 28 +- .../migrations/0008_unique_constraints.py | 35 +++ netbox/tenancy/models/contacts.py | 21 +- .../migrations/0033_unique_constraints.py | 22 +- netbox/virtualization/models.py | 21 +- .../migrations/0006_unique_constraints.py | 27 ++ netbox/wireless/models.py | 14 +- 20 files changed, 630 insertions(+), 142 deletions(-) create mode 100644 netbox/circuits/migrations/0039_unique_constraints.py create mode 100644 netbox/extras/migrations/0078_unique_constraints.py create mode 100644 netbox/ipam/migrations/0062_unique_constraints.py create mode 100644 netbox/tenancy/migrations/0008_unique_constraints.py create mode 100644 netbox/wireless/migrations/0006_unique_constraints.py diff --git a/netbox/circuits/migrations/0039_unique_constraints.py b/netbox/circuits/migrations/0039_unique_constraints.py new file mode 100644 index 000000000..1d5b62499 --- /dev/null +++ b/netbox/circuits/migrations/0039_unique_constraints.py @@ -0,0 +1,39 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('circuits', '0038_cabling_cleanup'), + ] + + operations = [ + migrations.RemoveConstraint( + model_name='providernetwork', + name='circuits_providernetwork_provider_name', + ), + migrations.AlterUniqueTogether( + name='circuit', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='circuittermination', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='providernetwork', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='circuit', + constraint=models.UniqueConstraint(fields=('provider', 'cid'), name='circuits_circuit_unique_provider_cid'), + ), + migrations.AddConstraint( + model_name='circuittermination', + constraint=models.UniqueConstraint(fields=('circuit', 'term_side'), name='circuits_circuittermination_unique_circuit_term_side'), + ), + migrations.AddConstraint( + model_name='providernetwork', + constraint=models.UniqueConstraint(fields=('provider', 'name'), name='circuits_providernetwork_unique_provider_name'), + ), + ] diff --git a/netbox/circuits/models/circuits.py b/netbox/circuits/models/circuits.py index c08b5473a..ea74eeb40 100644 --- a/netbox/circuits/models/circuits.py +++ b/netbox/circuits/models/circuits.py @@ -132,7 +132,12 @@ class Circuit(NetBoxModel): class Meta: ordering = ['provider', 'cid'] - unique_together = ['provider', 'cid'] + constraints = ( + models.UniqueConstraint( + fields=('provider', 'cid'), + name='%(app_label)s_%(class)s_unique_provider_cid' + ), + ) def __str__(self): return self.cid @@ -208,7 +213,12 @@ class CircuitTermination( class Meta: ordering = ['circuit', 'term_side'] - unique_together = ['circuit', 'term_side'] + constraints = ( + models.UniqueConstraint( + fields=('circuit', 'term_side'), + name='%(app_label)s_%(class)s_unique_circuit_term_side' + ), + ) def __str__(self): return f'Termination {self.term_side}: {self.site or self.provider_network}' diff --git a/netbox/circuits/models/providers.py b/netbox/circuits/models/providers.py index e136e13ea..2a1e01626 100644 --- a/netbox/circuits/models/providers.py +++ b/netbox/circuits/models/providers.py @@ -106,10 +106,9 @@ class ProviderNetwork(NetBoxModel): constraints = ( models.UniqueConstraint( fields=('provider', 'name'), - name='circuits_providernetwork_provider_name' + name='%(app_label)s_%(class)s_unique_provider_name' ), ) - unique_together = ('provider', 'name') def __str__(self): return self.name diff --git a/netbox/dcim/migrations/0162_unique_constraints.py b/netbox/dcim/migrations/0162_unique_constraints.py index 08c113f50..a2f471632 100644 --- a/netbox/dcim/migrations/0162_unique_constraints.py +++ b/netbox/dcim/migrations/0162_unique_constraints.py @@ -1,5 +1,3 @@ -# Generated by Django 4.1.1 on 2022-09-14 20:57 - from django.db import migrations, models @@ -34,10 +32,134 @@ class Migration(migrations.Migration): model_name='sitegroup', name='dcim_sitegroup_slug', ), + migrations.AlterUniqueTogether( + name='consoleport', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='consoleporttemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='consoleserverport', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='consoleserverporttemplate', + unique_together=set(), + ), migrations.AlterUniqueTogether( name='device', unique_together=set(), ), + migrations.AlterUniqueTogether( + name='devicebay', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='devicebaytemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='devicetype', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='frontport', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='frontporttemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='interface', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='interfacetemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='inventoryitem', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='inventoryitemtemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='modulebay', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='modulebaytemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='moduletype', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='powerfeed', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='poweroutlet', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='poweroutlettemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='powerpanel', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='powerport', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='powerporttemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='rack', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='rearport', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='rearporttemplate', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='consoleport', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_consoleport_unique_device_name'), + ), + migrations.AddConstraint( + model_name='consoleporttemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_consoleporttemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='consoleporttemplate', + constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_consoleporttemplate_unique_module_type_name'), + ), + migrations.AddConstraint( + model_name='consoleserverport', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_consoleserverport_unique_device_name'), + ), + migrations.AddConstraint( + model_name='consoleserverporttemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_consoleserverporttemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='consoleserverporttemplate', + constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_consoleserverporttemplate_unique_module_type_name'), + ), migrations.AddConstraint( model_name='device', constraint=models.UniqueConstraint(fields=('name', 'site', 'tenant'), name='dcim_device_unique_name_site_tenant'), @@ -54,6 +176,62 @@ class Migration(migrations.Migration): model_name='device', constraint=models.UniqueConstraint(fields=('virtual_chassis', 'vc_position'), name='dcim_device_unique_virtual_chassis_vc_position'), ), + migrations.AddConstraint( + model_name='devicebay', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_devicebay_unique_device_name'), + ), + migrations.AddConstraint( + model_name='devicebaytemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_devicebaytemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='devicetype', + constraint=models.UniqueConstraint(fields=('manufacturer', 'model'), name='dcim_devicetype_unique_manufacturer_model'), + ), + migrations.AddConstraint( + model_name='devicetype', + constraint=models.UniqueConstraint(fields=('manufacturer', 'slug'), name='dcim_devicetype_unique_manufacturer_slug'), + ), + migrations.AddConstraint( + model_name='frontport', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_frontport_unique_device_name'), + ), + migrations.AddConstraint( + model_name='frontport', + constraint=models.UniqueConstraint(fields=('rear_port', 'rear_port_position'), name='dcim_frontport_unique_rear_port_position'), + ), + migrations.AddConstraint( + model_name='frontporttemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_frontporttemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='frontporttemplate', + constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_frontporttemplate_unique_module_type_name'), + ), + migrations.AddConstraint( + model_name='frontporttemplate', + constraint=models.UniqueConstraint(fields=('rear_port', 'rear_port_position'), name='dcim_frontporttemplate_unique_rear_port_position'), + ), + migrations.AddConstraint( + model_name='interface', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_interface_unique_device_name'), + ), + migrations.AddConstraint( + model_name='interfacetemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_interfacetemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='interfacetemplate', + constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_interfacetemplate_unique_module_type_name'), + ), + migrations.AddConstraint( + model_name='inventoryitem', + constraint=models.UniqueConstraint(fields=('device', 'parent', 'name'), name='dcim_inventoryitem_unique_device_parent_name'), + ), + migrations.AddConstraint( + model_name='inventoryitemtemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'parent', 'name'), name='dcim_inventoryitemtemplate_unique_device_type_parent_name'), + ), migrations.AddConstraint( model_name='location', constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('site', 'name'), name='dcim_location_name', violation_error_message='A location with this name already exists within the specified site.'), @@ -62,6 +240,70 @@ class Migration(migrations.Migration): model_name='location', constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('site', 'slug'), name='dcim_location_slug', violation_error_message='A location with this slug already exists within the specified site.'), ), + migrations.AddConstraint( + model_name='modulebay', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_modulebay_unique_device_name'), + ), + migrations.AddConstraint( + model_name='modulebaytemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_modulebaytemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='moduletype', + constraint=models.UniqueConstraint(fields=('manufacturer', 'model'), name='dcim_moduletype_unique_manufacturer_model'), + ), + migrations.AddConstraint( + model_name='powerfeed', + constraint=models.UniqueConstraint(fields=('power_panel', 'name'), name='dcim_powerfeed_unique_power_panel_name'), + ), + migrations.AddConstraint( + model_name='poweroutlet', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_poweroutlet_unique_device_name'), + ), + migrations.AddConstraint( + model_name='poweroutlettemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_poweroutlettemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='poweroutlettemplate', + constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_poweroutlettemplate_unique_module_type_name'), + ), + migrations.AddConstraint( + model_name='powerpanel', + constraint=models.UniqueConstraint(fields=('site', 'name'), name='dcim_powerpanel_unique_site_name'), + ), + migrations.AddConstraint( + model_name='powerport', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_powerport_unique_device_name'), + ), + migrations.AddConstraint( + model_name='powerporttemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_powerporttemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='powerporttemplate', + constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_powerporttemplate_unique_module_type_name'), + ), + migrations.AddConstraint( + model_name='rack', + constraint=models.UniqueConstraint(fields=('location', 'name'), name='dcim_rack_unique_location_name'), + ), + migrations.AddConstraint( + model_name='rack', + constraint=models.UniqueConstraint(fields=('location', 'facility_id'), name='dcim_rack_unique_location_facility_id'), + ), + migrations.AddConstraint( + model_name='rearport', + constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_rearport_unique_device_name'), + ), + migrations.AddConstraint( + model_name='rearporttemplate', + constraint=models.UniqueConstraint(fields=('device_type', 'name'), name='dcim_rearporttemplate_unique_device_type_name'), + ), + migrations.AddConstraint( + model_name='rearporttemplate', + constraint=models.UniqueConstraint(fields=('module_type', 'name'), name='dcim_rearporttemplate_unique_module_type_name'), + ), migrations.AddConstraint( model_name='region', constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('name',), name='dcim_region_name', violation_error_message='A top-level region with this name already exists.'), diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index b7079d375..15389a2c0 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -61,6 +61,13 @@ class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel): class Meta: abstract = True + ordering = ('device_type', '_name') + constraints = ( + models.UniqueConstraint( + fields=('device_type', 'name'), + name='%(app_label)s_%(class)s_unique_device_type_name' + ), + ) def __str__(self): if self.label: @@ -100,6 +107,17 @@ class ModularComponentTemplateModel(ComponentTemplateModel): class Meta: abstract = True + ordering = ('device_type', 'module_type', '_name') + constraints = ( + models.UniqueConstraint( + fields=('device_type', 'name'), + name='%(app_label)s_%(class)s_unique_device_type_name' + ), + models.UniqueConstraint( + fields=('module_type', 'name'), + name='%(app_label)s_%(class)s_unique_module_type_name' + ), + ) def to_objectchange(self, action): objectchange = super().to_objectchange(action) @@ -145,13 +163,6 @@ class ConsolePortTemplate(ModularComponentTemplateModel): component_model = ConsolePort - class Meta: - ordering = ('device_type', 'module_type', '_name') - unique_together = ( - ('device_type', 'name'), - ('module_type', 'name'), - ) - def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), @@ -181,13 +192,6 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel): component_model = ConsoleServerPort - class Meta: - ordering = ('device_type', 'module_type', '_name') - unique_together = ( - ('device_type', 'name'), - ('module_type', 'name'), - ) - def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), @@ -229,13 +233,6 @@ class PowerPortTemplate(ModularComponentTemplateModel): component_model = PowerPort - class Meta: - ordering = ('device_type', 'module_type', '_name') - unique_together = ( - ('device_type', 'name'), - ('module_type', 'name'), - ) - def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), @@ -291,13 +288,6 @@ class PowerOutletTemplate(ModularComponentTemplateModel): component_model = PowerOutlet - class Meta: - ordering = ('device_type', 'module_type', '_name') - unique_together = ( - ('device_type', 'name'), - ('module_type', 'name'), - ) - def clean(self): super().clean() @@ -372,13 +362,6 @@ class InterfaceTemplate(ModularComponentTemplateModel): component_model = Interface - class Meta: - ordering = ('device_type', 'module_type', '_name') - unique_together = ( - ('device_type', 'name'), - ('module_type', 'name'), - ) - def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), @@ -428,12 +411,20 @@ class FrontPortTemplate(ModularComponentTemplateModel): component_model = FrontPort - class Meta: - ordering = ('device_type', 'module_type', '_name') - unique_together = ( - ('device_type', 'name'), - ('module_type', 'name'), - ('rear_port', 'rear_port_position'), + class Meta(ModularComponentTemplateModel.Meta): + constraints = ( + models.UniqueConstraint( + fields=('device_type', 'name'), + name='%(app_label)s_%(class)s_unique_device_type_name' + ), + models.UniqueConstraint( + fields=('module_type', 'name'), + name='%(app_label)s_%(class)s_unique_module_type_name' + ), + models.UniqueConstraint( + fields=('rear_port', 'rear_port_position'), + name='%(app_label)s_%(class)s_unique_rear_port_position' + ), ) def clean(self): @@ -507,13 +498,6 @@ class RearPortTemplate(ModularComponentTemplateModel): component_model = RearPort - class Meta: - ordering = ('device_type', 'module_type', '_name') - unique_together = ( - ('device_type', 'name'), - ('module_type', 'name'), - ) - def instantiate(self, **kwargs): return self.component_model( name=self.resolve_name(kwargs.get('module')), @@ -547,10 +531,6 @@ class ModuleBayTemplate(ComponentTemplateModel): component_model = ModuleBay - class Meta: - ordering = ('device_type', '_name') - unique_together = ('device_type', 'name') - def instantiate(self, device): return self.component_model( device=device, @@ -574,10 +554,6 @@ class DeviceBayTemplate(ComponentTemplateModel): """ component_model = DeviceBay - class Meta: - ordering = ('device_type', '_name') - unique_together = ('device_type', 'name') - def instantiate(self, device): return self.component_model( device=device, @@ -653,7 +629,12 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel): class Meta: ordering = ('device_type__id', 'parent__id', '_name') - unique_together = ('device_type', 'parent', 'name') + constraints = ( + models.UniqueConstraint( + fields=('device_type', 'parent', 'name'), + name='%(app_label)s_%(class)s_unique_device_type_parent_name' + ), + ) def instantiate(self, **kwargs): parent = InventoryItem.objects.get(name=self.parent.name, **kwargs) if self.parent else None diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index c521ee095..59d63ef7b 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -69,6 +69,13 @@ class ComponentModel(NetBoxModel): class Meta: abstract = True + ordering = ('device', '_name') + constraints = ( + models.UniqueConstraint( + fields=('device', 'name'), + name='%(app_label)s_%(class)s_unique_device_name' + ), + ) def __str__(self): if self.label: @@ -99,7 +106,7 @@ class ModularComponentModel(ComponentModel): object_id_field='component_id' ) - class Meta: + class Meta(ComponentModel.Meta): abstract = True @@ -265,10 +272,6 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint): clone_fields = ('device', 'module', 'type', 'speed') - class Meta: - ordering = ('device', '_name') - unique_together = ('device', 'name') - def get_absolute_url(self): return reverse('dcim:consoleport', kwargs={'pk': self.pk}) @@ -292,10 +295,6 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint): clone_fields = ('device', 'module', 'type', 'speed') - class Meta: - ordering = ('device', '_name') - unique_together = ('device', 'name') - def get_absolute_url(self): return reverse('dcim:consoleserverport', kwargs={'pk': self.pk}) @@ -329,10 +328,6 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint): clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw') - class Meta: - ordering = ('device', '_name') - unique_together = ('device', 'name') - def get_absolute_url(self): return reverse('dcim:powerport', kwargs={'pk': self.pk}) @@ -443,10 +438,6 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint): clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg') - class Meta: - ordering = ('device', '_name') - unique_together = ('device', 'name') - def get_absolute_url(self): return reverse('dcim:poweroutlet', kwargs={'pk': self.pk}) @@ -677,9 +668,8 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'poe_mode', 'poe_type', 'vrf', ) - class Meta: + class Meta(ModularComponentModel.Meta): ordering = ('device', CollateAsChar('_name')) - unique_together = ('device', 'name') def get_absolute_url(self): return reverse('dcim:interface', kwargs={'pk': self.pk}) @@ -895,11 +885,16 @@ class FrontPort(ModularComponentModel, CabledObjectModel): clone_fields = ('device', 'type', 'color') - class Meta: - ordering = ('device', '_name') - unique_together = ( - ('device', 'name'), - ('rear_port', 'rear_port_position'), + class Meta(ModularComponentModel.Meta): + constraints = ( + models.UniqueConstraint( + fields=('device', 'name'), + name='%(app_label)s_%(class)s_unique_device_name' + ), + models.UniqueConstraint( + fields=('rear_port', 'rear_port_position'), + name='%(app_label)s_%(class)s_unique_rear_port_position' + ), ) def get_absolute_url(self): @@ -944,10 +939,6 @@ class RearPort(ModularComponentModel, CabledObjectModel): ) clone_fields = ('device', 'type', 'color', 'positions') - class Meta: - ordering = ('device', '_name') - unique_together = ('device', 'name') - def get_absolute_url(self): return reverse('dcim:rearport', kwargs={'pk': self.pk}) @@ -980,10 +971,6 @@ class ModuleBay(ComponentModel): clone_fields = ('device',) - class Meta: - ordering = ('device', '_name') - unique_together = ('device', 'name') - def get_absolute_url(self): return reverse('dcim:modulebay', kwargs={'pk': self.pk}) @@ -1002,10 +989,6 @@ class DeviceBay(ComponentModel): clone_fields = ('device',) - class Meta: - ordering = ('device', '_name') - unique_together = ('device', 'name') - def get_absolute_url(self): return reverse('dcim:devicebay', kwargs={'pk': self.pk}) @@ -1141,7 +1124,12 @@ class InventoryItem(MPTTModel, ComponentModel): class Meta: ordering = ('device__id', 'parent__id', '_name') - unique_together = ('device', 'parent', 'name') + constraints = ( + models.UniqueConstraint( + fields=('device', 'parent', 'name'), + name='%(app_label)s_%(class)s_unique_device_parent_name' + ), + ) def get_absolute_url(self): return reverse('dcim:inventoryitem', kwargs={'pk': self.pk}) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index eb21e532b..491846c39 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -143,10 +143,16 @@ class DeviceType(NetBoxModel): class Meta: ordering = ['manufacturer', 'model'] - unique_together = [ - ['manufacturer', 'model'], - ['manufacturer', 'slug'], - ] + constraints = ( + models.UniqueConstraint( + fields=('manufacturer', 'model'), + name='%(app_label)s_%(class)s_unique_manufacturer_model' + ), + models.UniqueConstraint( + fields=('manufacturer', 'slug'), + name='%(app_label)s_%(class)s_unique_manufacturer_slug' + ), + ) def __str__(self): return self.model @@ -341,8 +347,11 @@ class ModuleType(NetBoxModel): class Meta: ordering = ('manufacturer', 'model') - unique_together = ( - ('manufacturer', 'model'), + constraints = ( + models.UniqueConstraint( + fields=('manufacturer', 'model'), + name='%(app_label)s_%(class)s_unique_manufacturer_model' + ), ) def __str__(self): diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 83eead67f..39f0f37ef 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -50,7 +50,12 @@ class PowerPanel(NetBoxModel): class Meta: ordering = ['site', 'name'] - unique_together = ['site', 'name'] + constraints = ( + models.UniqueConstraint( + fields=('site', 'name'), + name='%(app_label)s_%(class)s_unique_site_name' + ), + ) def __str__(self): return self.name @@ -138,7 +143,12 @@ class PowerFeed(NetBoxModel, PathEndpoint, CabledObjectModel): class Meta: ordering = ['power_panel', 'name'] - unique_together = ['power_panel', 'name'] + constraints = ( + models.UniqueConstraint( + fields=('power_panel', 'name'), + name='%(app_label)s_%(class)s_unique_power_panel_name' + ), + ) def __str__(self): return self.name diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 20027675a..10550e906 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -3,12 +3,11 @@ import decimal from django.apps import apps from django.contrib.auth.models import User from django.contrib.contenttypes.fields import GenericRelation -from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import ArrayField from django.core.exceptions import ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import Count, Sum +from django.db.models import Count from django.urls import reverse from dcim.choices import * @@ -18,7 +17,7 @@ from netbox.models import OrganizationalModel, NetBoxModel from utilities.choices import ColorChoices from utilities.fields import ColorField, NaturalOrderingField from utilities.utils import array_to_string, drange -from .device_components import PowerOutlet, PowerPort +from .device_components import PowerPort from .devices import Device from .power import PowerFeed @@ -191,10 +190,16 @@ class Rack(NetBoxModel): class Meta: ordering = ('site', 'location', '_name', 'pk') # (site, location, name) may be non-unique - unique_together = ( + constraints = ( # Name and facility_id must be unique *only* within a Location - ('location', 'name'), - ('location', 'facility_id'), + models.UniqueConstraint( + fields=('location', 'name'), + name='%(app_label)s_%(class)s_unique_location_name' + ), + models.UniqueConstraint( + fields=('location', 'facility_id'), + name='%(app_label)s_%(class)s_unique_location_facility_id' + ), ) def __str__(self): diff --git a/netbox/extras/migrations/0078_unique_constraints.py b/netbox/extras/migrations/0078_unique_constraints.py new file mode 100644 index 000000000..4a56831a7 --- /dev/null +++ b/netbox/extras/migrations/0078_unique_constraints.py @@ -0,0 +1,27 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0077_customlink_extend_text_and_url'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='exporttemplate', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='webhook', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='exporttemplate', + constraint=models.UniqueConstraint(fields=('content_type', 'name'), name='extras_exporttemplate_unique_content_type_name'), + ), + migrations.AddConstraint( + model_name='webhook', + constraint=models.UniqueConstraint(fields=('payload_url', 'type_create', 'type_update', 'type_delete'), name='extras_webhook_unique_payload_url_types'), + ), + ] diff --git a/netbox/extras/models/models.py b/netbox/extras/models/models.py index 0df34c146..266953f61 100644 --- a/netbox/extras/models/models.py +++ b/netbox/extras/models/models.py @@ -131,7 +131,12 @@ class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): class Meta: ordering = ('name',) - unique_together = ('payload_url', 'type_create', 'type_update', 'type_delete',) + constraints = ( + models.UniqueConstraint( + fields=('payload_url', 'type_create', 'type_update', 'type_delete'), + name='%(app_label)s_%(class)s_unique_payload_url_types' + ), + ) def __str__(self): return self.name @@ -297,9 +302,12 @@ class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel): class Meta: ordering = ['content_type', 'name'] - unique_together = [ - ['content_type', 'name'] - ] + constraints = ( + models.UniqueConstraint( + fields=('content_type', 'name'), + name='%(app_label)s_%(class)s_unique_content_type_name' + ), + ) def __str__(self): return f"{self.content_type}: {self.name}" diff --git a/netbox/ipam/migrations/0062_unique_constraints.py b/netbox/ipam/migrations/0062_unique_constraints.py new file mode 100644 index 000000000..47c1a1214 --- /dev/null +++ b/netbox/ipam/migrations/0062_unique_constraints.py @@ -0,0 +1,43 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ipam', '0061_fhrpgroup_name'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='fhrpgroupassignment', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='vlan', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='vlangroup', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='fhrpgroupassignment', + constraint=models.UniqueConstraint(fields=('interface_type', 'interface_id', 'group'), name='ipam_fhrpgroupassignment_unique_interface_group'), + ), + migrations.AddConstraint( + model_name='vlan', + constraint=models.UniqueConstraint(fields=('group', 'vid'), name='ipam_vlan_unique_group_vid'), + ), + migrations.AddConstraint( + model_name='vlan', + constraint=models.UniqueConstraint(fields=('group', 'name'), name='ipam_vlan_unique_group_name'), + ), + migrations.AddConstraint( + model_name='vlangroup', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'name'), name='ipam_vlangroup_unique_scope_name'), + ), + migrations.AddConstraint( + model_name='vlangroup', + constraint=models.UniqueConstraint(fields=('scope_type', 'scope_id', 'slug'), name='ipam_vlangroup_unique_scope_slug'), + ), + ] diff --git a/netbox/ipam/models/fhrp.py b/netbox/ipam/models/fhrp.py index 88e6e19d9..633affa41 100644 --- a/netbox/ipam/models/fhrp.py +++ b/netbox/ipam/models/fhrp.py @@ -102,7 +102,12 @@ class FHRPGroupAssignment(WebhooksMixin, ChangeLoggedModel): class Meta: ordering = ('-priority', 'pk') - unique_together = ('interface_type', 'interface_id', 'group') + constraints = ( + models.UniqueConstraint( + fields=('interface_type', 'interface_id', 'group'), + name='%(app_label)s_%(class)s_unique_interface_group' + ), + ) verbose_name = 'FHRP group assignment' def __str__(self): diff --git a/netbox/ipam/models/vlans.py b/netbox/ipam/models/vlans.py index f0e062721..c8c401e1c 100644 --- a/netbox/ipam/models/vlans.py +++ b/netbox/ipam/models/vlans.py @@ -70,10 +70,16 @@ class VLANGroup(OrganizationalModel): class Meta: ordering = ('name', 'pk') # Name may be non-unique - unique_together = [ - ['scope_type', 'scope_id', 'name'], - ['scope_type', 'scope_id', 'slug'], - ] + constraints = ( + models.UniqueConstraint( + fields=('scope_type', 'scope_id', 'name'), + name='%(app_label)s_%(class)s_unique_scope_name' + ), + models.UniqueConstraint( + fields=('scope_type', 'scope_id', 'slug'), + name='%(app_label)s_%(class)s_unique_scope_slug' + ), + ) verbose_name = 'VLAN group' verbose_name_plural = 'VLAN groups' @@ -189,10 +195,16 @@ class VLAN(NetBoxModel): class Meta: ordering = ('site', 'group', 'vid', 'pk') # (site, group, vid) may be non-unique - unique_together = [ - ['group', 'vid'], - ['group', 'name'], - ] + constraints = ( + models.UniqueConstraint( + fields=('group', 'vid'), + name='%(app_label)s_%(class)s_unique_group_vid' + ), + models.UniqueConstraint( + fields=('group', 'name'), + name='%(app_label)s_%(class)s_unique_group_name' + ), + ) verbose_name = 'VLAN' verbose_name_plural = 'VLANs' diff --git a/netbox/tenancy/migrations/0008_unique_constraints.py b/netbox/tenancy/migrations/0008_unique_constraints.py new file mode 100644 index 000000000..092878524 --- /dev/null +++ b/netbox/tenancy/migrations/0008_unique_constraints.py @@ -0,0 +1,35 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0007_contact_link'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='contact', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='contactassignment', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='contactgroup', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='contact', + constraint=models.UniqueConstraint(fields=('group', 'name'), name='tenancy_contact_unique_group_name'), + ), + migrations.AddConstraint( + model_name='contactassignment', + constraint=models.UniqueConstraint(fields=('content_type', 'object_id', 'contact', 'role'), name='tenancy_contactassignment_unique_object_contact_role'), + ), + migrations.AddConstraint( + model_name='contactgroup', + constraint=models.UniqueConstraint(fields=('parent', 'name'), name='tenancy_contactgroup_unique_parent_name'), + ), + ] diff --git a/netbox/tenancy/models/contacts.py b/netbox/tenancy/models/contacts.py index 41881f853..79c0a2db3 100644 --- a/netbox/tenancy/models/contacts.py +++ b/netbox/tenancy/models/contacts.py @@ -41,8 +41,11 @@ class ContactGroup(NestedGroupModel): class Meta: ordering = ['name'] - unique_together = ( - ('parent', 'name') + constraints = ( + models.UniqueConstraint( + fields=('parent', 'name'), + name='%(app_label)s_%(class)s_unique_parent_name' + ), ) def get_absolute_url(self): @@ -118,8 +121,11 @@ class Contact(NetBoxModel): class Meta: ordering = ['name'] - unique_together = ( - ('group', 'name') + constraints = ( + models.UniqueConstraint( + fields=('group', 'name'), + name='%(app_label)s_%(class)s_unique_group_name' + ), ) def __str__(self): @@ -159,7 +165,12 @@ class ContactAssignment(WebhooksMixin, ChangeLoggedModel): class Meta: ordering = ('priority', 'contact') - unique_together = ('content_type', 'object_id', 'contact', 'role', 'priority') + constraints = ( + models.UniqueConstraint( + fields=('content_type', 'object_id', 'contact', 'role'), + name='%(app_label)s_%(class)s_unique_object_contact_role' + ), + ) def __str__(self): if self.priority: diff --git a/netbox/virtualization/migrations/0033_unique_constraints.py b/netbox/virtualization/migrations/0033_unique_constraints.py index fe02881b0..4667dcbd3 100644 --- a/netbox/virtualization/migrations/0033_unique_constraints.py +++ b/netbox/virtualization/migrations/0033_unique_constraints.py @@ -1,5 +1,3 @@ -# Generated by Django 4.1.1 on 2022-09-14 20:57 - from django.db import migrations, models @@ -10,10 +8,26 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AlterUniqueTogether( + name='cluster', + unique_together=set(), + ), migrations.AlterUniqueTogether( name='virtualmachine', unique_together=set(), ), + migrations.AlterUniqueTogether( + name='vminterface', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='cluster', + constraint=models.UniqueConstraint(fields=('group', 'name'), name='virtualization_cluster_unique_group_name'), + ), + migrations.AddConstraint( + model_name='cluster', + constraint=models.UniqueConstraint(fields=('site', 'name'), name='virtualization_cluster_unique_site_name'), + ), migrations.AddConstraint( model_name='virtualmachine', constraint=models.UniqueConstraint(fields=('name', 'cluster', 'tenant'), name='virtualization_virtualmachine_unique_name_cluster_tenant'), @@ -22,4 +36,8 @@ class Migration(migrations.Migration): model_name='virtualmachine', constraint=models.UniqueConstraint(condition=models.Q(('tenant__isnull', True)), fields=('name', 'cluster'), name='virtualization_virtualmachine_unique_name_cluster', violation_error_message='Virtual machine name must be unique per site.'), ), + migrations.AddConstraint( + model_name='vminterface', + constraint=models.UniqueConstraint(fields=('virtual_machine', 'name'), name='virtualization_vminterface_unique_virtual_machine_name'), + ), ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 1b0a6ba06..b0e732188 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -160,9 +160,15 @@ class Cluster(NetBoxModel): class Meta: ordering = ['name'] - unique_together = ( - ('group', 'name'), - ('site', 'name'), + constraints = ( + models.UniqueConstraint( + fields=('group', 'name'), + name='%(app_label)s_%(class)s_unique_group_name' + ), + models.UniqueConstraint( + fields=('site', 'name'), + name='%(app_label)s_%(class)s_unique_site_name' + ), ) def __str__(self): @@ -461,9 +467,14 @@ class VMInterface(NetBoxModel, BaseInterface): ) class Meta: - verbose_name = 'interface' ordering = ('virtual_machine', CollateAsChar('_name')) - unique_together = ('virtual_machine', 'name') + constraints = ( + models.UniqueConstraint( + fields=('virtual_machine', 'name'), + name='%(app_label)s_%(class)s_unique_virtual_machine_name' + ), + ) + verbose_name = 'interface' def __str__(self): return self.name diff --git a/netbox/wireless/migrations/0006_unique_constraints.py b/netbox/wireless/migrations/0006_unique_constraints.py new file mode 100644 index 000000000..f638ae1ab --- /dev/null +++ b/netbox/wireless/migrations/0006_unique_constraints.py @@ -0,0 +1,27 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('wireless', '0005_wirelesslink_interface_types'), + ] + + operations = [ + migrations.AlterUniqueTogether( + name='wirelesslangroup', + unique_together=set(), + ), + migrations.AlterUniqueTogether( + name='wirelesslink', + unique_together=set(), + ), + migrations.AddConstraint( + model_name='wirelesslangroup', + constraint=models.UniqueConstraint(fields=('parent', 'name'), name='wireless_wirelesslangroup_unique_parent_name'), + ), + migrations.AddConstraint( + model_name='wirelesslink', + constraint=models.UniqueConstraint(fields=('interface_a', 'interface_b'), name='wireless_wirelesslink_unique_interfaces'), + ), + ] diff --git a/netbox/wireless/models.py b/netbox/wireless/models.py index c383ad642..29fe33f4b 100644 --- a/netbox/wireless/models.py +++ b/netbox/wireless/models.py @@ -69,8 +69,11 @@ class WirelessLANGroup(NestedGroupModel): class Meta: ordering = ('name', 'pk') - unique_together = ( - ('parent', 'name') + constraints = ( + models.UniqueConstraint( + fields=('parent', 'name'), + name='%(app_label)s_%(class)s_unique_parent_name' + ), ) verbose_name = 'Wireless LAN Group' @@ -195,7 +198,12 @@ class WirelessLink(WirelessAuthenticationBase, NetBoxModel): class Meta: ordering = ['pk'] - unique_together = ('interface_a', 'interface_b') + constraints = ( + models.UniqueConstraint( + fields=('interface_a', 'interface_b'), + name='%(app_label)s_%(class)s_unique_interfaces' + ), + ) def __str__(self): return f'#{self.pk}' From 7ff2cb75a8fe40a464c180547a495c887a3fdab1 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Tue, 27 Sep 2022 15:44:38 -0400 Subject: [PATCH 18/18] Use templated app & model names for all unique constraints --- .../migrations/0162_unique_constraints.py | 8 +++++++ netbox/dcim/models/cables.py | 2 +- netbox/dcim/models/devices.py | 14 +++++------ netbox/dcim/models/sites.py | 24 +++++++++---------- netbox/virtualization/models.py | 6 ++--- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/netbox/dcim/migrations/0162_unique_constraints.py b/netbox/dcim/migrations/0162_unique_constraints.py index a2f471632..5dac7039c 100644 --- a/netbox/dcim/migrations/0162_unique_constraints.py +++ b/netbox/dcim/migrations/0162_unique_constraints.py @@ -8,6 +8,10 @@ class Migration(migrations.Migration): ] operations = [ + migrations.RemoveConstraint( + model_name='cabletermination', + name='dcim_cable_termination_unique_termination', + ), migrations.RemoveConstraint( model_name='location', name='dcim_location_name', @@ -136,6 +140,10 @@ class Migration(migrations.Migration): name='rearporttemplate', unique_together=set(), ), + migrations.AddConstraint( + model_name='cabletermination', + constraint=models.UniqueConstraint(fields=('termination_type', 'termination_id'), name='dcim_cabletermination_unique_termination'), + ), migrations.AddConstraint( model_name='consoleport', constraint=models.UniqueConstraint(fields=('device', 'name'), name='dcim_consoleport_unique_device_name'), diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index e05eb6d51..fad3e8bd6 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -269,7 +269,7 @@ class CableTermination(models.Model): constraints = ( models.UniqueConstraint( fields=('termination_type', 'termination_id'), - name='dcim_cable_termination_unique_termination' + name='%(app_label)s_%(class)s_unique_termination' ), ) diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 491846c39..79cc8c86b 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -662,22 +662,22 @@ class Device(NetBoxModel, ConfigContextModel): ordering = ('_name', 'pk') # Name may be null constraints = ( models.UniqueConstraint( - name='dcim_device_unique_name_site_tenant', - fields=('name', 'site', 'tenant') + fields=('name', 'site', 'tenant'), + name='%(app_label)s_%(class)s_unique_name_site_tenant' ), models.UniqueConstraint( - name='dcim_device_unique_name_site', fields=('name', 'site'), + name='%(app_label)s_%(class)s_unique_name_site', condition=Q(tenant__isnull=True), violation_error_message="Device name must be unique per site." ), models.UniqueConstraint( - name='dcim_device_unique_rack_position_face', - fields=('rack', 'position', 'face') + fields=('rack', 'position', 'face'), + name='%(app_label)s_%(class)s_unique_rack_position_face' ), models.UniqueConstraint( - name='dcim_device_unique_virtual_chassis_vc_position', - fields=('virtual_chassis', 'vc_position') + fields=('virtual_chassis', 'vc_position'), + name='%(app_label)s_%(class)s_unique_virtual_chassis_vc_position' ), ) diff --git a/netbox/dcim/models/sites.py b/netbox/dcim/models/sites.py index 90f855741..9ddadace2 100644 --- a/netbox/dcim/models/sites.py +++ b/netbox/dcim/models/sites.py @@ -62,21 +62,21 @@ class Region(NestedGroupModel): constraints = ( models.UniqueConstraint( fields=('parent', 'name'), - name='dcim_region_parent_name' + name='%(app_label)s_%(class)s_parent_name' ), models.UniqueConstraint( fields=('name',), - name='dcim_region_name', + name='%(app_label)s_%(class)s_name', condition=Q(parent__isnull=True), violation_error_message="A top-level region with this name already exists." ), models.UniqueConstraint( fields=('parent', 'slug'), - name='dcim_region_parent_slug' + name='%(app_label)s_%(class)s_parent_slug' ), models.UniqueConstraint( fields=('slug',), - name='dcim_region_slug', + name='%(app_label)s_%(class)s_slug', condition=Q(parent__isnull=True), violation_error_message="A top-level region with this slug already exists." ), @@ -136,21 +136,21 @@ class SiteGroup(NestedGroupModel): constraints = ( models.UniqueConstraint( fields=('parent', 'name'), - name='dcim_sitegroup_parent_name' + name='%(app_label)s_%(class)s_parent_name' ), models.UniqueConstraint( fields=('name',), - name='dcim_sitegroup_name', + name='%(app_label)s_%(class)s_name', condition=Q(parent__isnull=True), violation_error_message="A top-level site group with this name already exists." ), models.UniqueConstraint( fields=('parent', 'slug'), - name='dcim_sitegroup_parent_slug' + name='%(app_label)s_%(class)s_parent_slug' ), models.UniqueConstraint( fields=('slug',), - name='dcim_sitegroup_slug', + name='%(app_label)s_%(class)s_slug', condition=Q(parent__isnull=True), violation_error_message="A top-level site group with this slug already exists." ), @@ -355,21 +355,21 @@ class Location(NestedGroupModel): constraints = ( models.UniqueConstraint( fields=('site', 'parent', 'name'), - name='dcim_location_parent_name' + name='%(app_label)s_%(class)s_parent_name' ), models.UniqueConstraint( fields=('site', 'name'), - name='dcim_location_name', + name='%(app_label)s_%(class)s_name', condition=Q(parent__isnull=True), violation_error_message="A location with this name already exists within the specified site." ), models.UniqueConstraint( fields=('site', 'parent', 'slug'), - name='dcim_location_parent_slug' + name='%(app_label)s_%(class)s_parent_slug' ), models.UniqueConstraint( fields=('site', 'slug'), - name='dcim_location_slug', + name='%(app_label)s_%(class)s_slug', condition=Q(parent__isnull=True), violation_error_message="A location with this slug already exists within the specified site." ), diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index b0e732188..5a1c361c2 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -318,12 +318,12 @@ class VirtualMachine(NetBoxModel, ConfigContextModel): ordering = ('_name', 'pk') # Name may be non-unique constraints = ( models.UniqueConstraint( - name='virtualization_virtualmachine_unique_name_cluster_tenant', - fields=('name', 'cluster', 'tenant') + fields=('name', 'cluster', 'tenant'), + name='%(app_label)s_%(class)s_unique_name_cluster_tenant' ), models.UniqueConstraint( - name='virtualization_virtualmachine_unique_name_cluster', fields=('name', 'cluster'), + name='%(app_label)s_%(class)s_unique_name_cluster', condition=Q(tenant__isnull=True), violation_error_message="Virtual machine name must be unique per site." ),