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