netbox/netbox/virtualization/models/clusters.py
Arthur Hanson 6dc75d8db1
7699 Add Scope to Cluster (#17848)
* 7699 Add Scope to Cluster

* 7699 Serializer

* 7699 filterset

* 7699 bulk_edit

* 7699 bulk_import

* 7699 model_form

* 7699 graphql, tables

* 7699 fixes

* 7699 fixes

* 7699 fixes

* 7699 fixes

* 7699 fix tests

* 7699 fix graphql tests for clusters reference

* 7699 fix dcim tests

* 7699 fix ipam tests

* 7699 fix tests

* 7699 use mixin for model

* 7699 change mixin name

* 7699 scope form

* 7699 scope form

* 7699 scoped form, fitlerset

* 7699 review changes

* 7699 move ScopedFilterset

* 7699 move CachedScopeMixin

* 7699 review changes

* 7699 review changes

* 7699 refactor mixins

* 7699 _sitegroup -> _site_group

* 7699 update docstring

* Misc cleanup

* Update migrations

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
2024-11-01 14:18:23 -04:00

146 lines
4.4 KiB
Python

from django.apps import apps
from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from dcim.models import Device
from dcim.models.mixins import CachedScopeMixin
from netbox.models import OrganizationalModel, PrimaryModel
from netbox.models.features import ContactsMixin
from virtualization.choices import *
__all__ = (
'Cluster',
'ClusterGroup',
'ClusterType',
)
class ClusterType(OrganizationalModel):
"""
A type of Cluster.
"""
class Meta:
ordering = ('name',)
verbose_name = _('cluster type')
verbose_name_plural = _('cluster types')
class ClusterGroup(ContactsMixin, OrganizationalModel):
"""
An organizational group of Clusters.
"""
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='cluster_group'
)
class Meta:
ordering = ('name',)
verbose_name = _('cluster group')
verbose_name_plural = _('cluster groups')
class Cluster(ContactsMixin, CachedScopeMixin, PrimaryModel):
"""
A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
)
type = models.ForeignKey(
verbose_name=_('type'),
to=ClusterType,
on_delete=models.PROTECT,
related_name='clusters'
)
group = models.ForeignKey(
to=ClusterGroup,
on_delete=models.PROTECT,
related_name='clusters',
blank=True,
null=True
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=ClusterStatusChoices,
default=ClusterStatusChoices.STATUS_ACTIVE
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='clusters',
blank=True,
null=True
)
# Generic relations
vlan_groups = GenericRelation(
to='ipam.VLANGroup',
content_type_field='scope_type',
object_id_field='scope_id',
related_query_name='cluster'
)
clone_fields = (
'scope_type', 'scope_id', 'type', 'group', 'status', 'tenant',
)
prerequisite_models = (
'virtualization.ClusterType',
)
class Meta:
ordering = ['name']
constraints = (
models.UniqueConstraint(
fields=('group', 'name'),
name='%(app_label)s_%(class)s_unique_group_name'
),
models.UniqueConstraint(
fields=('_site', 'name'),
name='%(app_label)s_%(class)s_unique__site_name'
),
)
verbose_name = _('cluster')
verbose_name_plural = _('clusters')
def __str__(self):
return self.name
def get_status_color(self):
return ClusterStatusChoices.colors.get(self.status)
def clean(self):
super().clean()
site = location = None
if self.scope_type:
scope_type = self.scope_type.model_class()
if scope_type == apps.get_model('dcim', 'site'):
site = self.scope
elif scope_type == apps.get_model('dcim', 'location'):
location = self.scope
site = location.site
# If the Cluster is assigned to a Site, verify that all host Devices belong to that Site.
if not self._state.adding:
if site:
if nonsite_devices := Device.objects.filter(cluster=self).exclude(site=site).count():
raise ValidationError({
'scope': _(
"{count} devices are assigned as hosts for this cluster but are not in site {site}"
).format(count=nonsite_devices, site=site)
})
if location:
if nonlocation_devices := Device.objects.filter(cluster=self).exclude(location=location).count():
raise ValidationError({
'scope': _(
"{count} devices are assigned as hosts for this cluster but are not in location {location}"
).format(count=nonlocation_devices, location=location)
})