diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 379d71b0d..ee72a0850 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -9,6 +9,7 @@ from timezone_field.rest_framework import TimeZoneSerializerField from dcim.choices import * from dcim.constants import * from dcim.models import * +from extras.api.nested_serializers import NestedConfigTemplateSerializer from ipam.api.nested_serializers import ( NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer, NestedVRFSerializer, @@ -651,6 +652,7 @@ class DeviceSerializer(NetBoxModelSerializer): cluster = NestedClusterSerializer(required=False, allow_null=True) virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None) + config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None) class Meta: model = Device @@ -658,7 +660,7 @@ class DeviceSerializer(NetBoxModelSerializer): 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', - 'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', + 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', ] @swagger_serializer_method(serializer_or_field=NestedDeviceSerializer) diff --git a/netbox/dcim/filtersets.py b/netbox/dcim/filtersets.py index 774f8a41f..f63ab79ff 100644 --- a/netbox/dcim/filtersets.py +++ b/netbox/dcim/filtersets.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from django.utils.translation import gettext as _ from extras.filtersets import LocalConfigContextFilterSet +from extras.models import ConfigTemplate from ipam.models import ASN, L2VPN, IPAddress, VRF from netbox.filtersets import ( BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, @@ -936,6 +937,10 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter method='_virtual_chassis_member', label=_('Is a virtual chassis member') ) + config_template_id = django_filters.ModelMultipleChoiceFilter( + queryset=ConfigTemplate.objects.all(), + label=_('Config template (ID)'), + ) console_ports = django_filters.BooleanFilter( method='_console_ports', label=_('Has console ports'), diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index c00359d4c..0edabb1f4 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -6,6 +6,7 @@ from timezone_field import TimeZoneFormField from dcim.choices import * from dcim.constants import * from dcim.models import * +from extras.models import ConfigTemplate from ipam.models import ASN, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant @@ -540,6 +541,10 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): max_length=200, required=False ) + config_template = DynamicModelChoiceField( + queryset=ConfigTemplate.objects.all(), + required=False + ) comments = CommentField( widget=forms.Textarea, label='Comments' @@ -550,6 +555,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): ('Device', ('device_role', 'status', 'tenant', 'platform', 'description')), ('Location', ('site', 'location')), ('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')), + ('Configuration', ('config_template',)), ) nullable_fields = ( 'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments', diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 1e8abcac6..1371ea5d7 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -8,6 +8,7 @@ from django.utils.translation import gettext as _ from dcim.choices import * from dcim.constants import * from dcim.models import * +from extras.models import ConfigTemplate from ipam.models import VRF from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant @@ -434,12 +435,17 @@ class DeviceImportForm(BaseDeviceImportForm): required=False, help_text=_('Airflow direction') ) + config_template = CSVModelChoiceField( + queryset=ConfigTemplate.objects.all(), + to_field_name='name', + help_text=_('Config template') + ) class Meta(BaseDeviceImportForm.Meta): fields = [ 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis', - 'vc_position', 'vc_priority', 'cluster', 'description', 'comments', 'tags', + 'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments', 'tags', ] def __init__(self, data=None, *args, **kwargs): diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index b5a6cd53b..58f44d479 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -6,6 +6,7 @@ from dcim.choices import * from dcim.constants import * from dcim.models import * from extras.forms import LocalConfigContextFilterForm +from extras.models import ConfigTemplate from ipam.models import ASN, L2VPN, VRF from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm @@ -598,7 +599,7 @@ class DeviceFilterForm( ('Components', ( 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', )), - ('Miscellaneous', ('has_primary_ip', 'virtual_chassis_member', 'local_context_data')) + ('Miscellaneous', ('has_primary_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data')) ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -680,6 +681,11 @@ class DeviceFilterForm( required=False, label='MAC address' ) + config_template_id = DynamicModelMultipleChoiceField( + queryset=ConfigTemplate.objects.all(), + required=False, + label=_('Config template') + ) has_primary_ip = forms.NullBooleanField( required=False, label='Has a primary IP', diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 8bac5d342..e71e772b6 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -7,6 +7,7 @@ from timezone_field import TimeZoneFormField from dcim.choices import * from dcim.constants import * from dcim.models import * +from extras.models import ConfigTemplate from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm @@ -565,6 +566,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm): label=_('Priority'), help_text=_("The priority of the device in the virtual chassis") ) + config_template = DynamicModelChoiceField( + queryset=ConfigTemplate.objects.all(), + required=False + ) class Meta: model = Device @@ -572,7 +577,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm): 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack', 'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', - 'description', 'comments', 'tags', 'local_context_data' + 'description', 'config_template', 'comments', 'tags', 'local_context_data' ] help_texts = { 'device_role': _("The function this device serves"), diff --git a/netbox/dcim/migrations/0169_device_configtemplate.py b/netbox/dcim/migrations/0169_device_configtemplate.py new file mode 100644 index 000000000..7fd80e2b5 --- /dev/null +++ b/netbox/dcim/migrations/0169_device_configtemplate.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.6 on 2023-02-10 14:32 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('extras', '0086_configtemplate'), + ('dcim', '0168_interface_template_enabled'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='config_template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='extras.configtemplate'), + ), + ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 94f61aba7..59a6bfd3b 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -590,6 +590,13 @@ class Device(PrimaryModel, ConfigContextModel): null=True, validators=[MaxValueValidator(255)] ) + config_template = models.ForeignKey( + to='extras.ConfigTemplate', + on_delete=models.PROTECT, + related_name='devices', + blank=True, + null=True + ) # Generic relations contacts = GenericRelation( diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 904e96b83..1cbc388fb 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -203,6 +203,9 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): vc_priority = tables.Column( verbose_name='VC Priority' ) + config_template = tables.Column( + linkify=True + ) comments = columns.MarkdownColumn() tags = columns.TagColumn( url_name='dcim:device_list' @@ -214,7 +217,7 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable): 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face', 'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', - 'vc_priority', 'description', 'comments', 'contacts', 'tags', 'created', 'last_updated', + 'vc_priority', 'description', 'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 095314e7b..30ca6f1b9 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -1997,6 +1997,32 @@ class DeviceInventoryView(DeviceComponentsView): ) +@register_model_view(Device, 'render-config') +class DeviceRenderConfigView(generic.ObjectView): + queryset = Device.objects.all() + template_name = 'dcim/device/render_config.html' + tab = ViewTab( + label=_('Render Config'), + permission='extras.view_configtemplate', + weight=2000 + ) + + def get_extra_context(self, request, instance): + context_data = { + 'device': instance, + } + context_data.update(**instance.get_config_context()) + if instance.config_template: + rendered_config = instance.config_template.render(context=context_data) + else: + rendered_config = None + + return { + 'context_data': context_data, + 'rendered_config': rendered_config, + } + + @register_model_view(Device, 'configcontext', path='config-context') class DeviceConfigContextView(ObjectConfigContextView): queryset = Device.objects.annotate_config_context_data() @@ -2004,7 +2030,7 @@ class DeviceConfigContextView(ObjectConfigContextView): tab = ViewTab( label=_('Config Context'), permission='extras.view_configcontext', - weight=2000 + weight=2100 ) diff --git a/netbox/extras/api/nested_serializers.py b/netbox/extras/api/nested_serializers.py index 5644b0b4e..dab0798fe 100644 --- a/netbox/extras/api/nested_serializers.py +++ b/netbox/extras/api/nested_serializers.py @@ -7,6 +7,7 @@ from users.api.nested_serializers import NestedUserSerializer __all__ = [ 'NestedConfigContextSerializer', + 'NestedConfigTemplateSerializer', 'NestedCustomFieldSerializer', 'NestedCustomLinkSerializer', 'NestedExportTemplateSerializer', @@ -51,6 +52,14 @@ class NestedConfigContextSerializer(WritableNestedSerializer): fields = ['id', 'url', 'display', 'name'] +class NestedConfigTemplateSerializer(WritableNestedSerializer): + url = serializers.HyperlinkedIdentityField(view_name='extras-api:configtemplate-detail') + + class Meta: + model = models.ConfigTemplate + fields = ['id', 'url', 'display', 'name'] + + class NestedExportTemplateSerializer(WritableNestedSerializer): url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 3c2cc6299..3af7814ae 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -9,9 +9,7 @@
-
- Device -
+
Device
@@ -111,6 +109,10 @@ + + + +
Asset Tag {{ object.asset_tag|placeholder }}
Config Template{{ object.config_template|linkify|placeholder }}
diff --git a/netbox/templates/dcim/device/render_config.html b/netbox/templates/dcim/device/render_config.html new file mode 100644 index 000000000..9ff6fbada --- /dev/null +++ b/netbox/templates/dcim/device/render_config.html @@ -0,0 +1,47 @@ +{% extends 'dcim/device/base.html' %} +{% load static %} + +{% block title %}{{ object }} - Config{% endblock %} + +{% block content %} +
+
+
+
Config Template
+
+ + + + + + + + + + + + + +
Config Template{{ object.config_template|linkify|placeholder }}
Data Source{{ object.config_template.data_file.source|linkify|placeholder }}
Data File{{ object.config_template.data_file|linkify|placeholder }}
+
+
+
+
+
+
Context Data
+
{{ context_data|pprint }}
+
+
+
+
+
+
+ {% if rendered_config %} +
{{ rendered_config }}
+ {% else %} +
No configuration template found
+ {% endif %} +
+
+
+{% endblock %} diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index b814e65ef..07e3bbdc9 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -65,6 +65,7 @@
{% render_field form.status %} {% render_field form.platform %} + {% render_field form.config_template %} {% if object.pk %} {% render_field form.primary_ip4 %} {% render_field form.primary_ip6 %}