diff --git a/docs/release-notes/version-3.2.md b/docs/release-notes/version-3.2.md index 2944b5ee9..f35100c72 100644 --- a/docs/release-notes/version-3.2.md +++ b/docs/release-notes/version-3.2.md @@ -43,6 +43,7 @@ FIELD_CHOICES = { * [#7650](https://github.com/netbox-community/netbox/issues/7650) - Add support for local account password validation * [#7681](https://github.com/netbox-community/netbox/issues/7681) - Add `service_id` field for provider networks * [#7759](https://github.com/netbox-community/netbox/issues/7759) - Improved the user preferences form +* [#7784](https://github.com/netbox-community/netbox/issues/7784) - Support cluster type assignment for config contexts * [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group ### Other Changes @@ -77,6 +78,8 @@ FIELD_CHOICES = { * Added `module` field * dcim.Site * Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields +* extras.ConfigContext + * Add `cluster_types` field * ipam.VLANGroup * Added the `/availables-vlans/` endpoint * Added the `min_vid` and `max_vid` fields diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 9e4665cc2..fa0e5189f 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -19,8 +19,10 @@ from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantG from tenancy.models import Tenant, TenantGroup from users.api.nested_serializers import NestedUserSerializer from utilities.api import get_serializer_for_model -from virtualization.api.nested_serializers import NestedClusterGroupSerializer, NestedClusterSerializer -from virtualization.models import Cluster, ClusterGroup +from virtualization.api.nested_serializers import ( + NestedClusterGroupSerializer, NestedClusterSerializer, NestedClusterTypeSerializer, +) +from virtualization.models import Cluster, ClusterGroup, ClusterType from .nested_serializers import * __all__ = ( @@ -267,6 +269,12 @@ class ConfigContextSerializer(ValidatedModelSerializer): required=False, many=True ) + cluster_types = SerializedPKRelatedField( + queryset=ClusterType.objects.all(), + serializer=NestedClusterTypeSerializer, + required=False, + many=True + ) cluster_groups = SerializedPKRelatedField( queryset=ClusterGroup.objects.all(), serializer=NestedClusterGroupSerializer, @@ -302,8 +310,8 @@ class ConfigContextSerializer(ValidatedModelSerializer): model = ConfigContext fields = [ 'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', - 'device_types', 'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', - 'data', 'created', 'last_updated', + 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', + 'tenants', 'tags', 'data', 'created', 'last_updated', ] diff --git a/netbox/extras/filtersets.py b/netbox/extras/filtersets.py index 6233ca442..bf25ff76c 100644 --- a/netbox/extras/filtersets.py +++ b/netbox/extras/filtersets.py @@ -7,7 +7,7 @@ from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGrou from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet from tenancy.models import Tenant, TenantGroup from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter -from virtualization.models import Cluster, ClusterGroup +from virtualization.models import Cluster, ClusterGroup, ClusterType from .choices import * from .models import * @@ -279,6 +279,17 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet): to_field_name='slug', label='Platform (slug)', ) + cluster_type_id = django_filters.ModelMultipleChoiceFilter( + field_name='cluster_types', + queryset=ClusterType.objects.all(), + label='Cluster type', + ) + cluster_type = django_filters.ModelMultipleChoiceFilter( + field_name='cluster_types__slug', + queryset=ClusterType.objects.all(), + to_field_name='slug', + label='Cluster type (slug)', + ) cluster_group_id = django_filters.ModelMultipleChoiceFilter( field_name='cluster_groups', queryset=ClusterGroup.objects.all(), diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 07375a203..29527c20e 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -12,7 +12,7 @@ from utilities.forms import ( add_blank_choice, APISelectMultiple, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DateTimePicker, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, ) -from virtualization.models import Cluster, ClusterGroup +from virtualization.models import Cluster, ClusterGroup, ClusterType __all__ = ( 'ConfigContextFilterForm', @@ -158,7 +158,7 @@ class ConfigContextFilterForm(FilterForm): ['q', 'tag'], ['region_id', 'site_group_id', 'site_id'], ['device_type_id', 'platform_id', 'role_id'], - ['cluster_group_id', 'cluster_id'], + ['cluster_type_id', 'cluster_group_id', 'cluster_id'], ['tenant_group_id', 'tenant_id'] ] region_id = DynamicModelMultipleChoiceField( @@ -197,6 +197,12 @@ class ConfigContextFilterForm(FilterForm): label=_('Platforms'), fetch_trigger='open' ) + cluster_type_id = DynamicModelMultipleChoiceField( + queryset=ClusterType.objects.all(), + required=False, + label=_('Cluster types'), + fetch_trigger='open' + ) cluster_group_id = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), required=False, diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py index 1e619ebec..d75214722 100644 --- a/netbox/extras/forms/models.py +++ b/netbox/extras/forms/models.py @@ -10,7 +10,7 @@ from utilities.forms import ( add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect, ) -from virtualization.models import Cluster, ClusterGroup +from virtualization.models import Cluster, ClusterGroup, ClusterType __all__ = ( 'AddRemoveTagsForm', @@ -165,6 +165,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): queryset=Platform.objects.all(), required=False ) + cluster_types = DynamicModelMultipleChoiceField( + queryset=ClusterType.objects.all(), + required=False + ) cluster_groups = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), required=False @@ -193,7 +197,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm): model = ConfigContext fields = ( 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types', - 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', + 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', ) diff --git a/netbox/extras/migrations/0067_configcontext_cluster_types.py b/netbox/extras/migrations/0067_configcontext_cluster_types.py new file mode 100644 index 000000000..f9376da77 --- /dev/null +++ b/netbox/extras/migrations/0067_configcontext_cluster_types.py @@ -0,0 +1,17 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0026_vminterface_bridge'), + ('extras', '0066_customfield_name_validation'), + ] + + operations = [ + migrations.AddField( + model_name='configcontext', + name='cluster_types', + field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_cluster_types_+', to='virtualization.ClusterType'), + ), + ] diff --git a/netbox/extras/models/configcontexts.py b/netbox/extras/models/configcontexts.py index 8c142de8b..2a14f143f 100644 --- a/netbox/extras/models/configcontexts.py +++ b/netbox/extras/models/configcontexts.py @@ -71,6 +71,11 @@ class ConfigContext(ChangeLoggedModel): related_name='+', blank=True ) + cluster_types = models.ManyToManyField( + to='virtualization.ClusterType', + related_name='+', + blank=True + ) cluster_groups = models.ManyToManyField( to='virtualization.ClusterGroup', related_name='+', diff --git a/netbox/extras/querysets.py b/netbox/extras/querysets.py index 59d16fff8..982f33d02 100644 --- a/netbox/extras/querysets.py +++ b/netbox/extras/querysets.py @@ -22,8 +22,9 @@ class ConfigContextQuerySet(RestrictedQuerySet): # Device type assignment is relevant only for Devices device_type = getattr(obj, 'device_type', None) - # Get assigned Cluster and ClusterGroup, if any + # Get assigned cluster, group, and type (if any) cluster = getattr(obj, 'cluster', None) + cluster_type = getattr(cluster, 'type', None) cluster_group = getattr(cluster, 'group', None) # Get the group of the assigned tenant, if any @@ -44,6 +45,7 @@ class ConfigContextQuerySet(RestrictedQuerySet): Q(device_types=device_type) | Q(device_types=None), Q(roles=role) | Q(roles=None), Q(platforms=obj.platform) | Q(platforms=None), + Q(cluster_types=cluster_type) | Q(cluster_types=None), Q(cluster_groups=cluster_group) | Q(cluster_groups=None), Q(clusters=cluster) | Q(clusters=None), Q(tenant_groups=tenant_group) | Q(tenant_groups=None), @@ -93,6 +95,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet): } base_query = Q( Q(platforms=OuterRef('platform')) | Q(platforms=None), + Q(cluster_types=OuterRef('cluster__type')) | Q(cluster_types=None), Q(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None), Q(clusters=OuterRef('cluster')) | Q(clusters=None), Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None), diff --git a/netbox/extras/tables.py b/netbox/extras/tables.py index 266f2089a..62317e636 100644 --- a/netbox/extras/tables.py +++ b/netbox/extras/tables.py @@ -193,7 +193,7 @@ class ConfigContextTable(BaseTable): model = ConfigContext fields = ( 'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', - 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', + 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', ) default_columns = ('pk', 'name', 'weight', 'is_active', 'description') diff --git a/netbox/extras/tests/test_filtersets.py b/netbox/extras/tests/test_filtersets.py index 0f4b35cf6..a5f77afa9 100644 --- a/netbox/extras/tests/test_filtersets.py +++ b/netbox/extras/tests/test_filtersets.py @@ -399,6 +399,13 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): ) Platform.objects.bulk_create(platforms) + cluster_types = ( + ClusterType(name='Cluster Type 1', slug='cluster-type-1'), + ClusterType(name='Cluster Type 2', slug='cluster-type-2'), + ClusterType(name='Cluster Type 3', slug='cluster-type-3'), + ) + ClusterType.objects.bulk_create(cluster_types) + cluster_groups = ( ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'), ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'), @@ -406,11 +413,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): ) ClusterGroup.objects.bulk_create(cluster_groups) - cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1') clusters = ( - Cluster(name='Cluster 1', type=cluster_type), - Cluster(name='Cluster 2', type=cluster_type), - Cluster(name='Cluster 3', type=cluster_type), + Cluster(name='Cluster 1', type=cluster_types[0]), + Cluster(name='Cluster 2', type=cluster_types[1]), + Cluster(name='Cluster 3', type=cluster_types[2]), ) Cluster.objects.bulk_create(clusters) @@ -442,6 +448,7 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): c.device_types.set([device_types[i]]) c.roles.set([device_roles[i]]) c.platforms.set([platforms[i]]) + c.cluster_types.set([cluster_types[i]]) c.cluster_groups.set([cluster_groups[i]]) c.clusters.set([clusters[i]]) c.tenant_groups.set([tenant_groups[i]]) @@ -504,6 +511,13 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests): params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_cluster_type(self): + cluster_types = ClusterType.objects.all()[:2] + params = {'cluster_type_id': [cluster_types[0].pk, cluster_types[1].pk]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + params = {'cluster_type': [cluster_types[0].slug, cluster_types[1].slug]} + self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) + def test_cluster(self): clusters = Cluster.objects.all()[:2] params = {'cluster_id': [clusters[0].pk, clusters[1].pk]} diff --git a/netbox/extras/tests/test_models.py b/netbox/extras/tests/test_models.py index 10d4168b4..17138d42b 100644 --- a/netbox/extras/tests/test_models.py +++ b/netbox/extras/tests/test_models.py @@ -216,80 +216,77 @@ class ConfigContextTest(TestCase): self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context()) def test_annotation_same_as_get_for_object_virtualmachine_relations(self): + cluster_type = ClusterType.objects.create(name="Cluster Type") + cluster_group = ClusterGroup.objects.create(name="Cluster Group") + cluster = Cluster.objects.create(name="Cluster", group=cluster_group, type=cluster_type) site_context = ConfigContext.objects.create( name="site", weight=100, - data={ - "site": 1 - } + data={"site": 1} ) site_context.sites.add(self.site) + region_context = ConfigContext.objects.create( name="region", weight=100, - data={ - "region": 1 - } + data={"region": 1} ) region_context.regions.add(self.region) + sitegroup_context = ConfigContext.objects.create( name="sitegroup", weight=100, - data={ - "sitegroup": 1 - } + data={"sitegroup": 1} ) sitegroup_context.site_groups.add(self.sitegroup) + platform_context = ConfigContext.objects.create( name="platform", weight=100, - data={ - "platform": 1 - } + data={"platform": 1} ) platform_context.platforms.add(self.platform) + tenant_group_context = ConfigContext.objects.create( name="tenant group", weight=100, - data={ - "tenant_group": 1 - } + data={"tenant_group": 1} ) tenant_group_context.tenant_groups.add(self.tenantgroup) + tenant_context = ConfigContext.objects.create( name="tenant", weight=100, - data={ - "tenant": 1 - } + data={"tenant": 1} ) tenant_context.tenants.add(self.tenant) + tag_context = ConfigContext.objects.create( name="tag", weight=100, - data={ - "tag": 1 - } + data={"tag": 1} ) tag_context.tags.add(self.tag) - cluster_group = ClusterGroup.objects.create(name="Cluster Group") + + cluster_type_context = ConfigContext.objects.create( + name="cluster type", + weight=100, + data={"cluster_type": 1} + ) + cluster_type_context.cluster_types.add(cluster_type) + cluster_group_context = ConfigContext.objects.create( name="cluster group", weight=100, - data={ - "cluster_group": 1 - } + data={"cluster_group": 1} ) cluster_group_context.cluster_groups.add(cluster_group) - cluster_type = ClusterType.objects.create(name="Cluster Type 1") - cluster = Cluster.objects.create(name="Cluster", group=cluster_group, type=cluster_type) + cluster_context = ConfigContext.objects.create( name="cluster", weight=100, - data={ - "cluster": 1 - } + data={"cluster": 1} ) cluster_context.clusters.add(cluster) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 256709c6a..1660f8210 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -285,6 +285,7 @@ class ConfigContextView(generic.ObjectView): ('Device Types', instance.device_types.all), ('Roles', instance.roles.all), ('Platforms', instance.platforms.all), + ('Cluster Types', instance.cluster_types.all), ('Cluster Groups', instance.cluster_groups.all), ('Clusters', instance.clusters.all), ('Tenant Groups', instance.tenant_groups.all), diff --git a/netbox/templates/extras/configcontext_edit.html b/netbox/templates/extras/configcontext_edit.html index 4e4506dc6..7b37a69c6 100644 --- a/netbox/templates/extras/configcontext_edit.html +++ b/netbox/templates/extras/configcontext_edit.html @@ -20,6 +20,7 @@ {% render_field form.device_types %} {% render_field form.roles %} {% render_field form.platforms %} + {% render_field form.cluster_types %} {% render_field form.cluster_groups %} {% render_field form.clusters %} {% render_field form.tenant_groups %}