Selaa lähdekoodia

Closes #4833: Allow assigning config contexts by device type

Jeremy Stretch 4 vuotta sitten
vanhempi
commit
cd629fc737

+ 4 - 2
docs/models/extras/configcontext.md

@@ -3,11 +3,13 @@
 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:
 
 * Region
+* Site group
 * Site
+* Device type (devices only)
 * Role
 * Platform
-* Cluster group
-* Cluster
+* Cluster group (VMs only)
+* Cluster (VMs only)
 * Tenant group
 * Tenant
 * Tag

+ 1 - 0
docs/release-notes/version-2.11.md

@@ -84,6 +84,7 @@ A new Cloud model has been introduced to represent the boundary of a network tha
 
 ### Enhancements
 
+* [#4833](https://github.com/netbox-community/netbox/issues/4833) - Allow assigning config contexts by device type
 * [#5370](https://github.com/netbox-community/netbox/issues/5370) - Extend custom field support to organizational models
 * [#5375](https://github.com/netbox-community/netbox/issues/5375) - Add `speed` attribute to console port models
 * [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models

+ 11 - 5
netbox/extras/api/serializers.py

@@ -4,10 +4,10 @@ from drf_yasg.utils import swagger_serializer_method
 from rest_framework import serializers
 
 from dcim.api.nested_serializers import (
-    NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedRackSerializer,
-    NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
+    NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedDeviceTypeSerializer, NestedPlatformSerializer,
+    NestedRackSerializer, NestedRegionSerializer, NestedSiteSerializer, NestedSiteGroupSerializer,
 )
-from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
+from dcim.models import Device, DeviceRole, DeviceType, Platform, Rack, Region, Site, SiteGroup
 from extras.choices import *
 from extras.models import *
 from extras.utils import FeatureQuery
@@ -251,6 +251,12 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         required=False,
         many=True
     )
+    device_types = SerializedPKRelatedField(
+        queryset=DeviceType.objects.all(),
+        serializer=NestedDeviceTypeSerializer,
+        required=False,
+        many=True
+    )
     roles = SerializedPKRelatedField(
         queryset=DeviceRole.objects.all(),
         serializer=NestedDeviceRoleSerializer,
@@ -298,8 +304,8 @@ class ConfigContextSerializer(ValidatedModelSerializer):
         model = ConfigContext
         fields = [
             'id', 'url', 'display', 'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites',
-            'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data', 'created',
-            'last_updated',
+            'device_types', 'roles', 'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
+            'data', 'created', 'last_updated',
         ]
 
 

+ 6 - 1
netbox/extras/filters.py

@@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
 from django.db.models import Q
 from django.forms import DateField, IntegerField, NullBooleanField
 
-from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.filters import BaseFilterSet, ContentTypeFilter
 from virtualization.models import Cluster, ClusterGroup
@@ -206,6 +206,11 @@ class ConfigContextFilterSet(BaseFilterSet):
         to_field_name='slug',
         label='Site (slug)',
     )
+    device_type_id = django_filters.ModelMultipleChoiceFilter(
+        field_name='device_types',
+        queryset=DeviceType.objects.all(),
+        label='Device type',
+    )
     role_id = django_filters.ModelMultipleChoiceFilter(
         field_name='roles',
         queryset=DeviceRole.objects.all(),

+ 13 - 4
netbox/extras/forms.py

@@ -4,11 +4,11 @@ from django.contrib.contenttypes.models import ContentType
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 
-from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Platform, Region, Site, SiteGroup
 from tenancy.models import Tenant, TenantGroup
 from utilities.forms import (
     add_blank_choice, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ColorSelect,
-    CommentField, CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
+    CSVModelForm, DateTimePicker, DynamicModelMultipleChoiceField, JSONField, SlugField, StaticSelect2,
     BOOLEAN_WITH_BLANK_CHOICES,
 )
 from virtualization.models import Cluster, ClusterGroup
@@ -218,6 +218,10 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
         queryset=Site.objects.all(),
         required=False
     )
+    device_types = DynamicModelMultipleChoiceField(
+        queryset=DeviceType.objects.all(),
+        required=False
+    )
     roles = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.all(),
         required=False
@@ -253,8 +257,8 @@ class ConfigContextForm(BootstrapMixin, forms.ModelForm):
     class Meta:
         model = ConfigContext
         fields = (
-            'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'platforms',
-            'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
+            'name', 'weight', 'description', 'is_active', 'regions', 'site_groups', 'sites', 'roles', 'device_types',
+            'platforms', 'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags', 'data',
         )
 
 
@@ -306,6 +310,11 @@ class ConfigContextFilterForm(BootstrapMixin, forms.Form):
         required=False,
         label=_('Sites')
     )
+    device_type_id = DynamicModelMultipleChoiceField(
+        queryset=DeviceType.objects.all(),
+        required=False,
+        label=_('Device types')
+    )
     role_id = DynamicModelMultipleChoiceField(
         queryset=DeviceRole.objects.all(),
         required=False,

+ 5 - 0
netbox/extras/migrations/0056_sitegroup.py → netbox/extras/migrations/0056_extend_configcontext.py

@@ -14,4 +14,9 @@ class Migration(migrations.Migration):
             name='site_groups',
             field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_site_groups_+', to='dcim.SiteGroup'),
         ),
+        migrations.AddField(
+            model_name='configcontext',
+            name='device_types',
+            field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_device_types_+', to='dcim.DeviceType'),
+        ),
     ]

+ 1 - 1
netbox/extras/migrations/0057_customlink_rename_fields.py

@@ -6,7 +6,7 @@ from django.db import migrations, models
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('extras', '0056_sitegroup'),
+        ('extras', '0056_extend_configcontext'),
     ]
 
     operations = [

+ 5 - 0
netbox/extras/models/configcontexts.py

@@ -56,6 +56,11 @@ class ConfigContext(ChangeLoggedModel):
         related_name='+',
         blank=True
     )
+    device_types = models.ManyToManyField(
+        to='dcim.DeviceType',
+        related_name='+',
+        blank=True
+    )
     roles = models.ManyToManyField(
         to='dcim.DeviceRole',
         related_name='+',

+ 6 - 1
netbox/extras/querysets.py

@@ -19,7 +19,10 @@ class ConfigContextQuerySet(RestrictedQuerySet):
         # `device_role` for Device; `role` for VirtualMachine
         role = getattr(obj, 'device_role', None) or obj.role
 
-        # Virtualization cluster for VirtualMachine
+        # Device type assignment is relevant only for Devices
+        device_type = getattr(obj, 'device_type', None)
+
+        # Cluster assignment is relevant only for VirtualMachines
         cluster = getattr(obj, 'cluster', None)
         cluster_group = getattr(cluster, 'group', None)
 
@@ -36,6 +39,7 @@ class ConfigContextQuerySet(RestrictedQuerySet):
         queryset = self.filter(
             Q(regions__in=regions) | Q(regions=None),
             Q(sites=obj.site) | Q(sites=None),
+            Q(device_types=device_type) | Q(device_types=None),
             Q(roles=role) | Q(roles=None),
             Q(platforms=obj.platform) | Q(platforms=None),
             Q(cluster_groups=cluster_group) | Q(cluster_groups=None),
@@ -108,6 +112,7 @@ class ConfigContextModelQuerySet(RestrictedQuerySet):
         )
 
         if self.model._meta.model_name == 'device':
+            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)
             region_field = 'site__region'

+ 15 - 1
netbox/extras/tests/test_filters.py

@@ -4,7 +4,7 @@ from django.contrib.auth.models import User
 from django.contrib.contenttypes.models import ContentType
 from django.test import TestCase
 
-from dcim.models import DeviceRole, Platform, Rack, Region, Site, SiteGroup
+from dcim.models import DeviceRole, DeviceType, Manufacturer, Platform, Rack, Region, Site, SiteGroup
 from extras.choices import JournalEntryKindChoices, ObjectChangeActionChoices
 from extras.filters import *
 from extras.models import *
@@ -379,6 +379,14 @@ class ConfigContextTestCase(TestCase):
         )
         Site.objects.bulk_create(sites)
 
+        manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
+        device_types = (
+            DeviceType(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1'),
+            DeviceType(manufacturer=manufacturer, model='Device Type 2', slug='device-type-3'),
+            DeviceType(manufacturer=manufacturer, model='Device Type 3', slug='device-type-4'),
+        )
+        DeviceType.objects.bulk_create(device_types)
+
         device_roles = (
             DeviceRole(name='Device Role 1', slug='device-role-1'),
             DeviceRole(name='Device Role 2', slug='device-role-2'),
@@ -433,6 +441,7 @@ class ConfigContextTestCase(TestCase):
             c.regions.set([regions[i]])
             c.site_groups.set([site_groups[i]])
             c.sites.set([sites[i]])
+            c.roles.set([device_types[i]])
             c.roles.set([device_roles[i]])
             c.platforms.set([platforms[i]])
             c.cluster_groups.set([cluster_groups[i]])
@@ -475,6 +484,11 @@ class ConfigContextTestCase(TestCase):
         params = {'site': [sites[0].slug, sites[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]}
+        self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
+
     def test_role(self):
         device_roles = DeviceRole.objects.all()[:2]
         params = {'role_id': [device_roles[0].pk, device_roles[1].pk]}

+ 16 - 2
netbox/templates/extras/configcontext.html

@@ -94,13 +94,27 @@
                             {% endif %}
                         </td>
                     </tr>
+                    <tr>
+                        <td>Device Types</td>
+                        <td>
+                            {% if object.device_types.all %}
+                                <ul>
+                                    {% for devicetype in object.device_types.all %}
+                                        <li><a href="{{ devicetype.get_absolute_url }}">{{ devicetype }}</a></li>
+                                    {% endfor %}
+                                </ul>
+                            {% else %}
+                                <span class="text-muted">None</span>
+                            {% endif %}
+                        </td>
+                    </tr>
                     <tr>
                         <td>Roles</td>
                         <td>
                             {% if object.roles.all %}
                                 <ul>
-                                    {% for role in object.roles.all %}
-                                        <li><a href="{% url 'dcim:device_list' %}?role={{ role.slug }}">{{ role }}</a></li>
+                                    {% for devicerole in object.roles.all %}
+                                        <li><a href="{{ devicerole.get_absolute_url }}">{{ devicerole }}</a></li>
                                     {% endfor %}
                                 </ul>
                             {% else %}

+ 1 - 0
netbox/templates/extras/configcontext_edit.html

@@ -17,6 +17,7 @@
             {% render_field form.regions %}
             {% render_field form.site_groups %}
             {% render_field form.sites %}
+            {% render_field form.device_types %}
             {% render_field form.roles %}
             {% render_field form.platforms %}
             {% render_field form.cluster_groups %}