From 15be44d06b2ce546d924c4202b084424428b0fb6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Sun, 12 Feb 2023 16:31:56 -0500 Subject: [PATCH] Add config_template FK to Device --- docs/models/dcim/device.md | 4 ++++ docs/models/dcim/devicerole.md | 4 ++++ netbox/dcim/api/serializers.py | 4 ++-- netbox/dcim/api/views.py | 2 +- netbox/dcim/filtersets.py | 4 ++++ netbox/dcim/forms/bulk_edit.py | 8 ++++++-- netbox/dcim/forms/bulk_import.py | 9 ++++++++- netbox/dcim/forms/filtersets.py | 5 +++++ netbox/dcim/forms/model_forms.py | 8 ++++++-- .../0170_devicerole_config_template.py | 20 +++++++++++++++++++ netbox/dcim/models/devices.py | 13 ++++++++++++ netbox/dcim/tables/devices.py | 7 +++++-- netbox/dcim/views.py | 10 +++++++--- .../templates/dcim/device/render_config.html | 6 +++--- netbox/templates/dcim/devicerole.html | 4 ++++ 15 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 netbox/dcim/migrations/0170_devicerole_config_template.py diff --git a/docs/models/dcim/device.md b/docs/models/dcim/device.md index 33d07e07e..8f97b920b 100644 --- a/docs/models/dcim/device.md +++ b/docs/models/dcim/device.md @@ -72,6 +72,10 @@ The device's operational status. A device may be associated with a particular [platform](./platform.md) to indicate its operating system. Note that only platforms assigned to the associated manufacturer (or to no manufacturer) will be available for selection. +### Configuration Template + +The [configuration template](../extras/configtemplate.md) from which the configuration for this device can be rendered. If set, this will override any config template referenced by the device's role or platform. + ### Primary IPv4 & IPv6 Addresses Each device may designate one primary IPv4 address and/or one primary IPv6 address for management purposes. diff --git a/docs/models/dcim/devicerole.md b/docs/models/dcim/devicerole.md index e9bdc0fa6..786170f2b 100644 --- a/docs/models/dcim/devicerole.md +++ b/docs/models/dcim/devicerole.md @@ -19,3 +19,7 @@ The color used when displaying the role in the NetBox UI. ### VM Role If selected, this role may be assigned to [virtual machines](../virtualization/virtualmachine.md) + +### Configuration Template + +The default [configuration template](../extras/configtemplate.md) for devices assigned to this role. diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index ee72a0850..79b48bf65 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -606,8 +606,8 @@ class DeviceRoleSerializer(NetBoxModelSerializer): class Meta: model = DeviceRole fields = [ - 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'tags', 'custom_fields', - 'created', 'last_updated', 'device_count', 'virtualmachine_count', + 'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', + 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 954c6a2a5..484e983f0 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -366,7 +366,7 @@ class InventoryItemTemplateViewSet(NetBoxModelViewSet): # class DeviceRoleViewSet(NetBoxModelViewSet): - queryset = DeviceRole.objects.prefetch_related('tags').annotate( + queryset = DeviceRole.objects.prefetch_related('config_template', 'tags').annotate( device_count=count_related(Device, 'device_role'), virtualmachine_count=count_related(VirtualMachine, 'role') ) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index f63ab79ff..9e0963fcc 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -777,6 +777,10 @@ class InventoryItemTemplateFilterSet(ChangeLoggedModelFilterSet, DeviceTypeCompo class DeviceRoleFilterSet(OrganizationalModelFilterSet): + config_template_id = django_filters.ModelMultipleChoiceFilter( + queryset=ConfigTemplate.objects.all(), + label=_('Config template (ID)'), + ) class Meta: model = DeviceRole diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 0edabb1f4..a5d895f66 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -455,6 +455,10 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): widget=BulkEditNullBooleanSelect, label=_('VM role') ) + config_template = DynamicModelChoiceField( + queryset=ConfigTemplate.objects.all(), + required=False + ) description = forms.CharField( max_length=200, required=False @@ -462,9 +466,9 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): model = DeviceRole fieldsets = ( - (None, ('color', 'vm_role', 'description')), + (None, ('color', 'vm_role', 'config_template', 'description')), ) - nullable_fields = ('color', 'description') + nullable_fields = ('color', 'config_template', 'description') class PlatformBulkEditForm(NetBoxModelBulkEditForm): diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 1371ea5d7..a7a21ba51 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -308,11 +308,17 @@ class ModuleTypeImportForm(NetBoxModelImportForm): class DeviceRoleImportForm(NetBoxModelImportForm): + config_template = CSVModelChoiceField( + queryset=ConfigTemplate.objects.all(), + to_field_name='name', + required=False, + help_text=_('Config template') + ) slug = SlugField() class Meta: model = DeviceRole - fields = ('name', 'slug', 'color', 'vm_role', 'description', 'tags') + fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags') help_texts = { 'color': mark_safe(_('RGB color in hexadecimal (e.g. 00ff00)')), } @@ -438,6 +444,7 @@ class DeviceImportForm(BaseDeviceImportForm): config_template = CSVModelChoiceField( queryset=ConfigTemplate.objects.all(), to_field_name='name', + required=False, help_text=_('Config template') ) diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 58f44d479..8eeca7484 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -569,6 +569,11 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm): class DeviceRoleFilterForm(NetBoxModelFilterSetForm): model = DeviceRole + config_template_id = DynamicModelMultipleChoiceField( + queryset=ConfigTemplate.objects.all(), + required=False, + label=_('Config template') + ) tag = TagFilterField(model) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index e71e772b6..320a45c91 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -417,18 +417,22 @@ class ModuleTypeForm(NetBoxModelForm): class DeviceRoleForm(NetBoxModelForm): + config_template = DynamicModelChoiceField( + queryset=ConfigTemplate.objects.all(), + required=False + ) slug = SlugField() fieldsets = ( ('Device Role', ( - 'name', 'slug', 'color', 'vm_role', 'description', 'tags', + 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', )), ) class Meta: model = DeviceRole fields = [ - 'name', 'slug', 'color', 'vm_role', 'description', 'tags', + 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags', ] diff --git a/netbox/dcim/migrations/0170_devicerole_config_template.py b/netbox/dcim/migrations/0170_devicerole_config_template.py new file mode 100644 index 000000000..95975f3b4 --- /dev/null +++ b/netbox/dcim/migrations/0170_devicerole_config_template.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.6 on 2023-02-12 20:41 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0086_configtemplate'), + ('dcim', '0169_device_configtemplate'), + ] + + operations = [ + migrations.AddField( + model_name='devicerole', + name='config_template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='device_roles', to='extras.configtemplate'), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 59a6bfd3b..8b64fc8c5 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -410,6 +410,13 @@ class DeviceRole(OrganizationalModel): verbose_name='VM Role', help_text=_('Virtual machines may be assigned to this role') ) + config_template = models.ForeignKey( + to='extras.ConfigTemplate', + on_delete=models.PROTECT, + related_name='device_roles', + blank=True, + null=True + ) def get_absolute_url(self): return reverse('dcim:devicerole', args=[self.pk]) @@ -869,6 +876,12 @@ class Device(PrimaryModel, ConfigContextModel): def interfaces_count(self): return self.vc_interfaces().count() + def get_config_template(self): + """ + Return the appropriate ConfigTemplate (if any) for this Device. + """ + return self.config_template or self.device_role.config_template + def get_vc_master(self): """ If this Device is a VirtualChassis member, return the VC master. Otherwise, return None. diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 1cbc388fb..5aeb64b07 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -86,6 +86,9 @@ class DeviceRoleTable(NetBoxTable): ) color = columns.ColorColumn() vm_role = columns.BooleanColumn() + config_template = tables.Column( + linkify=True + ) tags = columns.TagColumn( url_name='dcim:devicerole_list' ) @@ -93,8 +96,8 @@ class DeviceRoleTable(NetBoxTable): class Meta(NetBoxTable.Meta): model = models.DeviceRole fields = ( - 'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description', 'slug', 'tags', - 'actions', 'created', 'last_updated', + 'pk', 'id', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'config_template', 'description', + 'slug', 'tags', 'actions', 'created', 'last_updated', ) default_columns = ('pk', 'name', 'device_count', 'vm_count', 'color', 'vm_role', 'description') diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 30ca6f1b9..164839d57 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -13,7 +13,7 @@ from django.views.generic import View from circuits.models import Circuit, CircuitTermination from extras.views import ObjectConfigContextView -from ipam.models import ASN, IPAddress, Prefix, Service, VLAN, VLANGroup +from ipam.models import ASN, IPAddress, Prefix, VLAN, VLANGroup from ipam.tables import InterfaceVLANTable from netbox.views import generic from utilities.forms import ConfirmationForm @@ -2008,16 +2008,20 @@ class DeviceRenderConfigView(generic.ObjectView): ) def get_extra_context(self, request, instance): + # Compile context data context_data = { 'device': instance, } context_data.update(**instance.get_config_context()) - if instance.config_template: - rendered_config = instance.config_template.render(context=context_data) + + # Render the config template + if config_template := instance.get_config_template(): + rendered_config = config_template.render(context=context_data) else: rendered_config = None return { + 'config_template': config_template, 'context_data': context_data, 'rendered_config': rendered_config, } diff --git a/netbox/templates/dcim/device/render_config.html b/netbox/templates/dcim/device/render_config.html index 9ff6fbada..1571b56f3 100644 --- a/netbox/templates/dcim/device/render_config.html +++ b/netbox/templates/dcim/device/render_config.html @@ -12,15 +12,15 @@ - + - + - +
Config Template{{ object.config_template|linkify|placeholder }}{{ config_template|linkify|placeholder }}
Data Source{{ object.config_template.data_file.source|linkify|placeholder }}{{ config_template.data_file.source|linkify|placeholder }}
Data File{{ object.config_template.data_file|linkify|placeholder }}{{ config_template.data_file|linkify|placeholder }}
diff --git a/netbox/templates/dcim/devicerole.html b/netbox/templates/dcim/devicerole.html index bc01dbdb7..7c0bf67f6 100644 --- a/netbox/templates/dcim/devicerole.html +++ b/netbox/templates/dcim/devicerole.html @@ -42,6 +42,10 @@ VM Role {% checkmark object.vm_role %} + + Config Template + {{ object.config_template|linkify|placeholder }} +