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:
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
|
@ -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,
|
||||||
|
@ -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'),
|
||||||
|
),
|
||||||
]
|
]
|
@ -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 = [
|
||||||
|
@ -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='+',
|
||||||
|
@ -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'
|
||||||
|
@ -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]}
|
||||||
|
@ -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 %}
|
||||||
|
@ -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 %}
|
||||||
|
Reference in New Issue
Block a user