Closes #8471: Add status field to Cluster

This commit is contained in:
jeremystretch 2022-05-19 16:13:22 -04:00
parent e208404e3a
commit 64146b8cb1
15 changed files with 110 additions and 24 deletions

View File

@ -1,5 +1,5 @@
# Clusters # Clusters
A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification), and may optionally be assigned to a cluster group, site, and/or tenant. Each cluster must have a unique name within its assigned group and/or site, if any. A cluster is a logical grouping of physical resources within which virtual machines run. A cluster must be assigned a type (technological classification) and operational status, and may optionally be assigned to a cluster group, site, and/or tenant. Each cluster must have a unique name within its assigned group and/or site, if any.
Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device. Physical devices may be associated with clusters as hosts. This allows users to track on which host(s) a particular virtual machine may reside. However, NetBox does not support pinning a specific VM within a cluster to a particular host device.

View File

@ -9,6 +9,7 @@
### Enhancements ### Enhancements
* [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses * [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
* [#8471](https://github.com/netbox-community/netbox/issues/8471) - Add `status` field to Cluster
* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping * [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results * [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
@ -23,3 +24,5 @@
* ipam.IPAddress * ipam.IPAddress
* The `nat_inside` field no longer requires a unique value * The `nat_inside` field no longer requires a unique value
* The `nat_outside` field has changed from a single IP address instance to a list of multiple IP addresses * The `nat_outside` field has changed from a single IP address instance to a list of multiple IP addresses
* virtualization.Cluster
* Add required `status` field (default value: `active`)

View File

@ -45,6 +45,7 @@ class ClusterSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail') url = serializers.HyperlinkedIdentityField(view_name='virtualization-api:cluster-detail')
type = NestedClusterTypeSerializer() type = NestedClusterTypeSerializer()
group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None) group = NestedClusterGroupSerializer(required=False, allow_null=True, default=None)
status = ChoiceField(choices=ClusterStatusChoices, required=False)
tenant = NestedTenantSerializer(required=False, allow_null=True) tenant = NestedTenantSerializer(required=False, allow_null=True)
site = NestedSiteSerializer(required=False, allow_null=True, default=None) site = NestedSiteSerializer(required=False, allow_null=True, default=None)
device_count = serializers.IntegerField(read_only=True) device_count = serializers.IntegerField(read_only=True)
@ -53,8 +54,8 @@ class ClusterSerializer(NetBoxModelSerializer):
class Meta: class Meta:
model = Cluster model = Cluster
fields = [ fields = [
'id', 'url', 'display', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags', 'custom_fields', 'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'comments', 'tags',
'created', 'last_updated', 'device_count', 'virtualmachine_count', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
] ]

View File

@ -1,6 +1,28 @@
from utilities.choices import ChoiceSet from utilities.choices import ChoiceSet
#
# Clusters
#
class ClusterStatusChoices(ChoiceSet):
key = 'Cluster.status'
STATUS_PLANNED = 'planned'
STATUS_STAGING = 'staging'
STATUS_ACTIVE = 'active'
STATUS_DECOMMISSIONING = 'decommissioning'
STATUS_OFFLINE = 'offline'
CHOICES = [
(STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_STAGING, 'Staging', 'blue'),
(STATUS_ACTIVE, 'Active', 'green'),
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
(STATUS_OFFLINE, 'Offline', 'red'),
]
# #
# VirtualMachines # VirtualMachines
# #

View File

@ -90,6 +90,10 @@ class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilte
to_field_name='slug', to_field_name='slug',
label='Cluster type (slug)', label='Cluster type (slug)',
) )
status = django_filters.MultipleChoiceFilter(
choices=ClusterStatusChoices,
null_value=None
)
class Meta: class Meta:
model = Cluster model = Cluster

View File

@ -58,6 +58,12 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
queryset=ClusterGroup.objects.all(), queryset=ClusterGroup.objects.all(),
required=False required=False
) )
status = forms.ChoiceField(
choices=add_blank_choice(ClusterStatusChoices),
required=False,
initial='',
widget=StaticSelect()
)
tenant = DynamicModelChoiceField( tenant = DynamicModelChoiceField(
queryset=Tenant.objects.all(), queryset=Tenant.objects.all(),
required=False required=False
@ -85,7 +91,7 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm):
model = Cluster model = Cluster
fieldsets = ( fieldsets = (
(None, ('type', 'group', 'tenant',)), (None, ('type', 'group', 'status', 'tenant',)),
('Site', ('region', 'site_group', 'site',)), ('Site', ('region', 'site_group', 'site',)),
) )
nullable_fields = ( nullable_fields = (

View File

@ -44,6 +44,10 @@ class ClusterCSVForm(NetBoxModelCSVForm):
required=False, required=False,
help_text='Assigned cluster group' help_text='Assigned cluster group'
) )
status = CSVChoiceField(
choices=ClusterStatusChoices,
help_text='Operational status'
)
site = CSVModelChoiceField( site = CSVModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='name', to_field_name='name',
@ -59,7 +63,7 @@ class ClusterCSVForm(NetBoxModelCSVForm):
class Meta: class Meta:
model = Cluster model = Cluster
fields = ('name', 'type', 'group', 'site', 'comments') fields = ('name', 'type', 'group', 'status', 'site', 'comments')
class VirtualMachineCSVForm(NetBoxModelCSVForm): class VirtualMachineCSVForm(NetBoxModelCSVForm):

View File

@ -35,7 +35,7 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
model = Cluster model = Cluster
fieldsets = ( fieldsets = (
(None, ('q', 'tag')), (None, ('q', 'tag')),
('Attributes', ('group_id', 'type_id')), ('Attributes', ('group_id', 'type_id', 'status')),
('Location', ('region_id', 'site_group_id', 'site_id')), ('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')), ('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role')), ('Contacts', ('contact', 'contact_role')),
@ -50,6 +50,10 @@ class ClusterFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
required=False, required=False,
label=_('Region') label=_('Region')
) )
status = MultipleChoiceField(
choices=ClusterStatusChoices,
required=False
)
site_group_id = DynamicModelMultipleChoiceField( site_group_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(), queryset=SiteGroup.objects.all(),
required=False, required=False,

View File

@ -79,15 +79,19 @@ class ClusterForm(TenancyForm, NetBoxModelForm):
comments = CommentField() comments = CommentField()
fieldsets = ( fieldsets = (
('Cluster', ('name', 'type', 'group', 'region', 'site_group', 'site', 'tags')), ('Cluster', ('name', 'type', 'group', 'status', 'tags')),
('Site', ('region', 'site_group', 'site')),
('Tenancy', ('tenant_group', 'tenant')), ('Tenancy', ('tenant_group', 'tenant')),
) )
class Meta: class Meta:
model = Cluster model = Cluster
fields = ( fields = (
'name', 'type', 'group', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags', 'name', 'type', 'group', 'status', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags',
) )
widgets = {
'status': StaticSelect(),
}
class ClusterAddDevicesForm(BootstrapMixin, forms.Form): class ClusterAddDevicesForm(BootstrapMixin, forms.Form):

View File

@ -0,0 +1,18 @@
# Generated by Django 4.0.4 on 2022-05-19 19:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('virtualization', '0029_created_datetimefield'),
]
operations = [
migrations.AddField(
model_name='cluster',
name='status',
field=models.CharField(default='active', max_length=50),
),
]

View File

@ -119,6 +119,11 @@ class Cluster(NetBoxModel):
blank=True, blank=True,
null=True null=True
) )
status = models.CharField(
max_length=50,
choices=ClusterStatusChoices,
default=ClusterStatusChoices.STATUS_ACTIVE
)
tenant = models.ForeignKey( tenant = models.ForeignKey(
to='tenancy.Tenant', to='tenancy.Tenant',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -165,6 +170,9 @@ class Cluster(NetBoxModel):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('virtualization:cluster', args=[self.pk]) return reverse('virtualization:cluster', args=[self.pk])
def get_status_color(self):
return ClusterStatusChoices.colors.get(self.status)
def clean(self): def clean(self):
super().clean() super().clean()

View File

@ -66,6 +66,7 @@ class ClusterTable(NetBoxTable):
group = tables.Column( group = tables.Column(
linkify=True linkify=True
) )
status = columns.ChoiceFieldColumn()
tenant = tables.Column( tenant = tables.Column(
linkify=True linkify=True
) )
@ -93,7 +94,7 @@ class ClusterTable(NetBoxTable):
class Meta(NetBoxTable.Meta): class Meta(NetBoxTable.Meta):
model = Cluster model = Cluster
fields = ( fields = (
'pk', 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'device_count', 'vm_count', 'contacts', 'pk', 'id', 'name', 'type', 'group', 'status', 'tenant', 'site', 'comments', 'device_count', 'vm_count',
'tags', 'created', 'last_updated', 'contacts', 'tags', 'created', 'last_updated',
) )
default_columns = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count') default_columns = ('pk', 'name', 'type', 'group', 'status', 'tenant', 'site', 'device_count', 'vm_count')

View File

@ -4,6 +4,7 @@ from rest_framework import status
from dcim.choices import InterfaceModeChoices from dcim.choices import InterfaceModeChoices
from ipam.models import VLAN, VRF from ipam.models import VLAN, VRF
from utilities.testing import APITestCase, APIViewTestCases from utilities.testing import APITestCase, APIViewTestCases
from virtualization.choices import *
from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface from virtualization.models import Cluster, ClusterGroup, ClusterType, VirtualMachine, VMInterface
@ -85,6 +86,7 @@ class ClusterTest(APIViewTestCases.APIViewTestCase):
model = Cluster model = Cluster
brief_fields = ['display', 'id', 'name', 'url', 'virtualmachine_count'] brief_fields = ['display', 'id', 'name', 'url', 'virtualmachine_count']
bulk_update_data = { bulk_update_data = {
'status': 'offline',
'comments': 'New comment', 'comments': 'New comment',
} }
@ -104,9 +106,9 @@ class ClusterTest(APIViewTestCases.APIViewTestCase):
ClusterGroup.objects.bulk_create(cluster_groups) ClusterGroup.objects.bulk_create(cluster_groups)
clusters = ( clusters = (
Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0]), Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED),
Cluster(name='Cluster 2', type=cluster_types[0], group=cluster_groups[0]), Cluster(name='Cluster 2', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED),
Cluster(name='Cluster 3', type=cluster_types[0], group=cluster_groups[0]), Cluster(name='Cluster 3', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED),
) )
Cluster.objects.bulk_create(clusters) Cluster.objects.bulk_create(clusters)
@ -115,16 +117,19 @@ class ClusterTest(APIViewTestCases.APIViewTestCase):
'name': 'Cluster 4', 'name': 'Cluster 4',
'type': cluster_types[1].pk, 'type': cluster_types[1].pk,
'group': cluster_groups[1].pk, 'group': cluster_groups[1].pk,
'status': ClusterStatusChoices.STATUS_STAGING,
}, },
{ {
'name': 'Cluster 5', 'name': 'Cluster 5',
'type': cluster_types[1].pk, 'type': cluster_types[1].pk,
'group': cluster_groups[1].pk, 'group': cluster_groups[1].pk,
'status': ClusterStatusChoices.STATUS_STAGING,
}, },
{ {
'name': 'Cluster 6', 'name': 'Cluster 6',
'type': cluster_types[1].pk, 'type': cluster_types[1].pk,
'group': cluster_groups[1].pk, 'group': cluster_groups[1].pk,
'status': ClusterStatusChoices.STATUS_STAGING,
}, },
] ]

View File

@ -123,9 +123,9 @@ class ClusterTestCase(TestCase, ChangeLoggedFilterSetTests):
Tenant.objects.bulk_create(tenants) Tenant.objects.bulk_create(tenants)
clusters = ( clusters = (
Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0], site=sites[0], tenant=tenants[0]), Cluster(name='Cluster 1', type=cluster_types[0], group=cluster_groups[0], status=ClusterStatusChoices.STATUS_PLANNED, site=sites[0], tenant=tenants[0]),
Cluster(name='Cluster 2', type=cluster_types[1], group=cluster_groups[1], site=sites[1], tenant=tenants[1]), Cluster(name='Cluster 2', type=cluster_types[1], group=cluster_groups[1], status=ClusterStatusChoices.STATUS_STAGING, site=sites[1], tenant=tenants[1]),
Cluster(name='Cluster 3', type=cluster_types[2], group=cluster_groups[2], site=sites[2], tenant=tenants[2]), Cluster(name='Cluster 3', type=cluster_types[2], group=cluster_groups[2], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[2], tenant=tenants[2]),
) )
Cluster.objects.bulk_create(clusters) Cluster.objects.bulk_create(clusters)
@ -161,6 +161,10 @@ class ClusterTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'group': [groups[0].slug, groups[1].slug]} params = {'group': [groups[0].slug, 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_status(self):
params = {'status': [ClusterStatusChoices.STATUS_PLANNED, ClusterStatusChoices.STATUS_STAGING]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_type(self): def test_type(self):
types = ClusterType.objects.all()[:2] types = ClusterType.objects.all()[:2]
params = {'type_id': [types[0].pk, types[1].pk]} params = {'type_id': [types[0].pk, types[1].pk]}

View File

@ -101,9 +101,9 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
ClusterType.objects.bulk_create(clustertypes) ClusterType.objects.bulk_create(clustertypes)
Cluster.objects.bulk_create([ Cluster.objects.bulk_create([
Cluster(name='Cluster 1', group=clustergroups[0], type=clustertypes[0], site=sites[0]), Cluster(name='Cluster 1', group=clustergroups[0], type=clustertypes[0], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[0]),
Cluster(name='Cluster 2', group=clustergroups[0], type=clustertypes[0], site=sites[0]), Cluster(name='Cluster 2', group=clustergroups[0], type=clustertypes[0], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[0]),
Cluster(name='Cluster 3', group=clustergroups[0], type=clustertypes[0], site=sites[0]), Cluster(name='Cluster 3', group=clustergroups[0], type=clustertypes[0], status=ClusterStatusChoices.STATUS_ACTIVE, site=sites[0]),
]) ])
tags = create_tags('Alpha', 'Bravo', 'Charlie') tags = create_tags('Alpha', 'Bravo', 'Charlie')
@ -112,6 +112,7 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
'name': 'Cluster X', 'name': 'Cluster X',
'group': clustergroups[1].pk, 'group': clustergroups[1].pk,
'type': clustertypes[1].pk, 'type': clustertypes[1].pk,
'status': ClusterStatusChoices.STATUS_OFFLINE,
'tenant': None, 'tenant': None,
'site': sites[1].pk, 'site': sites[1].pk,
'comments': 'Some comments', 'comments': 'Some comments',
@ -119,15 +120,16 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
} }
cls.csv_data = ( cls.csv_data = (
"name,type", "name,type,status",
"Cluster 4,Cluster Type 1", "Cluster 4,Cluster Type 1,active",
"Cluster 5,Cluster Type 1", "Cluster 5,Cluster Type 1,active",
"Cluster 6,Cluster Type 1", "Cluster 6,Cluster Type 1,active",
) )
cls.bulk_edit_data = { cls.bulk_edit_data = {
'group': clustergroups[1].pk, 'group': clustergroups[1].pk,
'type': clustertypes[1].pk, 'type': clustertypes[1].pk,
'status': ClusterStatusChoices.STATUS_OFFLINE,
'tenant': None, 'tenant': None,
'site': sites[1].pk, 'site': sites[1].pk,
'comments': 'New comments', 'comments': 'New comments',