Skip to content

Commit cd629fc

Browse files
committed
Closes #4833: Allow assigning config contexts by device type
1 parent b070be1 commit cd629fc

File tree

12 files changed

+84
-17
lines changed

12 files changed

+84
-17
lines changed

docs/models/extras/configcontext.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
Sometimes it is desirable to associate additional data with a group of devices or virtual machines to aid in automated configuration. For example, you might want to associate a set of syslog servers for all devices within a particular region. Context data enables the association of extra user-defined data with devices and virtual machines grouped by one or more of the following assignments:
44

55
* Region
6+
* Site group
67
* Site
8+
* Device type (devices only)
79
* Role
810
* Platform
9-
* Cluster group
10-
* Cluster
11+
* Cluster group (VMs only)
12+
* Cluster (VMs only)
1113
* Tenant group
1214
* Tenant
1315
* Tag

docs/release-notes/version-2.11.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ A new Cloud model has been introduced to represent the boundary of a network tha
8484

8585
### Enhancements
8686

87+
* [#4833](https://github.com/netbox-community/netbox/issues/4833) - Allow assigning config contexts by device type
8788
* [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models
8889
* [#5375](https://github.com/netbox-community/netbox/issues/5375) - Add `speed` attribute to console port models
8990
* [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models

netbox/extras/api/serializers.py

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

66
from dcim.api.nested_serializers import (
7-
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
8-
NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
7+
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer,
8+
NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
99
)
10-
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
10+
from dcim.models import Device, DeviceRole, DeviceType, Platform, Rack, Region, Site, SiteGroup
1111
from extras.choices import *
1212
from extras.models import *
1313
from extras.utils import FeatureQuery
@@ -251,6 +251,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
251251
required=False,
252252
many=True
253253
)
254+
device_types = SerializedPKRelatedField(
255+
queryset=DeviceType.objects.all(),
256+
serializer=NestedDeviceTypeSerializer,
257+
required=False,
258+
many=True
259+
)
254260
roles = SerializedPKRelatedField(
255261
queryset=DeviceRole.objects.all(),
256262
serializer=NestedDeviceRoleSerializer,
@@ -298,8 +304,8 @@ class Meta:
298304
model = ConfigContext
299305
fields = [
300306
'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
301-
'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', 'created',
302-
'last_updated',
307+
'device_types', 'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
308+
'data', 'created', 'last_updated',
303309
]
304310

305311

netbox/extras/filters.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.db.models import Q
55
from django.forms import DateField, IntegerField, NullBooleanField
66

7-
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
7+
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
88
from tenancy.models import Tenant, TenantGroup
99
from utilities.filters import BaseFilterSet, ContentTypeFilter
1010
from virtualization.models import Cluster, ClusterGroup
@@ -206,6 +206,11 @@ class ConfigContextFilterSet(BaseFilterSet):
206206
to_field_name='slug',
207207
label='Site (slug)',
208208
)
209+
device_type_id = django_filters.ModelMultipleChoiceFilter(
210+
field_name='device_types',
211+
queryset=DeviceType.objects.all(),
212+
label='Device type',
213+
)
209214
role_id = django_filters.ModelMultipleChoiceFilter(
210215
field_name='roles',
211216
queryset=DeviceRole.objects.all(),

netbox/extras/forms.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from django.utils.safestring import mark_safe
55
from django.utils.translation import gettext as _
66

7-
from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
7+
from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
88
from tenancy.models import Tenant, TenantGroup
99
from utilities.forms import (
1010
add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
11-
CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
11+
CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
1212
BOOLEAN_WITH_BLANK_CHOICES,
1313
)
1414
from virtualization.models import Cluster, ClusterGroup
@@ -218,6 +218,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
218218
queryset=Site.objects.all(),
219219
required=False
220220
)
221+
device_types = DynamicModelMultipleChoiceField(
222+
queryset=DeviceType.objects.all(),
223+
required=False
224+
)
221225
roles = DynamicModelMultipleChoiceField(
222226
queryset=DeviceRole.objects.all(),
223227
required=False
@@ -253,8 +257,8 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
253257
class Meta:
254258
model = ConfigContext
255259
fields = (
256-
'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'platforms',
257-
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
260+
'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
261+
'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
258262
)
259263

260264

@@ -306,6 +310,11 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
306310
required=False,
307311
label=_('Sites')
308312
)
313+
device_type_id = DynamicModelMultipleChoiceField(
314+
queryset=DeviceType.objects.all(),
315+
required=False,
316+
label=_('Device types')
317+
)
309318
role_id = DynamicModelMultipleChoiceField(
310319
queryset=DeviceRole.objects.all(),
311320
required=False,

netbox/extras/migrations/0056_sitegroup.py renamed to netbox/extras/migrations/0056_extend_configcontext.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,9 @@ class Migration(migrations.Migration):
1414
name='site_groups',
1515
field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_site_groups_+', to='dcim.SiteGroup'),
1616
),
17+
migrations.AddField(
18+
model_name='configcontext',
19+
name='device_types',
20+
field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_device_types_+', to='dcim.DeviceType'),
21+
),
1722
]

netbox/extras/migrations/0057_customlink_rename_fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
class Migration(migrations.Migration):
77

88
dependencies = [
9-
('extras', '0056_sitegroup'),
9+
('extras', '0056_extend_configcontext'),
1010
]
1111

1212
operations = [

netbox/extras/models/configcontexts.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ class ConfigContext(ChangeLoggedModel):
5656
related_name='+',
5757
blank=True
5858
)
59+
device_types = models.ManyToManyField(
60+
to='dcim.DeviceType',
61+
related_name='+',
62+
blank=True
63+
)
5964
roles = models.ManyToManyField(
6065
to='dcim.DeviceRole',
6166
related_name='+',

netbox/extras/querysets.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ 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-
# Virtualization cluster for VirtualMachine
22+
# Device type assignment is relevant only for Devices
23+
device_type = getattr(obj, 'device_type', None)
24+
25+
# Cluster assignment is relevant only for VirtualMachines
2326
cluster = getattr(obj, 'cluster', None)
2427
cluster_group = getattr(cluster, 'group', None)
2528

@@ -36,6 +39,7 @@ def get_for_object(self, obj, aggregate_data=False):
3639
queryset = self.filter(
3740
Q(regions__in=regions) | Q(regions=None),
3841
Q(sites=obj.site) | Q(sites=None),
42+
Q(device_types=device_type) | Q(device_types=None),
3943
Q(roles=role) | Q(roles=None),
4044
Q(platforms=obj.platform) | Q(platforms=None),
4145
Q(cluster_groups=cluster_group) | Q(cluster_groups=None),
@@ -108,6 +112,7 @@ def _get_config_context_filters(self):
108112
)
109113

110114
if self.model._meta.model_name == 'device':
115+
base_query.add((Q(device_types=OuterRef('device_type')) | Q(device_types=None)), Q.AND)
111116
base_query.add((Q(roles=OuterRef('device_role')) | Q(roles=None)), Q.AND)
112117
base_query.add((Q(sites=OuterRef('site')) | Q(sites=None)), Q.AND)
113118
region_field = 'site__region'

netbox/extras/tests/test_filters.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.contrib.contenttypes.models import ContentType
55
from django.test import TestCase
66

7-
from dcim.models import DeviceRole, Platform, Rack, Region, Site, SiteGroup
7+
from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
88
from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
99
from extras.filters import *
1010
from extras.models import *
@@ -379,6 +379,14 @@ def setUpTestData(cls):
379379
)
380380
Site.objects.bulk_create(sites)
381381

382+
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
383+
device_types = (
384+
DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
385+
DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-3'),
386+
DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-4'),
387+
)
388+
DeviceType.objects.bulk_create(device_types)
389+
382390
device_roles = (
383391
DeviceRole(name='Device Role 1', slug='device-role-1'),
384392
DeviceRole(name='Device Role 2', slug='device-role-2'),
@@ -433,6 +441,7 @@ def setUpTestData(cls):
433441
c.regions.set([regions[i]])
434442
c.site_groups.set([site_groups[i]])
435443
c.sites.set([sites[i]])
444+
c.roles.set([device_types[i]])
436445
c.roles.set([device_roles[i]])
437446
c.platforms.set([platforms[i]])
438447
c.cluster_groups.set([cluster_groups[i]])
@@ -475,6 +484,11 @@ def test_site(self):
475484
params = {'site': [sites[0].slug, sites[1].slug]}
476485
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
477486

487+
def test_device_type(self):
488+
device_types = DeviceType.objects.all()[:2]
489+
params = {'device_type_id': [device_types[0].pk, device_types[1].pk]}
490+
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
491+
478492
def test_role(self):
479493
device_roles = DeviceRole.objects.all()[:2]
480494
params = {'role_id': [device_roles[0].pk, device_roles[1].pk]}

netbox/templates/extras/configcontext.html

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,27 @@
9494
{% endif %}
9595
</td>
9696
</tr>
97+
<tr>
98+
<td>Device Types</td>
99+
<td>
100+
{% if object.device_types.all %}
101+
<ul>
102+
{% for devicetype in object.device_types.all %}
103+
<li><a href="{{ devicetype.get_absolute_url }}">{{ devicetype }}</a></li>
104+
{% endfor %}
105+
</ul>
106+
{% else %}
107+
<span class="text-muted">None</span>
108+
{% endif %}
109+
</td>
110+
</tr>
97111
<tr>
98112
<td>Roles</td>
99113
<td>
100114
{% if object.roles.all %}
101115
<ul>
102-
{% for role in object.roles.all %}
103-
<li><a href="{% url 'dcim:device_list' %}?role={{ role.slug }}">{{ role }}</a></li>
116+
{% for devicerole in object.roles.all %}
117+
<li><a href="{{ devicerole.get_absolute_url }}">{{ devicerole }}</a></li>
104118
{% endfor %}
105119
</ul>
106120
{% else %}

netbox/templates/extras/configcontext_edit.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
{% render_field form.regions %}
1818
{% render_field form.site_groups %}
1919
{% render_field form.sites %}
20+
{% render_field form.device_types %}
2021
{% render_field form.roles %}
2122
{% render_field form.platforms %}
2223
{% render_field form.cluster_groups %}

0 commit comments

Comments
 (0)