Closes #11559: Implement config template rendering (#11769)

* WIP

* Add config_template field to Device

* Pre-fetch referenced templates

* Correct up_to_date callable

* Add config_template FK to Device

* Update & merge migrations

* Add config_template FK to Platform

* Add tagging support for ConfigTemplate

* Catch exceptions when rendering device templates in UI

* Refactor ConfigTemplate.render()

* Add support for returning plain text content

* Add ConfigTemplate model documentation

* Add feature documentation for config rendering
This commit is contained in:
Jeremy Stretch
2023-02-17 08:33:08 -05:00
committed by jeremystretch
parent db4e00d394
commit 73a7a2d27a
45 changed files with 886 additions and 36 deletions

View File

@@ -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
@@ -454,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
@@ -461,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):
@@ -475,7 +480,10 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
max_length=50,
required=False
)
# TODO: Bulk edit support for napalm_args
config_template = DynamicModelChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False
)
description = forms.CharField(
max_length=200,
required=False
@@ -483,9 +491,9 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
model = Platform
fieldsets = (
(None, ('manufacturer', 'napalm_driver', 'description')),
(None, ('manufacturer', 'config_template', 'napalm_driver', 'description')),
)
nullable_fields = ('manufacturer', 'napalm_driver', 'description')
nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description')
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
@@ -540,6 +548,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 +562,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',

View File

@@ -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
@@ -307,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. <code>00ff00</code>)')),
}
@@ -325,10 +332,18 @@ class PlatformImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Limit platform assignments to this manufacturer')
)
config_template = CSVModelChoiceField(
queryset=ConfigTemplate.objects.all(),
to_field_name='name',
required=False,
help_text=_('Config template')
)
class Meta:
model = Platform
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags')
fields = (
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
)
class BaseDeviceImportForm(NetBoxModelImportForm):
@@ -434,12 +449,18 @@ class DeviceImportForm(BaseDeviceImportForm):
required=False,
help_text=_('Airflow direction')
)
config_template = CSVModelChoiceField(
queryset=ConfigTemplate.objects.all(),
to_field_name='name',
required=False,
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):

View File

@@ -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
@@ -568,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)
@@ -578,6 +584,11 @@ class PlatformFilterForm(NetBoxModelFilterSetForm):
required=False,
label=_('Manufacturer')
)
config_template_id = DynamicModelMultipleChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False,
label=_('Config template')
)
tag = TagFilterField(model)
@@ -598,7 +609,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 +691,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',

View File

@@ -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
@@ -416,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',
]
@@ -436,13 +441,17 @@ class PlatformForm(NetBoxModelForm):
queryset=Manufacturer.objects.all(),
required=False
)
config_template = DynamicModelChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False
)
slug = SlugField(
max_length=64
)
fieldsets = (
('Platform', (
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
)),
)
@@ -450,7 +459,7 @@ class PlatformForm(NetBoxModelForm):
class Meta:
model = Platform
fields = [
'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags',
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
]
widgets = {
'napalm_args': forms.Textarea(),
@@ -565,6 +574,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 +585,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"),