Jelajahi Sumber

Closes #8471: Add status field to Cluster

jeremystretch 3 tahun lalu
induk
melakukan
64146b8cb1

+ 1 - 1
docs/models/virtualization/cluster.md

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

+ 3 - 0
docs/release-notes/version-3.3.md

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

+ 3 - 2
netbox/virtualization/api/serializers.py

@@ -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',
-            'created', 'last_updated', 'device_count', 'virtualmachine_count',
+            'id', 'url', 'display', 'name', 'type', 'group', 'status', 'tenant', 'site', 'comments', 'tags',
+            'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
         ]
         ]
 
 
 
 

+ 22 - 0
netbox/virtualization/choices.py

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

+ 4 - 0
netbox/virtualization/filtersets.py

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

+ 7 - 1
netbox/virtualization/forms/bulk_edit.py

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

+ 5 - 1
netbox/virtualization/forms/bulk_import.py

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

+ 5 - 1
netbox/virtualization/forms/filtersets.py

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

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

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

+ 18 - 0
netbox/virtualization/migrations/0030_cluster_status.py

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

+ 8 - 0
netbox/virtualization/models.py

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

+ 4 - 3
netbox/virtualization/tables/clusters.py

@@ -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',
-            'tags', 'created', 'last_updated',
+            'pk', 'id', 'name', 'type', 'group', 'status', 'tenant', 'site', 'comments', 'device_count', 'vm_count',
+            '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')

+ 8 - 3
netbox/virtualization/tests/test_api.py

@@ -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 2', type=cluster_types[0], group=cluster_groups[0]),
-            Cluster(name='Cluster 3', 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], status=ClusterStatusChoices.STATUS_PLANNED),
+            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,
             },
             },
         ]
         ]
 
 

+ 7 - 3
netbox/virtualization/tests/test_filtersets.py

@@ -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 2', type=cluster_types[1], group=cluster_groups[1], 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 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], status=ClusterStatusChoices.STATUS_STAGING, site=sites[1], tenant=tenants[1]),
+            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]}

+ 9 - 7
netbox/virtualization/tests/test_views.py

@@ -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 2', group=clustergroups[0], type=clustertypes[0], site=sites[0]),
-            Cluster(name='Cluster 3', 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], status=ClusterStatusChoices.STATUS_ACTIVE, 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",
-            "Cluster 4,Cluster Type 1",
-            "Cluster 5,Cluster Type 1",
-            "Cluster 6,Cluster Type 1",
+            "name,type,status",
+            "Cluster 4,Cluster Type 1,active",
+            "Cluster 5,Cluster Type 1,active",
+            "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',