Skip to content

Commit 379880c

Browse files
committed
Closes #9582: Enable assigning config contexts based on device location
1 parent 3416156 commit 379880c

File tree

15 files changed

+138
-86
lines changed

15 files changed

+138
-86
lines changed

docs/models/extras/configcontext.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ Sometimes it is desirable to associate additional data with a group of devices o
55
* Region
66
* Site group
77
* Site
8+
* Location (devices only)
89
* Device type (devices only)
910
* Role
1011
* Platform
12+
* Cluster type (VMs only)
1113
* Cluster group (VMs only)
1214
* Cluster (VMs only)
1315
* Tenant group

docs/release-notes/version-3.3.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
* [#8495](https://github.com/netbox-community/netbox/issues/8495) - Enable custom field grouping
2626
* [#8995](https://github.com/netbox-community/netbox/issues/8995) - Enable arbitrary ordering of REST API results
2727
* [#9166](https://github.com/netbox-community/netbox/issues/9166) - Add UI visibility toggle for custom fields
28+
* [#9582](https://github.com/netbox-community/netbox/issues/9582) - Enable assigning config contexts based on device location
2829

2930
### Other Changes
3031

@@ -45,6 +46,8 @@
4546
* Added required `status` field (default value: `active`)
4647
* dcim.Rack
4748
* The `elevation` endpoint now includes half-height rack units, and utilizes decimal values for the ID and name of each unit
49+
* extras.ConfigContext
50+
* Added the `locations` many-to-many field to track the assignment of ConfigContexts to Locations
4851
* extras.CustomField
4952
* Added `group_name` and `ui_visibility` fields
5053
* ipam.IPAddress

netbox/extras/api/serializers.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
from rest_framework import serializers
66

77
from dcim.api.nested_serializers import (
8-
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer, NestedRegionSerializer,
9-
NestedSiteSerializer, NestedSiteGroupSerializer,
8+
NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedLocationSerializer, NestedPlatformSerializer,
9+
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
1010
)
11-
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
11+
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
1212
from extras.choices import *
1313
from extras.models import *
1414
from extras.utils import FeatureQuery
@@ -272,6 +272,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
272272
required=False,
273273
many=True
274274
)
275+
locations = SerializedPKRelatedField(
276+
queryset=Location.objects.all(),
277+
serializer=NestedLocationSerializer,
278+
required=False,
279+
many=True
280+
)
275281
device_types = SerializedPKRelatedField(
276282
queryset=DeviceType.objects.all(),
277283
serializer=NestedDeviceTypeSerializer,
@@ -331,8 +337,8 @@ class Meta:
331337
model = ConfigContext
332338
fields = [
333339
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
334-
'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
335-
'tenants', 'tags', 'data', 'created', 'last_updated',
340+
'locations', 'device_types', 'roles', 'platforms', 'cluster_types', 'cluster_groups', 'clusters',
341+
'tenant_groups', 'tenants', 'tags', 'data', 'created', 'last_updated',
336342
]
337343

338344

netbox/extras/api/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class JournalEntryViewSet(NetBoxModelViewSet):
138138

139139
class ConfigContextViewSet(NetBoxModelViewSet):
140140
queryset = ConfigContext.objects.prefetch_related(
141-
'regions', 'site_groups', 'sites', 'roles', 'platforms', 'tenant_groups', 'tenants',
141+
'regions', 'site_groups', 'sites', 'locations', 'roles', 'platforms', 'tenant_groups', 'tenants',
142142
)
143143
serializer_class = serializers.ConfigContextSerializer
144144
filterset_class = filtersets.ConfigContextFilterSet

netbox/extras/filtersets.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.contrib.contenttypes.models import ContentType
44
from django.db.models import Q
55

6-
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
6+
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
77
from netbox.filtersets import BaseFilterSet, ChangeLoggedModelFilterSet, NetBoxModelFilterSet
88
from tenancy.models import Tenant, TenantGroup
99
from utilities.filters import ContentTypeFilter, MultiValueCharFilter, MultiValueNumberFilter
@@ -255,6 +255,17 @@ class ConfigContextFilterSet(ChangeLoggedModelFilterSet):
255255
to_field_name='slug',
256256
label='Site (slug)',
257257
)
258+
location_id = django_filters.ModelMultipleChoiceFilter(
259+
field_name='locations',
260+
queryset=Location.objects.all(),
261+
label='Location',
262+
)
263+
location = django_filters.ModelMultipleChoiceFilter(
264+
field_name='locations__slug',
265+
queryset=Location.objects.all(),
266+
to_field_name='slug',
267+
label='Location (slug)',
268+
)
258269
device_type_id = django_filters.ModelMultipleChoiceFilter(
259270
field_name='device_types',
260271
queryset=DeviceType.objects.all(),

netbox/extras/forms/filtersets.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from django.contrib.contenttypes.models import ContentType
44
from django.utils.translation import gettext as _
55

6-
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
6+
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
77
from extras.choices import *
88
from extras.models import *
99
from extras.utils import FeatureQuery
@@ -170,7 +170,7 @@ class TagFilterForm(FilterForm):
170170
class ConfigContextFilterForm(FilterForm):
171171
fieldsets = (
172172
(None, ('q', 'tag_id')),
173-
('Location', ('region_id', 'site_group_id', 'site_id')),
173+
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
174174
('Device', ('device_type_id', 'platform_id', 'role_id')),
175175
('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
176176
('Tenant', ('tenant_group_id', 'tenant_id'))
@@ -190,6 +190,11 @@ class ConfigContextFilterForm(FilterForm):
190190
required=False,
191191
label=_('Sites')
192192
)
193+
location_id = DynamicModelMultipleChoiceField(
194+
queryset=Location.objects.all(),
195+
required=False,
196+
label=_('Locations')
197+
)
193198
device_type_id = DynamicModelMultipleChoiceField(
194199
queryset=DeviceType.objects.all(),
195200
required=False,

netbox/extras/forms/models.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django import forms
22
from django.contrib.contenttypes.models import ContentType
33

4-
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
4+
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
55
from extras.choices import *
66
from extras.models import *
77
from extras.utils import FeatureQuery
@@ -166,6 +166,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
166166
queryset=Site.objects.all(),
167167
required=False
168168
)
169+
locations = DynamicModelMultipleChoiceField(
170+
queryset=Location.objects.all(),
171+
required=False
172+
)
169173
device_types = DynamicModelMultipleChoiceField(
170174
queryset=DeviceType.objects.all(),
171175
required=False
@@ -202,15 +206,22 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
202206
queryset=Tag.objects.all(),
203207
required=False
204208
)
205-
data = JSONField(
206-
label=''
209+
data = JSONField()
210+
211+
fieldsets = (
212+
('Config Context', ('name', 'weight', 'description', 'data', 'is_active')),
213+
('Assignment', (
214+
'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
215+
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
216+
)),
207217
)
208218

209219
class Meta:
210220
model = ConfigContext
211221
fields = (
212-
'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
213-
'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
222+
'name', 'weight', 'description', 'data', 'is_active', 'regions', 'site_groups', 'sites', 'locations',
223+
'roles', 'device_types', 'platforms', 'cluster_types', 'cluster_groups', 'clusters', 'tenant_groups',
224+
'tenants', 'tags',
214225
)
215226

216227

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 4.0.5 on 2022-06-22 19:13
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('dcim', '0156_location_status'),
10+
('extras', '0075_customfield_ui_visibility'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='configcontext',
16+
name='locations',
17+
field=models.ManyToManyField(blank=True, related_name='+', to='dcim.location'),
18+
),
19+
]

netbox/extras/models/configcontexts.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from collections import OrderedDict
2-
31
from django.core.validators import ValidationError
42
from django.db import models
53
from django.urls import reverse
@@ -55,6 +53,11 @@ class ConfigContext(WebhooksMixin, ChangeLoggedModel):
5553
related_name='+',
5654
blank=True
5755
)
56+
locations = models.ManyToManyField(
57+
to='dcim.Location',
58+
related_name='+',
59+
blank=True
60+
)
5861
device_types = models.ManyToManyField(
5962
to='dcim.DeviceType',
6063
related_name='+',
@@ -138,11 +141,10 @@ class Meta:
138141

139142
def get_config_context(self):
140143
"""
144+
Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs.
141145
Return the rendered configuration context for a device or VM.
142146
"""
143-
144-
# Compile all config data, overwriting lower-weight values with higher-weight values where a collision occurs
145-
data = OrderedDict()
147+
data = {}
146148

147149
if not hasattr(self, 'config_context_data'):
148150
# The annotation is not available, so we fall back to manually querying for the config context objects

netbox/extras/querysets.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ def get_for_object(self, obj, aggregate_data=False):
1919
# `device_role` for Device; `role` for VirtualMachine
2020
role = getattr(obj, 'device_role', None) or obj.role
2121

22-
# Device type assignment is relevant only for Devices
22+
# Device type and location assignment is relevant only for Devices
2323
device_type = getattr(obj, 'device_type', None)
24+
location = getattr(obj, 'location', None)
2425

2526
# Get assigned cluster, group, and type (if any)
2627
cluster = getattr(obj, 'cluster', None)
@@ -42,6 +43,7 @@ def get_for_object(self, obj, aggregate_data=False):
4243
Q(regions__in=regions) | Q(regions=None),
4344
Q(site_groups__in=sitegroups) | Q(site_groups=None),
4445
Q(sites=obj.site) | Q(sites=None),
46+
Q(locations=location) | Q(locations=None),
4547
Q(device_types=device_type) | Q(device_types=None),
4648
Q(roles=role) | Q(roles=None),
4749
Q(platforms=obj.platform) | Q(platforms=None),
@@ -114,6 +116,7 @@ def _get_config_context_filters(self):
114116
)
115117

116118
if self.model._meta.model_name == 'device':
119+
base_query.add((Q(locations=OuterRef('location')) | Q(locations=None)), Q.AND)
117120
base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
118121
base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
119122
base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)

0 commit comments

Comments
 (0)