1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #4833: Allow assigning config contexts by device type

This commit is contained in:
Jeremy Stretch
2021-03-29 15:40:09 -04:00
parent b070be1c41
commit cd629fc737
12 changed files with 84 additions and 17 deletions

View File

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

View File

@ -84,6 +84,7 @@ A new Cloud model has been introduced to represent the boundary of a network tha
### Enhancements ### 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 * [#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 * [#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 * [#5401](https://github.com/netbox-community/netbox/issues/5401) - Extend custom field support to device component models

View File

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

View File

@ -4,7 +4,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db.models import Q from django.db.models import Q
from django.forms import DateField, IntegerField, NullBooleanField 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 tenancy.models import Tenant, TenantGroup
from utilities.filters import BaseFilterSet, ContentTypeFilter from utilities.filters import BaseFilterSet, ContentTypeFilter
from virtualization.models import Cluster, ClusterGroup from virtualization.models import Cluster, ClusterGroup
@ -206,6 +206,11 @@ class ConfigContextFilterSet(BaseFilterSet):
to_field_name='slug', to_field_name='slug',
label='Site (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( role_id = django_filters.ModelMultipleChoiceFilter(
field_name='roles', field_name='roles',
queryset=DeviceRole.objects.all(), queryset=DeviceRole.objects.all(),

View File

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

View File

@ -14,4 +14,9 @@ class Migration(migrations.Migration):
name='site_groups', name='site_groups',
field=models.ManyToManyField(blank=True, related_name='_extras_configcontext_site_groups_+', to='dcim.SiteGroup'), 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'),
),
] ]

View File

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

View File

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

View File

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

View File

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

View File

@ -94,13 +94,27 @@
{% endif %} {% endif %}
</td> </td>
</tr> </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> <tr>
<td>Roles</td> <td>Roles</td>
<td> <td>
{% if object.roles.all %} {% if object.roles.all %}
<ul> <ul>
{% for role in object.roles.all %} {% for devicerole in object.roles.all %}
<li><a href="{% url 'dcim:device_list' %}?role={{ role.slug }}">{{ role }}</a></li> <li><a href="{{ devicerole.get_absolute_url }}">{{ devicerole }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}

View File

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