1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #1509: Extended cluster model to allow site assignment

This commit is contained in:
Jeremy Stretch
2017-09-22 12:53:09 -04:00
parent 4cfad2ef3b
commit 2ca161f3d8
9 changed files with 117 additions and 14 deletions

View File

@ -921,6 +921,12 @@ class Device(CreatedUpdatedModel, CustomFieldModel):
'primary_ip6': "The specified IP address ({}) is not assigned to this device.".format(self.primary_ip6),
})
# A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({
'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
})
def save(self, *args, **kwargs):
is_new = not bool(self.pk)

View File

@ -66,6 +66,16 @@
{% endif %}
</td>
</tr>
<tr>
<td>Site</td>
<td>
{% if cluster.site %}
<a href="{{ cluster.site.get_absolute_url }}">{{ cluster.site }}</a>
{% else %}
<span class="text-muted">None</span>
{% endif %}
</td>
</tr>
<tr>
<td>Virtual Machines</td>
<td><a href="{% url 'virtualization:virtualmachine_list' %}?cluster_id={{ cluster.pk }}">{{ cluster.virtual_machines.count }}</a></td>

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
from rest_framework import serializers
from dcim.api.serializers import NestedPlatformSerializer
from dcim.api.serializers import NestedPlatformSerializer, NestedSiteSerializer
from dcim.constants import VIFACE_FF_CHOICES
from dcim.models import Interface
from extras.api.customfields import CustomFieldModelSerializer
@ -57,10 +57,11 @@ class NestedClusterGroupSerializer(serializers.ModelSerializer):
class ClusterSerializer(CustomFieldModelSerializer):
type = NestedClusterTypeSerializer()
group = NestedClusterGroupSerializer()
site = NestedSiteSerializer()
class Meta:
model = Cluster
fields = ['id', 'name', 'type', 'group', 'comments', 'custom_fields']
fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields']
class NestedClusterSerializer(serializers.ModelSerializer):
@ -75,7 +76,7 @@ class WritableClusterSerializer(CustomFieldModelSerializer):
class Meta:
model = Cluster
fields = ['id', 'name', 'type', 'group', 'comments', 'custom_fields']
fields = ['id', 'name', 'type', 'group', 'site', 'comments', 'custom_fields']
#

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
import django_filters
from django.db.models import Q
from dcim.models import Platform
from dcim.models import Platform, Site
from extras.filters import CustomFieldFilterSet
from tenancy.models import Tenant
from utilities.filters import NullableModelMultipleChoiceFilter, NumericInFilter
@ -36,6 +36,16 @@ class ClusterFilter(CustomFieldFilterSet):
to_field_name='slug',
label='Cluster type (slug)',
)
site_id = django_filters.ModelMultipleChoiceFilter(
queryset=Site.objects.all(),
label='Site (ID)',
)
site = django_filters.ModelMultipleChoiceFilter(
name='site__slug',
queryset=Site.objects.all(),
to_field_name='slug',
label='Site (slug)',
)
class Meta:
model = Cluster

View File

@ -3,6 +3,7 @@ from __future__ import unicode_literals
from mptt.forms import TreeNodeChoiceField
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count
from dcim.constants import IFACE_FF_VIRTUAL, VIFACE_FF_CHOICES
@ -53,7 +54,7 @@ class ClusterForm(BootstrapMixin, CustomFieldForm):
class Meta:
model = Cluster
fields = ['name', 'type', 'group', 'comments']
fields = ['name', 'type', 'group', 'site', 'comments']
class ClusterCSVForm(forms.ModelForm):
@ -74,34 +75,50 @@ class ClusterCSVForm(forms.ModelForm):
'invalid_choice': 'Invalid cluster group name.',
}
)
site = forms.ModelChoiceField(
queryset=Site.objects.all(),
to_field_name='name',
required=False,
help_text='Name of assigned site',
error_messages={
'invalid_choice': 'Invalid site name.',
}
)
class Meta:
model = Cluster
fields = ['name', 'type', 'group', 'comments']
fields = ['name', 'type', 'group', 'site', 'comments']
class ClusterBulkEditForm(BootstrapMixin, CustomFieldBulkEditForm):
pk = forms.ModelMultipleChoiceField(queryset=Cluster.objects.all(), widget=forms.MultipleHiddenInput)
type = forms.ModelChoiceField(queryset=ClusterType.objects.all(), required=False)
group = forms.ModelChoiceField(queryset=ClusterGroup.objects.all(), required=False)
site = forms.ModelChoiceField(queryset=Site.objects.all(), required=False)
comments = CommentField(widget=SmallTextarea)
class Meta:
nullable_fields = ['group', 'comments']
nullable_fields = ['group', 'site', 'comments']
class ClusterFilterForm(BootstrapMixin, CustomFieldFilterForm):
model = Cluster
q = forms.CharField(required=False, label='Search')
type = FilterChoiceField(
queryset=ClusterType.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
required=False,
)
group = FilterChoiceField(
queryset=ClusterGroup.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
null_option=(0, 'None'),
required=False,
)
type = FilterChoiceField(
queryset=ClusterType.objects.annotate(filter_count=Count('clusters')),
site = FilterChoiceField(
queryset=Site.objects.annotate(filter_count=Count('clusters')),
to_field_name='slug',
null_option=(0, 'None'),
required=False,
)
@ -153,12 +170,28 @@ class ClusterAddDevicesForm(BootstrapMixin, ChainedFieldsMixin, forms.Form):
class Meta:
fields = ['region', 'site', 'rack', 'devices']
def __init__(self, *args, **kwargs):
def __init__(self, cluster, *args, **kwargs):
self.cluster = cluster
super(ClusterAddDevicesForm, self).__init__(*args, **kwargs)
self.fields['devices'].choices = []
def clean(self):
super(ClusterAddDevicesForm, self).clean()
# If the Cluster is assigned to a Site, all Devices must be assigned to that Site.
if self.cluster.site is not None:
for device in self.cleaned_data.get('devices'):
if device.site != self.cluster.site:
raise ValidationError({
'devices': "{} belongs to a different site ({}) than the cluster ({})".format(
device, device.site, self.cluster.site
)
})
class ClusterRemoveDevicesForm(ConfirmationForm):
pk = forms.ModelMultipleChoiceField(queryset=Device.objects.all(), widget=forms.MultipleHiddenInput)

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-22 16:30
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('dcim', '0044_virtualization'),
('virtualization', '0002_virtualmachine_add_status'),
]
operations = [
migrations.AddField(
model_name='cluster',
name='site',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='clusters', to='dcim.Site'),
),
]

View File

@ -1,10 +1,12 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from dcim.models import Device
from extras.models import CustomFieldModel, CustomFieldValue
from utilities.models import CreatedUpdatedModel
from utilities.utils import csv_format
@ -90,6 +92,13 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
blank=True,
null=True
)
site = models.ForeignKey(
to='dcim.Site',
on_delete=models.PROTECT,
related_name='clusters',
blank=True,
null=True
)
comments = models.TextField(
blank=True
)
@ -100,7 +109,7 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
)
csv_headers = [
'name', 'type', 'group', 'comments',
'name', 'type', 'group', 'site', 'comments',
]
class Meta:
@ -112,6 +121,18 @@ class Cluster(CreatedUpdatedModel, CustomFieldModel):
def get_absolute_url(self):
return reverse('virtualization:cluster', args=[self.pk])
def clean(self):
# If the Cluster is assigned to a Site, verify that all host Devices belong to that Site.
if self.pk and self.site:
nonsite_devices = Device.objects.filter(cluster=self).exclude(site=self.site).count()
if nonsite_devices:
raise ValidationError({
'site': "{} devices are assigned as hosts for this cluster but are not in site {}".format(
nonsite_devices, self.site
)
})
def to_csv(self):
return csv_format([
self.name,

View File

@ -79,7 +79,7 @@ class ClusterTable(BaseTable):
class Meta(BaseTable.Meta):
model = Cluster
fields = ('pk', 'name', 'type', 'group', 'device_count', 'vm_count')
fields = ('pk', 'name', 'type', 'group', 'site', 'device_count', 'vm_count')
#

View File

@ -166,7 +166,7 @@ class ClusterAddDevicesView(PermissionRequiredMixin, View):
def get(self, request, pk):
cluster = get_object_or_404(Cluster, pk=pk)
form = self.form()
form = self.form(cluster)
return render(request, self.template_name, {
'cluster': cluster,
@ -177,7 +177,7 @@ class ClusterAddDevicesView(PermissionRequiredMixin, View):
def post(self, request, pk):
cluster = get_object_or_404(Cluster, pk=pk)
form = self.form(request.POST)
form = self.form(cluster, request.POST)
if form.is_valid():