Skip to content

Commit

Permalink
Closes #9582: Enable assigning config contexts based on device location
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremystretch committed Jun 22, 2022
1 parent 3416156 commit 379880c
Show file tree
Hide file tree
Showing 15 changed files with 138 additions and 86 deletions.
2 changes: 2 additions & 0 deletions docs/models/extras/configcontext.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ Sometimes it is desirable to associate additional data with a group of devices o
* Region
* Site group
* Site
* Location (devices only)
* Device type (devices only)
* Role
* Platform
* Cluster type (VMs only)
* Cluster group (VMs only)
* Cluster (VMs only)
* Tenant group
Expand Down
3 changes: 3 additions & 0 deletions docs/release-notes/version-3.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
* [#9166](https://github.com/netbox-community/netbox/issues/9166) - Add UI visibility toggle for custom fields
* [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location

### Other Changes

Expand All @@ -45,6 +46,8 @@
* Added required `status` field (default value: `active`)
* dcim.Rack
* The `elevation` endpoint now includes half-height rack units, and utilizes decimal values for the ID and name of each unit
* extras.ConfigContext
* Added the `locations` many-to-many field to track the assignment of ConfigContexts to Locations
* extras.CustomField
* Added `group_name` and `ui_visibility` fields
* ipam.IPAddress
Expand Down
16 changes: 11 additions & 5 deletions netbox/extras/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from rest_framework import serializers

from dcim.api.nested_serializers import (
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedRegionSerializer,
NestedSiteSerializer, NestedSiteGroupSerializer,
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
)
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
from extras.utils import FeatureQuery
Expand Down Expand Up @@ -272,6 +272,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
required=False,
many=True
)
locations = SerializedPKRelatedField(
queryset=Location.objects.all(),
serializer=NestedLocationSerializer,
required=False,
many=True
)
device_types = SerializedPKRelatedField(
queryset=DeviceType.objects.all(),
serializer=NestedDeviceTypeSerializer,
Expand Down Expand Up @@ -331,8 +337,8 @@ class Meta:
model = ConfigContext
fields = [
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
'tenants', 'tags', 'data', 'created', 'last_updated',
'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
'tenant_groups', 'tenants', 'tags', 'data', 'created', 'last_updated',
]


Expand Down
2 changes: 1 addition & 1 deletion netbox/extras/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):

class ConfigContextViewSet(NetBoxModelViewSet):
queryset = ConfigContext.objects.prefetch_related(
'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants',
)
serializer_class = serializers.ConfigContextSerializer
filterset_class = filtersets.ConfigContextFilterSet
Expand Down
13 changes: 12 additions & 1 deletion netbox/extras/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q

from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
from tenancy.models import Tenant, TenantGroup
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
Expand Down Expand Up @@ -255,6 +255,17 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
to_field_name='slug',
label='Site (slug)',
)
location_id = django_filters.ModelMultipleChoiceFilter(
field_name='locations',
queryset=Location.objects.all(),
label='Location',
)
location = django_filters.ModelMultipleChoiceFilter(
field_name='locations__slug',
queryset=Location.objects.all(),
to_field_name='slug',
label='Location (slug)',
)
device_type_id = django_filters.ModelMultipleChoiceFilter(
field_name='device_types',
queryset=DeviceType.objects.all(),
Expand Down
9 changes: 7 additions & 2 deletions netbox/extras/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _

from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
from extras.utils import FeatureQuery
Expand Down Expand Up @@ -170,7 +170,7 @@ class TagFilterForm(FilterForm):
class ConfigContextFilterForm(FilterForm):
fieldsets = (
(None, ('q', 'tag_id')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Device', ('device_type_id', 'platform_id', 'role_id')),
('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
('Tenant', ('tenant_group_id', 'tenant_id'))
Expand All @@ -190,6 +190,11 @@ class ConfigContextFilterForm(FilterForm):
required=False,
label=_('Sites')
)
location_id = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False,
label=_('Locations')
)
device_type_id = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
required=False,
Expand Down
21 changes: 16 additions & 5 deletions netbox/extras/forms/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django import forms
from django.contrib.contenttypes.models import ContentType

from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import *
from extras.models import *
from extras.utils import FeatureQuery
Expand Down Expand Up @@ -166,6 +166,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
queryset=Site.objects.all(),
required=False
)
locations = DynamicModelMultipleChoiceField(
queryset=Location.objects.all(),
required=False
)
device_types = DynamicModelMultipleChoiceField(
queryset=DeviceType.objects.all(),
required=False
Expand Down Expand Up @@ -202,15 +206,22 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
queryset=Tag.objects.all(),
required=False
)
data = JSONField(
label=''
data = JSONField()

fieldsets = (
('Config Context', ('name', 'weight', 'description', 'data', 'is_active')),
('Assignment', (
'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
)),
)

class Meta:
model = ConfigContext
fields = (
'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
'name', 'weight', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations',
'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
'tenants', 'tags',
)


Expand Down
19 changes: 19 additions & 0 deletions netbox/extras/migrations/0076_configcontext_locations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 4.0.5 on 2022-06-22 19:13

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('dcim', '0156_location_status'),
('extras', '0075_customfield_ui_visibility'),
]

operations = [
migrations.AddField(
model_name='configcontext',
name='locations',
field=models.ManyToManyField(blank=True, related_name='+', to='dcim.location'),
),
]
12 changes: 7 additions & 5 deletions netbox/extras/models/configcontexts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from collections import OrderedDict

from django.core.validators import ValidationError
from django.db import models
from django.urls import reverse
Expand Down Expand Up @@ -55,6 +53,11 @@ class ConfigContext(WebhooksMixin, ChangeLoggedModel):
related_name='+',
blank=True
)
locations = models.ManyToManyField(
to='dcim.Location',
related_name='+',
blank=True
)
device_types = models.ManyToManyField(
to='dcim.DeviceType',
related_name='+',
Expand Down Expand Up @@ -138,11 +141,10 @@ class Meta:

def get_config_context(self):
"""
Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs.
Return the rendered configuration context for a device or VM.
"""

# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
data = OrderedDict()
data = {}

if not hasattr(self, 'config_context_data'):
# The annotation is not available, so we fall back to manually querying for the config context objects
Expand Down
5 changes: 4 additions & 1 deletion netbox/extras/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ def get_for_object(self, obj, aggregate_data=False):
# `device_role` for Device; `role` for VirtualMachine
role = getattr(obj, 'device_role', None) or obj.role

# Device type assignment is relevant only for Devices
# Device type and location assignment is relevant only for Devices
device_type = getattr(obj, 'device_type', None)
location = getattr(obj, 'location', None)

# Get assigned cluster, group, and type (if any)
cluster = getattr(obj, 'cluster', None)
Expand All @@ -42,6 +43,7 @@ def get_for_object(self, obj, aggregate_data=False):
Q(regions__in=regions) | Q(regions=None),
Q(site_groups__in=sitegroups) | Q(site_groups=None),
Q(sites=obj.site) | Q(sites=None),
Q(locations=location) | Q(locations=None),
Q(device_types=device_type) | Q(device_types=None),
Q(roles=role) | Q(roles=None),
Q(platforms=obj.platform) | Q(platforms=None),
Expand Down Expand Up @@ -114,6 +116,7 @@ def _get_config_context_filters(self):
)

if self.model._meta.model_name == 'device':
base_query.add((Q(locations=OuterRef('location')) | Q(locations=None)), Q.AND)
base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)
Expand Down
5 changes: 3 additions & 2 deletions netbox/extras/tables/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,9 @@ class ConfigContextTable(NetBoxTable):
class Meta(NetBoxTable.Meta):
model = ConfigContext
fields = (
'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'roles', 'platforms',
'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created', 'last_updated',
'pk', 'id', 'name', 'weight', 'is_active', 'description', 'regions', 'sites', 'locations', 'roles',
'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'created',
'last_updated',
)
default_columns = ('pk', 'name', 'weight', 'is_active', 'description')

Expand Down
30 changes: 23 additions & 7 deletions netbox/extras/tests/test_filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from django.test import TestCase

from circuits.models import Provider
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
from dcim.models import DeviceRole, DeviceType, Location, Manufacturer, Platform, Rack, Region, Site, SiteGroup
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
from extras.filtersets import *
from extras.models import *
Expand Down Expand Up @@ -368,9 +368,9 @@ class ConfigContextTestCase(TestCase, ChangeLoggedFilterSetTests):
def setUpTestData(cls):

regions = (
Region(name='Test Region 1', slug='test-region-1'),
Region(name='Test Region 2', slug='test-region-2'),
Region(name='Test Region 3', slug='test-region-3'),
Region(name='Region 1', slug='region-1'),
Region(name='Region 2', slug='region-2'),
Region(name='Region 3', slug='region-3'),
)
for r in regions:
r.save()
Expand All @@ -384,12 +384,20 @@ def setUpTestData(cls):
site_group.save()

sites = (
Site(name='Test Site 1', slug='test-site-1'),
Site(name='Test Site 2', slug='test-site-2'),
Site(name='Test Site 3', slug='test-site-3'),
Site(name='Site 1', slug='site-1'),
Site(name='Site 2', slug='site-2'),
Site(name='Site 3', slug='site-3'),
)
Site.objects.bulk_create(sites)

locations = (
Location(name='Location 1', slug='location-1', site=sites[0]),
Location(name='Location 2', slug='location-2', site=sites[1]),
Location(name='Location 3', slug='location-3', site=sites[2]),
)
for location in locations:
location.save()

manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
device_types = (
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
Expand Down Expand Up @@ -460,6 +468,7 @@ def setUpTestData(cls):
c.regions.set([regions[i]])
c.site_groups.set([site_groups[i]])
c.sites.set([sites[i]])
c.locations.set([locations[i]])
c.device_types.set([device_types[i]])
c.roles.set([device_roles[i]])
c.platforms.set([platforms[i]])
Expand Down Expand Up @@ -501,6 +510,13 @@ def test_site(self):
params = {'site': [sites[0].slug, sites[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

def test_location(self):
locations = Location.objects.all()[:2]
params = {'location_id': [locations[0].pk, locations[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'location': [locations[0].slug, locations[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)

def test_device_type(self):
device_types = DeviceType.objects.all()[:2]
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
Expand Down
Loading

0 comments on commit 379880c

Please sign in to comment.