diff --git a/netbox/templates/tenancy/tenant.html b/netbox/templates/tenancy/tenant.html index 053c69121..a03d60523 100644 --- a/netbox/templates/tenancy/tenant.html +++ b/netbox/templates/tenancy/tenant.html @@ -142,6 +142,10 @@

{{ stats.virtualmachine_count }}

Virtual machines

+
+

{{ stats.cluster_count }}

+

Clusters

+
diff --git a/netbox/templates/virtualization/cluster.html b/netbox/templates/virtualization/cluster.html index b543e85b5..264538237 100644 --- a/netbox/templates/virtualization/cluster.html +++ b/netbox/templates/virtualization/cluster.html @@ -83,6 +83,16 @@ {% endif %} + + Tenant + + {% if cluster.tenant %} + {{ cluster.tenant }} + {% else %} + None + {% endif %} + + Site diff --git a/netbox/templates/virtualization/cluster_edit.html b/netbox/templates/virtualization/cluster_edit.html index 629c779ec..0e188d8ab 100644 --- a/netbox/templates/virtualization/cluster_edit.html +++ b/netbox/templates/virtualization/cluster_edit.html @@ -8,6 +8,7 @@ {% render_field form.name %} {% render_field form.type %} {% render_field form.group %} + {% render_field form.tenant %} {% render_field form.site %} diff --git a/netbox/tenancy/api/serializers.py b/netbox/tenancy/api/serializers.py index 28ae04694..7599029c5 100644 --- a/netbox/tenancy/api/serializers.py +++ b/netbox/tenancy/api/serializers.py @@ -31,11 +31,12 @@ class TenantSerializer(TaggitSerializer, CustomFieldModelSerializer): virtualmachine_count = serializers.IntegerField(read_only=True) vlan_count = serializers.IntegerField(read_only=True) vrf_count = serializers.IntegerField(read_only=True) + cluster_count = serializers.IntegerField(read_only=True) class Meta: model = Tenant fields = [ 'id', 'name', 'slug', 'group', 'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'circuit_count', 'device_count', 'ipaddress_count', 'prefix_count', 'rack_count', - 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', + 'site_count', 'virtualmachine_count', 'vlan_count', 'vrf_count', 'cluster_count', ] diff --git a/netbox/tenancy/views.py b/netbox/tenancy/views.py index 965ae2853..c7690d04b 100644 --- a/netbox/tenancy/views.py +++ b/netbox/tenancy/views.py @@ -9,7 +9,7 @@ from ipam.models import IPAddress, Prefix, VLAN, VRF from utilities.views import ( BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView, ) -from virtualization.models import VirtualMachine +from virtualization.models import VirtualMachine, Cluster from . import filters, forms, tables from .models import Tenant, TenantGroup @@ -80,6 +80,7 @@ class TenantView(PermissionRequiredMixin, View): 'vlan_count': VLAN.objects.filter(tenant=tenant).count(), 'circuit_count': Circuit.objects.filter(tenant=tenant).count(), 'virtualmachine_count': VirtualMachine.objects.filter(tenant=tenant).count(), + 'cluster_count': Cluster.objects.filter(tenant=tenant).count(), } return render(request, 'tenancy/tenant.html', { diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 0b98ce44a..75f36fbb6 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -38,6 +38,7 @@ class ClusterGroupSerializer(ValidatedModelSerializer): class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer): type = NestedClusterTypeSerializer() group = NestedClusterGroupSerializer(required=False, allow_null=True) + tenant = NestedTenantSerializer(required=False, allow_null=True) site = NestedSiteSerializer(required=False, allow_null=True) tags = TagListSerializerField(required=False) device_count = serializers.IntegerField(read_only=True) @@ -46,7 +47,7 @@ class ClusterSerializer(TaggitSerializer, CustomFieldModelSerializer): class Meta: model = Cluster fields = [ - 'id', 'name', 'type', 'group', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'id', 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', ] diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index f6d7f1230..94b75d154 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -41,7 +41,7 @@ class ClusterGroupViewSet(ModelViewSet): class ClusterViewSet(CustomFieldModelViewSet): queryset = Cluster.objects.prefetch_related( - 'type', 'group', 'site', 'tags' + 'type', 'group', 'tenant', 'site', 'tags' ).annotate( device_count=get_subquery(Device, 'cluster'), virtualmachine_count=get_subquery(VirtualMachine, 'cluster') diff --git a/netbox/virtualization/filters.py b/netbox/virtualization/filters.py index 8365d6f91..a438d8598 100644 --- a/netbox/virtualization/filters.py +++ b/netbox/virtualization/filters.py @@ -4,6 +4,7 @@ from netaddr import EUI from netaddr.core import AddrFormatError from dcim.models import DeviceRole, Interface, Platform, Region, Site +from tenancy.models import Tenant from extras.filters import CustomFieldFilterSet from tenancy.filtersets import TenancyFilterSet from utilities.filters import ( @@ -56,6 +57,10 @@ class ClusterFilter(CustomFieldFilterSet): to_field_name='slug', label='Cluster type (slug)', ) + tenant = django_filters.ModelMultipleChoiceFilter( + queryset=Tenant.objects.all(), + label="Tenant (ID)" + ) site_id = django_filters.ModelMultipleChoiceFilter( queryset=Site.objects.all(), label='Site (ID)', diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 2cf55fcde..8094b0fbe 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -86,7 +86,7 @@ class ClusterForm(BootstrapMixin, CustomFieldForm): class Meta: model = Cluster fields = [ - 'name', 'type', 'group', 'site', 'comments', 'tags', + 'name', 'type', 'group', 'tenant', 'site', 'comments', 'tags', ] widgets = { 'type': APISelect( @@ -128,6 +128,15 @@ class ClusterCSVForm(forms.ModelForm): 'invalid_choice': 'Invalid site name.', } ) + tenant = forms.ModelChoiceField( + queryset=Tenant.objects.all(), + to_field_name='name', + required=False, + help_text='Name of assigned tenant', + error_messages={ + 'invalid_choice': 'Invalid tenant name' + } + ) class Meta: model = Cluster @@ -153,6 +162,10 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit api_url="/api/virtualization/cluster-groups/" ) ) + tenant = forms.ModelChoiceField( + queryset=Tenant.objects.all(), + required=False + ) site = forms.ModelChoiceField( queryset=Site.objects.all(), required=False, @@ -166,7 +179,7 @@ class ClusterBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEdit class Meta: nullable_fields = [ - 'group', 'site', 'comments', + 'group', 'site', 'comments', 'tenant', ] @@ -193,6 +206,15 @@ class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm): null_option=True, ) ) + tenant = FilterChoiceField( + queryset=Tenant.objects.all(), + null_label='-- None --', + required=False, + widget=APISelectMultiple( + api_url="/api/tenancy/tenants/", + null_option=True, + ) + ) site = FilterChoiceField( queryset=Site.objects.all(), to_field_name='slug', diff --git a/netbox/virtualization/migrations/0010_cluster_add_tenant.py b/netbox/virtualization/migrations/0010_cluster_add_tenant.py new file mode 100644 index 000000000..425b32635 --- /dev/null +++ b/netbox/virtualization/migrations/0010_cluster_add_tenant.py @@ -0,0 +1,18 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('tenancy', '0001_initial'), + ('virtualization', '0009_custom_tag_models'), + ] + + operations = [ + migrations.AddField( + model_name='cluster', + name='tenant', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='tenancy.Tenant'), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 6fea769e5..c47f516cf 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -103,6 +103,13 @@ class Cluster(ChangeLoggedModel, CustomFieldModel): blank=True, null=True ) + tenant = models.ForeignKey( + to='tenancy.Tenant', + on_delete=models.PROTECT, + related_name='tenants', + blank=True, + null=True + ) site = models.ForeignKey( to='dcim.Site', on_delete=models.PROTECT, @@ -150,6 +157,7 @@ class Cluster(ChangeLoggedModel, CustomFieldModel): self.type.name, self.group.name if self.group else None, self.site.name if self.site else None, + self.tenant.name if self.tenant else None, self.comments, ) diff --git a/netbox/virtualization/tables.py b/netbox/virtualization/tables.py index 6034dd8dc..ba4554ff5 100644 --- a/netbox/virtualization/tables.py +++ b/netbox/virtualization/tables.py @@ -84,13 +84,14 @@ class ClusterGroupTable(BaseTable): class ClusterTable(BaseTable): pk = ToggleColumn() name = tables.LinkColumn() + tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')]) device_count = tables.Column(accessor=Accessor('devices.count'), orderable=False, verbose_name='Devices') vm_count = tables.Column(accessor=Accessor('virtual_machines.count'), orderable=False, verbose_name='VMs') class Meta(BaseTable.Meta): model = Cluster - fields = ('pk', 'name', 'type', 'group', 'site', 'device_count', 'vm_count') + fields = ('pk', 'name', 'type', 'group', 'tenant', 'site', 'device_count', 'vm_count') # diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 06a39e651..73eccb4b2 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -96,7 +96,7 @@ class ClusterGroupBulkDeleteView(PermissionRequiredMixin, BulkDeleteView): class ClusterListView(PermissionRequiredMixin, ObjectListView): permission_required = 'virtualization.view_cluster' - queryset = Cluster.objects.prefetch_related('type', 'group', 'site') + queryset = Cluster.objects.prefetch_related('type', 'group', 'site', 'tenant') table = tables.ClusterTable filter = filters.ClusterFilter filter_form = forms.ClusterFilterForm