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

View File

@ -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

View File

@ -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',
]

View File

@ -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(),

View File

@ -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,

View File

@ -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'),
),
]

View File

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

View File

@ -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='+',

View File

@ -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'

View File

@ -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]}

View File

@ -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 %}

View File

@ -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 %}