Closes #7784: Support cluster type assignment for config contexts

This commit is contained in:
jeremystretch 2021-12-23 14:20:03 -05:00
parent bffd22038b
commit 77dd684916
13 changed files with 115 additions and 45 deletions

View File

@ -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

View File

@ -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.api.nested_serializers import (
from virtualization.models import Cluster, ClusterGroup 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', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
'data', 'created', 'last_updated', 'tenants', 'tags', 'data', 'created', 'last_updated',
] ]

View File

@ -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(),

View File

@ -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,

View File

@ -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',
) )

View File

@ -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'),
),
]

View File

@ -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='+',

View File

@ -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),

View File

@ -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')

View File

@ -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 1', type=cluster_types[0]),
Cluster(name='Cluster 2', type=cluster_type), Cluster(name='Cluster 2', type=cluster_types[1]),
Cluster(name='Cluster 3', type=cluster_type), 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]}

View File

@ -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={ data={"site": 1}
"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={ data={"region": 1}
"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={ data={"sitegroup": 1}
"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={ data={"platform": 1}
"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={ data={"tenant_group": 1}
"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={ data={"tenant": 1}
"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={ data={"tag": 1}
"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={ data={"cluster_group": 1}
"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={ data={"cluster": 1}
"cluster": 1
}
) )
cluster_context.clusters.add(cluster) cluster_context.clusters.add(cluster)

View File

@ -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),

View File

@ -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 %}