Sfoglia il codice sorgente

Closes #7784: Support cluster type assignment for config contexts

jeremystretch 4 anni fa
parent
commit
77dd684916

+ 3 - 0
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
 * [#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
 * [#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
 * [#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
 * [#8168](https://github.com/netbox-community/netbox/issues/8168) - Add `min_vid` and `max_vid` fields to VLAN group
 
 
 ### Other Changes
 ### Other Changes
@@ -77,6 +78,8 @@ FIELD_CHOICES = {
     * Added `module` field
     * Added `module` field
 * dcim.Site
 * dcim.Site
     * Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields
     * Removed the `asn`, `contact_name`, `contact_phone`, and `contact_email` fields
+* extras.ConfigContext
+    * Add `cluster_types` field
 * ipam.VLANGroup
 * ipam.VLANGroup
     * Added the `/availables-vlans/` endpoint
     * Added the `/availables-vlans/` endpoint
     * Added the `min_vid` and `max_vid` fields
     * Added the `min_vid` and `max_vid` fields

+ 12 - 4
netbox/extras/api/serializers.py

@@ -19,8 +19,10 @@ from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantG
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from users.api.nested_serializers import NestedUserSerializer
 from users.api.nested_serializers import NestedUserSerializer
 from utilities.api import get_serializer_for_model
 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 *
 from .nested_serializers import *
 
 
 __all__ = (
 __all__ = (
@@ -267,6 +269,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         required=False,
         required=False,
         many=True
         many=True
     )
     )
+    cluster_types = SerializedPKRelatedField(
+        queryset=ClusterType.objects.all(),
+        serializer=NestedClusterTypeSerializer,
+        required=False,
+        many=True
+    )
     cluster_groups = SerializedPKRelatedField(
     cluster_groups = SerializedPKRelatedField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         serializer=NestedClusterGroupSerializer,
         serializer=NestedClusterGroupSerializer,
@@ -302,8 +310,8 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         model = ConfigContext
         model = ConfigContext
         fields = [
         fields = [
             'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
             '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',
         ]
         ]
 
 
 
 

+ 12 - 1
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 netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet
 from tenancy.models import Tenant, TenantGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
 from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
-from virtualization.models import Cluster, ClusterGroup
+from virtualization.models import Cluster, ClusterGroup, ClusterType
 from .choices import *
 from .choices import *
 from .models import *
 from .models import *
 
 
@@ -279,6 +279,17 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
         to_field_name='slug',
         to_field_name='slug',
         label='Platform (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(
     cluster_group_id = django_filters.ModelMultipleChoiceFilter(
         field_name='cluster_groups',
         field_name='cluster_groups',
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),

+ 8 - 2
netbox/extras/forms/filtersets.py

@@ -12,7 +12,7 @@ from utilities.forms import (
     add_blank_choice, APISelectMultiple, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DateTimePicker,
     add_blank_choice, APISelectMultiple, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DateTimePicker,
     DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES,
     DynamicModelMultipleChoiceField, FilterForm, StaticSelect, StaticSelectMultiple, BOOLEAN_WITH_BLANK_CHOICES,
 )
 )
-from virtualization.models import Cluster, ClusterGroup
+from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
 __all__ = (
 __all__ = (
     'ConfigContextFilterForm',
     'ConfigContextFilterForm',
@@ -158,7 +158,7 @@ class ConfigContextFilterForm(FilterForm):
         ['q', 'tag'],
         ['q', 'tag'],
         ['region_id', 'site_group_id', 'site_id'],
         ['region_id', 'site_group_id', 'site_id'],
         ['device_type_id', 'platform_id', 'role_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']
         ['tenant_group_id', 'tenant_id']
     ]
     ]
     region_id = DynamicModelMultipleChoiceField(
     region_id = DynamicModelMultipleChoiceField(
@@ -197,6 +197,12 @@ class ConfigContextFilterForm(FilterForm):
         label=_('Platforms'),
         label=_('Platforms'),
         fetch_trigger='open'
         fetch_trigger='open'
     )
     )
+    cluster_type_id = DynamicModelMultipleChoiceField(
+        queryset=ClusterType.objects.all(),
+        required=False,
+        label=_('Cluster types'),
+        fetch_trigger='open'
+    )
     cluster_group_id = DynamicModelMultipleChoiceField(
     cluster_group_id = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         required=False,
         required=False,

+ 6 - 2
netbox/extras/forms/models.py

@@ -10,7 +10,7 @@ from utilities.forms import (
     add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField,
     add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField,
     ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
     ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect,
 )
 )
-from virtualization.models import Cluster, ClusterGroup
+from virtualization.models import Cluster, ClusterGroup, ClusterType
 
 
 __all__ = (
 __all__ = (
     'AddRemoveTagsForm',
     'AddRemoveTagsForm',
@@ -165,6 +165,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
         queryset=Platform.objects.all(),
         queryset=Platform.objects.all(),
         required=False
         required=False
     )
     )
+    cluster_types = DynamicModelMultipleChoiceField(
+        queryset=ClusterType.objects.all(),
+        required=False
+    )
     cluster_groups = DynamicModelMultipleChoiceField(
     cluster_groups = DynamicModelMultipleChoiceField(
         queryset=ClusterGroup.objects.all(),
         queryset=ClusterGroup.objects.all(),
         required=False
         required=False
@@ -193,7 +197,7 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
         model = ConfigContext
         model = ConfigContext
         fields = (
         fields = (
             'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
             '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',
         )
         )
 
 
 
 

+ 17 - 0
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'),
+        ),
+    ]

+ 5 - 0
netbox/extras/models/configcontexts.py

@@ -71,6 +71,11 @@ class ConfigContext(ChangeLoggedModel):
         related_name='+',
         related_name='+',
         blank=True
         blank=True
     )
     )
+    cluster_types = models.ManyToManyField(
+        to='virtualization.ClusterType',
+        related_name='+',
+        blank=True
+    )
     cluster_groups = models.ManyToManyField(
     cluster_groups = models.ManyToManyField(
         to='virtualization.ClusterGroup',
         to='virtualization.ClusterGroup',
         related_name='+',
         related_name='+',

+ 4 - 1
netbox/extras/querysets.py

@@ -22,8 +22,9 @@ class ConfigContextQuerySet(RestrictedQuerySet):
         # Device type assignment is relevant only for Devices
         # Device type assignment is relevant only for Devices
         device_type = getattr(obj, 'device_type', None)
         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 = getattr(obj, 'cluster', None)
+        cluster_type = getattr(cluster, 'type', None)
         cluster_group = getattr(cluster, 'group', None)
         cluster_group = getattr(cluster, 'group', None)
 
 
         # Get the group of the assigned tenant, if any
         # 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(device_types=device_type) | Q(device_types=None),
             Q(roles=role) | Q(roles=None),
             Q(roles=role) | Q(roles=None),
             Q(platforms=obj.platform) | Q(platforms=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(cluster_groups=cluster_group) | Q(cluster_groups=None),
             Q(clusters=cluster) | Q(clusters=None),
             Q(clusters=cluster) | Q(clusters=None),
             Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
             Q(tenant_groups=tenant_group) | Q(tenant_groups=None),
@@ -93,6 +95,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
         }
         }
         base_query = Q(
         base_query = Q(
             Q(platforms=OuterRef('platform')) | Q(platforms=None),
             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(cluster_groups=OuterRef('cluster__group')) | Q(cluster_groups=None),
             Q(clusters=OuterRef('cluster')) | Q(clusters=None),
             Q(clusters=OuterRef('cluster')) | Q(clusters=None),
             Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),
             Q(tenant_groups=OuterRef('tenant__group')) | Q(tenant_groups=None),

+ 1 - 1
netbox/extras/tables.py

@@ -193,7 +193,7 @@ class ConfigContextTable(BaseTable):
         model = ConfigContext
         model = ConfigContext
         fields = (
         fields = (
             'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles',
             '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')
         default_columns = ('pk', 'name', 'weight', 'is_active', 'description')
 
 

+ 18 - 4
netbox/extras/tests/test_filtersets.py

@@ -399,6 +399,13 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         )
         Platform.objects.bulk_create(platforms)
         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 = (
         cluster_groups = (
             ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
             ClusterGroup(name='Cluster Group 1', slug='cluster-group-1'),
             ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
             ClusterGroup(name='Cluster Group 2', slug='cluster-group-2'),
@@ -406,11 +413,10 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
         )
         )
         ClusterGroup.objects.bulk_create(cluster_groups)
         ClusterGroup.objects.bulk_create(cluster_groups)
 
 
-        cluster_type = ClusterType.objects.create(name='Cluster Type 1', slug='cluster-type-1')
         clusters = (
         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)
         Cluster.objects.bulk_create(clusters)
 
 
@@ -442,6 +448,7 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
             c.device_types.set([device_types[i]])
             c.device_types.set([device_types[i]])
             c.roles.set([device_roles[i]])
             c.roles.set([device_roles[i]])
             c.platforms.set([platforms[i]])
             c.platforms.set([platforms[i]])
+            c.cluster_types.set([cluster_types[i]])
             c.cluster_groups.set([cluster_groups[i]])
             c.cluster_groups.set([cluster_groups[i]])
             c.clusters.set([clusters[i]])
             c.clusters.set([clusters[i]])
             c.tenant_groups.set([tenant_groups[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]}
         params = {'cluster_group': [cluster_groups[0].slug, cluster_groups[1].slug]}
         self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
         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):
     def test_cluster(self):
         clusters = Cluster.objects.all()[:2]
         clusters = Cluster.objects.all()[:2]
         params = {'cluster_id': [clusters[0].pk, clusters[1].pk]}
         params = {'cluster_id': [clusters[0].pk, clusters[1].pk]}

+ 27 - 30
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())
         self.assertEqual(device.get_config_context(), annotated_queryset[0].get_config_context())
 
 
     def test_annotation_same_as_get_for_object_virtualmachine_relations(self):
     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(
         site_context = ConfigContext.objects.create(
             name="site",
             name="site",
             weight=100,
             weight=100,
-            data={
-                "site": 1
-            }
+            data={"site": 1}
         )
         )
         site_context.sites.add(self.site)
         site_context.sites.add(self.site)
+
         region_context = ConfigContext.objects.create(
         region_context = ConfigContext.objects.create(
             name="region",
             name="region",
             weight=100,
             weight=100,
-            data={
-                "region": 1
-            }
+            data={"region": 1}
         )
         )
         region_context.regions.add(self.region)
         region_context.regions.add(self.region)
+
         sitegroup_context = ConfigContext.objects.create(
         sitegroup_context = ConfigContext.objects.create(
             name="sitegroup",
             name="sitegroup",
             weight=100,
             weight=100,
-            data={
-                "sitegroup": 1
-            }
+            data={"sitegroup": 1}
         )
         )
         sitegroup_context.site_groups.add(self.sitegroup)
         sitegroup_context.site_groups.add(self.sitegroup)
+
         platform_context = ConfigContext.objects.create(
         platform_context = ConfigContext.objects.create(
             name="platform",
             name="platform",
             weight=100,
             weight=100,
-            data={
-                "platform": 1
-            }
+            data={"platform": 1}
         )
         )
         platform_context.platforms.add(self.platform)
         platform_context.platforms.add(self.platform)
+
         tenant_group_context = ConfigContext.objects.create(
         tenant_group_context = ConfigContext.objects.create(
             name="tenant group",
             name="tenant group",
             weight=100,
             weight=100,
-            data={
-                "tenant_group": 1
-            }
+            data={"tenant_group": 1}
         )
         )
         tenant_group_context.tenant_groups.add(self.tenantgroup)
         tenant_group_context.tenant_groups.add(self.tenantgroup)
+
         tenant_context = ConfigContext.objects.create(
         tenant_context = ConfigContext.objects.create(
             name="tenant",
             name="tenant",
             weight=100,
             weight=100,
-            data={
-                "tenant": 1
-            }
+            data={"tenant": 1}
         )
         )
         tenant_context.tenants.add(self.tenant)
         tenant_context.tenants.add(self.tenant)
+
         tag_context = ConfigContext.objects.create(
         tag_context = ConfigContext.objects.create(
             name="tag",
             name="tag",
             weight=100,
             weight=100,
-            data={
-                "tag": 1
-            }
+            data={"tag": 1}
         )
         )
         tag_context.tags.add(self.tag)
         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(
         cluster_group_context = ConfigContext.objects.create(
             name="cluster group",
             name="cluster group",
             weight=100,
             weight=100,
-            data={
-                "cluster_group": 1
-            }
+            data={"cluster_group": 1}
         )
         )
         cluster_group_context.cluster_groups.add(cluster_group)
         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(
         cluster_context = ConfigContext.objects.create(
             name="cluster",
             name="cluster",
             weight=100,
             weight=100,
-            data={
-                "cluster": 1
-            }
+            data={"cluster": 1}
         )
         )
         cluster_context.clusters.add(cluster)
         cluster_context.clusters.add(cluster)
 
 

+ 1 - 0
netbox/extras/views.py

@@ -285,6 +285,7 @@ class ConfigContextView(generic.ObjectView):
             ('Device Types', instance.device_types.all),
             ('Device Types', instance.device_types.all),
             ('Roles', instance.roles.all),
             ('Roles', instance.roles.all),
             ('Platforms', instance.platforms.all),
             ('Platforms', instance.platforms.all),
+            ('Cluster Types', instance.cluster_types.all),
             ('Cluster Groups', instance.cluster_groups.all),
             ('Cluster Groups', instance.cluster_groups.all),
             ('Clusters', instance.clusters.all),
             ('Clusters', instance.clusters.all),
             ('Tenant Groups', instance.tenant_groups.all),
             ('Tenant Groups', instance.tenant_groups.all),

+ 1 - 0
netbox/templates/extras/configcontext_edit.html

@@ -20,6 +20,7 @@
             {% render_field form.device_types %}
             {% render_field form.device_types %}
             {% render_field form.roles %}
             {% render_field form.roles %}
             {% render_field form.platforms %}
             {% render_field form.platforms %}
+            {% render_field form.cluster_types %}
             {% render_field form.cluster_groups %}
             {% render_field form.cluster_groups %}
             {% render_field form.clusters %}
             {% render_field form.clusters %}
             {% render_field form.tenant_groups %}
             {% render_field form.tenant_groups %}