Add config_template field to Device

This commit is contained in:
jeremystretch 2023-02-10 14:17:58 -05:00
parent f5e09c6f48
commit 8681e6d823
14 changed files with 154 additions and 9 deletions

View File

@ -9,6 +9,7 @@ from timezone_field.rest_framework import TimeZoneSerializerField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.api.nested_serializers import NestedConfigTemplateSerializer
from ipam.api.nested_serializers import ( from ipam.api.nested_serializers import (
NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer, NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer,
NestedVRFSerializer, NestedVRFSerializer,
@ -651,6 +652,7 @@ class DeviceSerializer(NetBoxModelSerializer):
cluster = NestedClusterSerializer(required=False, allow_null=True) cluster = NestedClusterSerializer(required=False, allow_null=True)
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None) 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) 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: class Meta:
model = Device model = Device
@ -658,7 +660,7 @@ class DeviceSerializer(NetBoxModelSerializer):
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip', 'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', '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) @swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)

View File

@ -3,6 +3,7 @@ from django.contrib.auth.models import User
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from extras.filtersets import LocalConfigContextFilterSet from extras.filtersets import LocalConfigContextFilterSet
from extras.models import ConfigTemplate
from ipam.models import ASN, L2VPN, IPAddress, VRF from ipam.models import ASN, L2VPN, IPAddress, VRF
from netbox.filtersets import ( from netbox.filtersets import (
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet, BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
@ -936,6 +937,10 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
method='_virtual_chassis_member', method='_virtual_chassis_member',
label=_('Is a 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( console_ports = django_filters.BooleanFilter(
method='_console_ports', method='_console_ports',
label=_('Has console ports'), label=_('Has console ports'),

View File

@ -6,6 +6,7 @@ from timezone_field import TimeZoneFormField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.models import ConfigTemplate
from ipam.models import ASN, VLAN, VLANGroup, VRF from ipam.models import ASN, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelBulkEditForm from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant from tenancy.models import Tenant
@ -540,6 +541,10 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
max_length=200, max_length=200,
required=False required=False
) )
config_template = DynamicModelChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False
)
comments = CommentField( comments = CommentField(
widget=forms.Textarea, widget=forms.Textarea,
label='Comments' label='Comments'
@ -550,6 +555,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
('Device', ('device_role', 'status', 'tenant', 'platform', 'description')), ('Device', ('device_role', 'status', 'tenant', 'platform', 'description')),
('Location', ('site', 'location')), ('Location', ('site', 'location')),
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')), ('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
('Configuration', ('config_template',)),
) )
nullable_fields = ( nullable_fields = (
'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments', 'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext as _
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.models import ConfigTemplate
from ipam.models import VRF from ipam.models import VRF
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant from tenancy.models import Tenant
@ -434,12 +435,17 @@ class DeviceImportForm(BaseDeviceImportForm):
required=False, required=False,
help_text=_('Airflow direction') help_text=_('Airflow direction')
) )
config_template = CSVModelChoiceField(
queryset=ConfigTemplate.objects.all(),
to_field_name='name',
help_text=_('Config template')
)
class Meta(BaseDeviceImportForm.Meta): class Meta(BaseDeviceImportForm.Meta):
fields = [ fields = [
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status', 'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis', '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): def __init__(self, data=None, *args, **kwargs):

View File

@ -6,6 +6,7 @@ from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.forms import LocalConfigContextFilterForm from extras.forms import LocalConfigContextFilterForm
from extras.models import ConfigTemplate
from ipam.models import ASN, L2VPN, VRF from ipam.models import ASN, L2VPN, VRF
from netbox.forms import NetBoxModelFilterSetForm from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
@ -598,7 +599,7 @@ class DeviceFilterForm(
('Components', ( ('Components', (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', '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( region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -680,6 +681,11 @@ class DeviceFilterForm(
required=False, required=False,
label='MAC address' label='MAC address'
) )
config_template_id = DynamicModelMultipleChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False,
label=_('Config template')
)
has_primary_ip = forms.NullBooleanField( has_primary_ip = forms.NullBooleanField(
required=False, required=False,
label='Has a primary IP', label='Has a primary IP',

View File

@ -7,6 +7,7 @@ from timezone_field import TimeZoneFormField
from dcim.choices import * from dcim.choices import *
from dcim.constants import * from dcim.constants import *
from dcim.models import * from dcim.models import *
from extras.models import ConfigTemplate
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
@ -565,6 +566,10 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=_('Priority'), label=_('Priority'),
help_text=_("The priority of the device in the virtual chassis") help_text=_("The priority of the device in the virtual chassis")
) )
config_template = DynamicModelChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False
)
class Meta: class Meta:
model = Device model = Device
@ -572,7 +577,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack', 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'rack',
'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'location', 'position', 'face', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6',
'cluster_group', 'cluster', 'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', '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 = { help_texts = {
'device_role': _("The function this device serves"), 'device_role': _("The function this device serves"),

View File

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

View File

@ -590,6 +590,13 @@ class Device(PrimaryModel, ConfigContextModel):
null=True, null=True,
validators=[MaxValueValidator(255)] validators=[MaxValueValidator(255)]
) )
config_template = models.ForeignKey(
to='extras.ConfigTemplate',
on_delete=models.PROTECT,
related_name='devices',
blank=True,
null=True
)
# Generic relations # Generic relations
contacts = GenericRelation( contacts = GenericRelation(

View File

@ -203,6 +203,9 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
vc_priority = tables.Column( vc_priority = tables.Column(
verbose_name='VC Priority' verbose_name='VC Priority'
) )
config_template = tables.Column(
linkify=True
)
comments = columns.MarkdownColumn() comments = columns.MarkdownColumn()
tags = columns.TagColumn( tags = columns.TagColumn(
url_name='dcim:device_list' 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', 'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face', 'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'position', 'face',
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', '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 = ( default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type', 'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',

View File

@ -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') @register_model_view(Device, 'configcontext', path='config-context')
class DeviceConfigContextView(ObjectConfigContextView): class DeviceConfigContextView(ObjectConfigContextView):
queryset = Device.objects.annotate_config_context_data() queryset = Device.objects.annotate_config_context_data()
@ -2004,7 +2030,7 @@ class DeviceConfigContextView(ObjectConfigContextView):
tab = ViewTab( tab = ViewTab(
label=_('Config Context'), label=_('Config Context'),
permission='extras.view_configcontext', permission='extras.view_configcontext',
weight=2000 weight=2100
) )

View File

@ -7,6 +7,7 @@ from users.api.nested_serializers import NestedUserSerializer
__all__ = [ __all__ = [
'NestedConfigContextSerializer', 'NestedConfigContextSerializer',
'NestedConfigTemplateSerializer',
'NestedCustomFieldSerializer', 'NestedCustomFieldSerializer',
'NestedCustomLinkSerializer', 'NestedCustomLinkSerializer',
'NestedExportTemplateSerializer', 'NestedExportTemplateSerializer',
@ -51,6 +52,14 @@ class NestedConfigContextSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name'] 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): class NestedExportTemplateSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail') url = serializers.HyperlinkedIdentityField(view_name='extras-api:exporttemplate-detail')

View File

@ -9,9 +9,7 @@
<div class="row"> <div class="row">
<div class="col col-12 col-xl-6"> <div class="col col-12 col-xl-6">
<div class="card"> <div class="card">
<h5 class="card-header"> <h5 class="card-header">Device</h5>
Device
</h5>
<div class="card-body"> <div class="card-body">
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
<tr> <tr>
@ -111,6 +109,10 @@
<th scope="row">Asset Tag</th> <th scope="row">Asset Tag</th>
<td class="font-monospace">{{ object.asset_tag|placeholder }}</td> <td class="font-monospace">{{ object.asset_tag|placeholder }}</td>
</tr> </tr>
<tr>
<th scope="row">Config Template</th>
<td>{{ object.config_template|linkify|placeholder }}</td>
</tr>
</table> </table>
</div> </div>
</div> </div>

View File

@ -0,0 +1,47 @@
{% extends 'dcim/device/base.html' %}
{% load static %}
{% block title %}{{ object }} - Config{% endblock %}
{% block content %}
<div class="row mb-3">
<div class="col-5">
<div class="card">
<h5 class="card-header">Config Template</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">Config Template</th>
<td>{{ object.config_template|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Data Source</th>
<td>{{ object.config_template.data_file.source|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Data File</th>
<td>{{ object.config_template.data_file|linkify|placeholder }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-7">
<div class="card">
<h5 class="card-header">Context Data</h5>
<pre class="card-body">{{ context_data|pprint }}</pre>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
{% if rendered_config %}
<pre class="card-body">{{ rendered_config }}</pre>
{% else %}
<div class="card-body text-muted">No configuration template found</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -65,6 +65,7 @@
</div> </div>
{% render_field form.status %} {% render_field form.status %}
{% render_field form.platform %} {% render_field form.platform %}
{% render_field form.config_template %}
{% if object.pk %} {% if object.pk %}
{% render_field form.primary_ip4 %} {% render_field form.primary_ip4 %}
{% render_field form.primary_ip6 %} {% render_field form.primary_ip6 %}