diff --git a/netbox/virtualization/models/__init__.py b/netbox/virtualization/models/__init__.py new file mode 100644 index 000000000..dc1e7eb20 --- /dev/null +++ b/netbox/virtualization/models/__init__.py @@ -0,0 +1,2 @@ +from .clusters import * +from .virtualmachines import * diff --git a/netbox/virtualization/models/clusters.py b/netbox/virtualization/models/clusters.py new file mode 100644 index 000000000..e7c1294c2 --- /dev/null +++ b/netbox/virtualization/models/clusters.py @@ -0,0 +1,135 @@ +from django.contrib.contenttypes.fields import GenericRelation +from django.core.exceptions import ValidationError +from django.db import models +from django.urls import reverse + +from dcim.models import Device +from netbox.models import OrganizationalModel, PrimaryModel +from virtualization.choices import * + +__all__ = ( + 'Cluster', + 'ClusterGroup', + 'ClusterType', +) + + +class ClusterType(OrganizationalModel): + """ + A type of Cluster. + """ + def get_absolute_url(self): + return reverse('virtualization:clustertype', args=[self.pk]) + + +class ClusterGroup(OrganizationalModel): + """ + An organizational group of Clusters. + """ + # Generic relations + vlan_groups = GenericRelation( + to='ipam.VLANGroup', + content_type_field='scope_type', + object_id_field='scope_id', + related_query_name='cluster_group' + ) + contacts = GenericRelation( + to='tenancy.ContactAssignment' + ) + + def get_absolute_url(self): + return reverse('virtualization:clustergroup', args=[self.pk]) + + +class Cluster(PrimaryModel): + """ + A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices. + """ + name = models.CharField( + max_length=100 + ) + type = models.ForeignKey( + 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( + 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 + ) + site = models.ForeignKey( + to='dcim.Site', + 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' + ) + contacts = GenericRelation( + to='tenancy.ContactAssignment' + ) + + clone_fields = ( + 'type', 'group', 'status', 'tenant', 'site', + ) + + 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' + ), + ) + + def __str__(self): + return self.name + + @classmethod + def get_prerequisite_models(cls): + return [ClusterType, ] + + def get_absolute_url(self): + return reverse('virtualization:cluster', args=[self.pk]) + + def get_status_color(self): + return ClusterStatusChoices.colors.get(self.status) + + def clean(self): + super().clean() + + # 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 + ) + }) diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models/virtualmachines.py similarity index 75% rename from netbox/virtualization/models.py rename to netbox/virtualization/models/virtualmachines.py index b5129d581..d64289eb2 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -6,162 +6,23 @@ from django.db.models import Q from django.db.models.functions import Lower from django.urls import reverse -from dcim.models import BaseInterface, Device +from dcim.models import BaseInterface from extras.models import ConfigContextModel from extras.querysets import ConfigContextModelQuerySet from netbox.config import get_config -from netbox.models import NetBoxModel, OrganizationalModel, PrimaryModel +from netbox.models import NetBoxModel, PrimaryModel from utilities.fields import NaturalOrderingField from utilities.ordering import naturalize_interface from utilities.query_functions import CollateAsChar -from .choices import * +from virtualization.choices import * +from .clusters import Cluster __all__ = ( - 'Cluster', - 'ClusterGroup', - 'ClusterType', 'VirtualMachine', 'VMInterface', ) -# -# Cluster types -# - -class ClusterType(OrganizationalModel): - """ - A type of Cluster. - """ - def get_absolute_url(self): - return reverse('virtualization:clustertype', args=[self.pk]) - - -# -# Cluster groups -# - -class ClusterGroup(OrganizationalModel): - """ - An organizational group of Clusters. - """ - # Generic relations - vlan_groups = GenericRelation( - to='ipam.VLANGroup', - content_type_field='scope_type', - object_id_field='scope_id', - related_query_name='cluster_group' - ) - contacts = GenericRelation( - to='tenancy.ContactAssignment' - ) - - def get_absolute_url(self): - return reverse('virtualization:clustergroup', args=[self.pk]) - - -# -# Clusters -# - -class Cluster(PrimaryModel): - """ - A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices. - """ - name = models.CharField( - max_length=100 - ) - type = models.ForeignKey( - 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( - 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 - ) - site = models.ForeignKey( - to='dcim.Site', - 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' - ) - contacts = GenericRelation( - to='tenancy.ContactAssignment' - ) - - clone_fields = ( - 'type', 'group', 'status', 'tenant', 'site', - ) - - 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' - ), - ) - - def __str__(self): - return self.name - - @classmethod - def get_prerequisite_models(cls): - return [ClusterType, ] - - def get_absolute_url(self): - return reverse('virtualization:cluster', args=[self.pk]) - - def get_status_color(self): - return ClusterStatusChoices.colors.get(self.status) - - def clean(self): - super().clean() - - # 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 - ) - }) - - -# -# Virtual machines -# - class VirtualMachine(PrimaryModel, ConfigContextModel): """ A virtual machine which runs inside a Cluster. @@ -357,10 +218,6 @@ class VirtualMachine(PrimaryModel, ConfigContextModel): return None -# -# Interfaces -# - class VMInterface(NetBoxModel, BaseInterface): virtual_machine = models.ForeignKey( to='virtualization.VirtualMachine',