mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-31 04:46:26 -06:00
7699 Add Scope to Cluster
This commit is contained in:
parent
ef1fdf0a01
commit
2bb49d7db6
4
netbox/virtualization/constants.py
Normal file
4
netbox/virtualization/constants.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# models values for ContentTypes which may be CircuitTermination scope types
|
||||||
|
CLUSTER_SCOPE_TYPES = (
|
||||||
|
'region', 'sitegroup', 'site', 'location',
|
||||||
|
)
|
@ -38,42 +38,42 @@ class ClusterGroupFilterSet(OrganizationalModelFilterSet, ContactModelFilterSet)
|
|||||||
|
|
||||||
|
|
||||||
class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
class ClusterFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilterSet):
|
||||||
region_id = TreeNodeMultipleChoiceFilter(
|
# region_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
# queryset=Region.objects.all(),
|
||||||
field_name='site__region',
|
# field_name='site__region',
|
||||||
lookup_expr='in',
|
# lookup_expr='in',
|
||||||
label=_('Region (ID)'),
|
# label=_('Region (ID)'),
|
||||||
)
|
# )
|
||||||
region = TreeNodeMultipleChoiceFilter(
|
# region = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=Region.objects.all(),
|
# queryset=Region.objects.all(),
|
||||||
field_name='site__region',
|
# field_name='site__region',
|
||||||
lookup_expr='in',
|
# lookup_expr='in',
|
||||||
to_field_name='slug',
|
# to_field_name='slug',
|
||||||
label=_('Region (slug)'),
|
# label=_('Region (slug)'),
|
||||||
)
|
# )
|
||||||
site_group_id = TreeNodeMultipleChoiceFilter(
|
# site_group_id = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
# queryset=SiteGroup.objects.all(),
|
||||||
field_name='site__group',
|
# field_name='site__group',
|
||||||
lookup_expr='in',
|
# lookup_expr='in',
|
||||||
label=_('Site group (ID)'),
|
# label=_('Site group (ID)'),
|
||||||
)
|
# )
|
||||||
site_group = TreeNodeMultipleChoiceFilter(
|
# site_group = TreeNodeMultipleChoiceFilter(
|
||||||
queryset=SiteGroup.objects.all(),
|
# queryset=SiteGroup.objects.all(),
|
||||||
field_name='site__group',
|
# field_name='site__group',
|
||||||
lookup_expr='in',
|
# lookup_expr='in',
|
||||||
to_field_name='slug',
|
# to_field_name='slug',
|
||||||
label=_('Site group (slug)'),
|
# label=_('Site group (slug)'),
|
||||||
)
|
# )
|
||||||
site_id = django_filters.ModelMultipleChoiceFilter(
|
# site_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Site.objects.all(),
|
# queryset=Site.objects.all(),
|
||||||
label=_('Site (ID)'),
|
# label=_('Site (ID)'),
|
||||||
)
|
# )
|
||||||
site = django_filters.ModelMultipleChoiceFilter(
|
# site = django_filters.ModelMultipleChoiceFilter(
|
||||||
field_name='site__slug',
|
# field_name='site__slug',
|
||||||
queryset=Site.objects.all(),
|
# queryset=Site.objects.all(),
|
||||||
to_field_name='slug',
|
# to_field_name='slug',
|
||||||
label=_('Site (slug)'),
|
# label=_('Site (slug)'),
|
||||||
)
|
# )
|
||||||
group_id = django_filters.ModelMultipleChoiceFilter(
|
group_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=ClusterGroup.objects.all(),
|
queryset=ClusterGroup.objects.all(),
|
||||||
label=_('Parent group (ID)'),
|
label=_('Parent group (ID)'),
|
||||||
|
51
netbox/virtualization/migrations/0042_cluster_scope.py
Normal file
51
netbox/virtualization/migrations/0042_cluster_scope.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def copy_site_assignments(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Copy site ForeignKey values to the scope GFK.
|
||||||
|
"""
|
||||||
|
ContentType = apps.get_model('contenttypes', 'ContentType')
|
||||||
|
Cluster = apps.get_model('virtualization', 'Cluster')
|
||||||
|
Site = apps.get_model('dcim', 'Site')
|
||||||
|
|
||||||
|
Cluster.objects.filter(site__isnull=False).update(
|
||||||
|
scope_type=ContentType.objects.get_for_model(Site),
|
||||||
|
scope_id=models.F('site_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
('virtualization', '0041_charfield_null_choices'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='scope_id',
|
||||||
|
field=models.PositiveBigIntegerField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='scope_type',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
limit_choices_to=models.Q(('model__in', ('region', 'sitegroup', 'site', 'location'))),
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.PROTECT,
|
||||||
|
related_name='+',
|
||||||
|
to='contenttypes.contenttype',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Copy over existing site assignments
|
||||||
|
migrations.RunPython(
|
||||||
|
code=copy_site_assignments,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
|
||||||
|
]
|
@ -0,0 +1,85 @@
|
|||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_denormalized_fields(apps, schema_editor):
|
||||||
|
"""
|
||||||
|
Copy site ForeignKey values to the scope GFK.
|
||||||
|
"""
|
||||||
|
Cluster = apps.get_model('virtualization', 'Cluster')
|
||||||
|
|
||||||
|
clusters = Cluster.objects.filter(site__isnull=False).prefetch_related('site')
|
||||||
|
for cluster in clusters:
|
||||||
|
cluster._region_id = cluster.site.region_id
|
||||||
|
cluster._sitegroup_id = cluster.site.group_id
|
||||||
|
cluster._site_id = cluster.site_id
|
||||||
|
# Note: Location cannot be set prior to migration
|
||||||
|
|
||||||
|
Cluster.objects.bulk_update(clusters, ['_region', '_sitegroup', '_site'])
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('virtualization', '0042_cluster_scope'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='_location',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='_clusters',
|
||||||
|
to='dcim.location',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='_region',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='_clusters',
|
||||||
|
to='dcim.region',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='_site',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='_clusters',
|
||||||
|
to='dcim.site',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='_sitegroup',
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='_clusters',
|
||||||
|
to='dcim.sitegroup',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
# Populate denormalized FK values
|
||||||
|
migrations.RunPython(
|
||||||
|
code=populate_denormalized_fields,
|
||||||
|
reverse_code=migrations.RunPython.noop
|
||||||
|
),
|
||||||
|
|
||||||
|
# Delete the site ForeignKey
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='cluster',
|
||||||
|
name='site',
|
||||||
|
),
|
||||||
|
|
||||||
|
]
|
@ -1,4 +1,5 @@
|
|||||||
from django.contrib.contenttypes.fields import GenericRelation
|
from django.apps import apps
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -7,6 +8,7 @@ from dcim.models import Device
|
|||||||
from netbox.models import OrganizationalModel, PrimaryModel
|
from netbox.models import OrganizationalModel, PrimaryModel
|
||||||
from netbox.models.features import ContactsMixin
|
from netbox.models.features import ContactsMixin
|
||||||
from virtualization.choices import *
|
from virtualization.choices import *
|
||||||
|
from virtualization.constants import CLUSTER_SCOPE_TYPES
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'Cluster',
|
'Cluster',
|
||||||
@ -76,13 +78,22 @@ class Cluster(ContactsMixin, PrimaryModel):
|
|||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
site = models.ForeignKey(
|
scope_type = models.ForeignKey(
|
||||||
to='dcim.Site',
|
to='contenttypes.ContentType',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='clusters',
|
limit_choices_to=models.Q(model__in=CLUSTER_SCOPE_TYPES),
|
||||||
|
related_name='+',
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True
|
null=True
|
||||||
)
|
)
|
||||||
|
scope_id = models.PositiveBigIntegerField(
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
scope = GenericForeignKey(
|
||||||
|
ct_field='scope_type',
|
||||||
|
fk_field='scope_id'
|
||||||
|
)
|
||||||
|
|
||||||
# Generic relations
|
# Generic relations
|
||||||
vlan_groups = GenericRelation(
|
vlan_groups = GenericRelation(
|
||||||
@ -92,8 +103,38 @@ class Cluster(ContactsMixin, PrimaryModel):
|
|||||||
related_query_name='cluster'
|
related_query_name='cluster'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Cached associations to enable efficient filtering
|
||||||
|
_location = models.ForeignKey(
|
||||||
|
to='dcim.Location',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='_clusters',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_site = models.ForeignKey(
|
||||||
|
to='dcim.Site',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='_clusters',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_region = models.ForeignKey(
|
||||||
|
to='dcim.Region',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='_clusters',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
_sitegroup = models.ForeignKey(
|
||||||
|
to='dcim.SiteGroup',
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name='_clusters',
|
||||||
|
blank=True,
|
||||||
|
null=True
|
||||||
|
)
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'type', 'group', 'status', 'tenant', 'site',
|
'scope_type', 'scope_id', 'type', 'group', 'status', 'tenant',
|
||||||
)
|
)
|
||||||
prerequisite_models = (
|
prerequisite_models = (
|
||||||
'virtualization.ClusterType',
|
'virtualization.ClusterType',
|
||||||
@ -131,3 +172,28 @@ class Cluster(ContactsMixin, PrimaryModel):
|
|||||||
"{count} devices are assigned as hosts for this cluster but are not in site {site}"
|
"{count} devices are assigned as hosts for this cluster but are not in site {site}"
|
||||||
).format(count=nonsite_devices, site=self.site)
|
).format(count=nonsite_devices, site=self.site)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
# Cache objects associated with the terminating object (for filtering)
|
||||||
|
self.cache_related_objects()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
|
def cache_related_objects(self):
|
||||||
|
self._region = self._sitegroup = self._site = self._location = None
|
||||||
|
if self.scope_type:
|
||||||
|
scope_type = self.scope_type.model_class()
|
||||||
|
if scope_type == apps.get_model('dcim', 'region'):
|
||||||
|
self._region = self.scope
|
||||||
|
elif scope_type == apps.get_model('dcim', 'sitegroup'):
|
||||||
|
self._sitegroup = self.scope
|
||||||
|
elif scope_type == apps.get_model('dcim', 'site'):
|
||||||
|
self._region = self.scope.region
|
||||||
|
self._sitegroup = self.scope.group
|
||||||
|
self._site = self.scope
|
||||||
|
elif scope_type == apps.get_model('dcim', 'location'):
|
||||||
|
self._region = self.scope.site.region
|
||||||
|
self._sitegroup = self.scope.site.group
|
||||||
|
self._site = self.scope.site
|
||||||
|
self._location = self.scope
|
||||||
|
cache_related_objects.alters_data = True
|
||||||
|
Loading…
Reference in New Issue
Block a user