Skip to content

Commit 2de09ca

Browse files
committed
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
1 parent ae0d204 commit 2de09ca

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+886
-36
lines changed
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Configuration Rendering
2+
3+
One of the critical aspects of operating a network is ensuring that every network node is configured correctly. By leveraging configuration templates and [context data](./context-data.md), NetBox can render complete configuration files for each device on your network.
4+
5+
```mermaid
6+
flowchart TD
7+
ConfigContext & ConfigTemplate --> Config{{Rendered configuration}}
8+
9+
click ConfigContext "../../models/extras/configcontext/"
10+
click ConfigTemplate "../../models/extras/configtemplate/"
11+
```
12+
13+
## Configuration Templates
14+
15+
Configuration templates are written in the [Jinja2 templating language](https://jinja.palletsprojects.com/), and may be automatically populated from remote data sources. Context data is applied to a template during rendering to output a complete configuration file. Below is an example template.
16+
17+
```jinja2
18+
{% extends 'base.j2' %}
19+
20+
{% block content %}
21+
system {
22+
host-name {{ device.name }};
23+
domain-name example.com;
24+
time-zone UTC;
25+
authentication-order [ password radius ];
26+
ntp {
27+
{% for server in ntp_servers %}
28+
server {{ server }};
29+
{% endfor %}
30+
}
31+
}
32+
{% for interface in device.interfaces.all() %}
33+
{% include 'common/interface.j2' %}
34+
{% endfor %}
35+
{% endblock %}
36+
```
37+
38+
When rendered for a specific NetBox device, the template's `device` variable will be populated with the device instance, and `ntp_servers` will be pulled from the device's available context data. The resulting output will be a valid configuration segment that can be applied directly to a compatible network device.

docs/features/context-data.md

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ Configuration context data (or "config contexts" for short) is a powerful featur
1111
}
1212
```
1313

14+
Context data can be consumed by remote API clients, or it can be employed natively to render [configuration templates](./configuration-rendering.md).
15+
1416
Config contexts can be computed for objects based on the following criteria:
1517

1618
| Type | Devices | Virtual Machines |

docs/models/dcim/device.md

+4
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ The device's operational status.
7272

7373
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.
7474

75+
### Configuration Template
76+
77+
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.
78+
7579
### Primary IPv4 & IPv6 Addresses
7680

7781
Each device may designate one primary IPv4 address and/or one primary IPv6 address for management purposes.

docs/models/dcim/devicerole.md

+4
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,7 @@ The color used when displaying the role in the NetBox UI.
1919
### VM Role
2020

2121
If selected, this role may be assigned to [virtual machines](../virtualization/virtualmachine.md)
22+
23+
### Configuration Template
24+
25+
The default [configuration template](../extras/configtemplate.md) for devices assigned to this role.

docs/models/dcim/platform.md

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ A unique URL-friendly identifier. (This value can be used for filtering.)
2222

2323
If designated, this platform will be available for use only to devices assigned to this [manufacturer](./manufacturer.md). This can be handy e.g. for limiting network operating systems to use on hardware produced by the relevant vendor. However, it should not be used when defining general-purpose software platforms.
2424

25+
### Configuration Template
26+
27+
The default [configuration template](../extras/configtemplate.md) for devices assigned to this platform.
28+
2529
### NAPALM Driver
2630

2731
The [NAPALM driver](https://napalm.readthedocs.io/en/latest/support/index.html) associated with this platform.

docs/models/extras/configtemplate.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Configuration Templates
2+
3+
Configuration templates can be used to render [devices](../dcim/device.md) configurations from [context data](../../features/context-data.md). Templates are written in the [Jinja2 language](https://jinja.palletsprojects.com/) and can be associated with devices roles, platforms, and/or individual devices.
4+
5+
Context data is made available to [devices](../dcim/device.md) and/or [virtual machines](../virtualization/virtualmachine.md) based on their relationships to other objects in NetBox. For example, context data can be associated only with devices assigned to a particular site, or only to virtual machines in a certain cluster.
6+
7+
See the [configuration rendering documentation](../../features/configuration-rendering.md) for more information.
8+
9+
## Fields
10+
11+
### Name
12+
13+
A unique human-friendly name.
14+
15+
### Weight
16+
17+
A numeric value which influences the order in which context data is merged. Contexts with a lower weight are merged before those with a higher weight.
18+
19+
### Data File
20+
21+
Template code may optionally be sourced from a remote [data file](../core/datafile.md), which is synchronized from a remote data source. When designating a data file, there is no need to specify template code: It will be populated automatically from the data file.
22+
23+
### Template Code
24+
25+
Jinja2 template code, if being defined locally rather than replicated from a data file.
26+
27+
### Environment Parameters
28+
29+
A dictionary of any additional parameters to pass when instantiating the [Jinja2 environment](https://jinja.palletsprojects.com/en/3.1.x/api/#jinja2.Environment). Jinja2 supports various optional parameters which can be used to modify its default behavior.

mkdocs.yml

+2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ nav:
7474
- Contacts: 'features/contacts.md'
7575
- Search: 'features/search.md'
7676
- Context Data: 'features/context-data.md'
77+
- Configuration Rendering: 'features/configuration-rendering.md'
7778
- Change Logging: 'features/change-logging.md'
7879
- Journaling: 'features/journaling.md'
7980
- Auth & Permissions: 'features/authentication-permissions.md'
@@ -196,6 +197,7 @@ nav:
196197
- Extras:
197198
- Branch: 'models/extras/branch.md'
198199
- ConfigContext: 'models/extras/configcontext.md'
200+
- ConfigTemplate: 'models/extras/configtemplate.md'
199201
- CustomField: 'models/extras/customfield.md'
200202
- CustomLink: 'models/extras/customlink.md'
201203
- ExportTemplate: 'models/extras/exporttemplate.md'

netbox/dcim/api/serializers.py

+7-5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from dcim.choices import *
1010
from dcim.constants import *
1111
from dcim.models import *
12+
from extras.api.nested_serializers import NestedConfigTemplateSerializer
1213
from ipam.api.nested_serializers import (
1314
NestedASNSerializer, NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer,
1415
NestedVRFSerializer,
@@ -605,8 +606,8 @@ class DeviceRoleSerializer(NetBoxModelSerializer):
605606
class Meta:
606607
model = DeviceRole
607608
fields = [
608-
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'description', 'tags', 'custom_fields',
609-
'created', 'last_updated', 'device_count', 'virtualmachine_count',
609+
'id', 'url', 'display', 'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
610+
'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
610611
]
611612

612613

@@ -619,8 +620,8 @@ class PlatformSerializer(NetBoxModelSerializer):
619620
class Meta:
620621
model = Platform
621622
fields = [
622-
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description',
623-
'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
623+
'id', 'url', 'display', 'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args',
624+
'description', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count', 'virtualmachine_count',
624625
]
625626

626627

@@ -651,14 +652,15 @@ class DeviceSerializer(NetBoxModelSerializer):
651652
cluster = NestedClusterSerializer(required=False, allow_null=True)
652653
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None)
653654
vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
655+
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
654656

655657
class Meta:
656658
model = Device
657659
fields = [
658660
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
659661
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
660662
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
661-
'comments', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
663+
'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
662664
]
663665

664666
@swagger_serializer_method(serializer_or_field=NestedDeviceSerializer)

netbox/dcim/api/views.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ class InventoryItemTemplateViewSet(NetBoxModelViewSet):
366366
#
367367

368368
class DeviceRoleViewSet(NetBoxModelViewSet):
369-
queryset = DeviceRole.objects.prefetch_related('tags').annotate(
369+
queryset = DeviceRole.objects.prefetch_related('config_template', 'tags').annotate(
370370
device_count=count_related(Device, 'device_role'),
371371
virtualmachine_count=count_related(VirtualMachine, 'role')
372372
)
@@ -379,7 +379,7 @@ class DeviceRoleViewSet(NetBoxModelViewSet):
379379
#
380380

381381
class PlatformViewSet(NetBoxModelViewSet):
382-
queryset = Platform.objects.prefetch_related('tags').annotate(
382+
queryset = Platform.objects.prefetch_related('config_template', 'tags').annotate(
383383
device_count=count_related(Device, 'platform'),
384384
virtualmachine_count=count_related(VirtualMachine, 'platform')
385385
)

netbox/dcim/filtersets.py

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django.utils.translation import gettext as _
44

55
from extras.filtersets import LocalConfigContextFilterSet
6+
from extras.models import ConfigTemplate
67
from ipam.models import ASN, L2VPN, IPAddress, VRF
78
from netbox.filtersets import (
89
BaseFilterSet, ChangeLoggedModelFilterSet, OrganizationalModelFilterSet, NetBoxModelFilterSet,
@@ -776,6 +777,10 @@ def search(self, queryset, name, value):
776777

777778

778779
class DeviceRoleFilterSet(OrganizationalModelFilterSet):
780+
config_template_id = django_filters.ModelMultipleChoiceFilter(
781+
queryset=ConfigTemplate.objects.all(),
782+
label=_('Config template (ID)'),
783+
)
779784

780785
class Meta:
781786
model = DeviceRole
@@ -794,6 +799,10 @@ class PlatformFilterSet(OrganizationalModelFilterSet):
794799
to_field_name='slug',
795800
label=_('Manufacturer (slug)'),
796801
)
802+
config_template_id = django_filters.ModelMultipleChoiceFilter(
803+
queryset=ConfigTemplate.objects.all(),
804+
label=_('Config template (ID)'),
805+
)
797806

798807
class Meta:
799808
model = Platform
@@ -936,6 +945,10 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
936945
method='_virtual_chassis_member',
937946
label=_('Is a virtual chassis member')
938947
)
948+
config_template_id = django_filters.ModelMultipleChoiceFilter(
949+
queryset=ConfigTemplate.objects.all(),
950+
label=_('Config template (ID)'),
951+
)
939952
console_ports = django_filters.BooleanFilter(
940953
method='_console_ports',
941954
label=_('Has console ports'),

netbox/dcim/forms/bulk_edit.py

+18-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from dcim.choices import *
77
from dcim.constants import *
88
from dcim.models import *
9+
from extras.models import ConfigTemplate
910
from ipam.models import ASN, VLAN, VLANGroup, VRF
1011
from netbox.forms import NetBoxModelBulkEditForm
1112
from tenancy.models import Tenant
@@ -454,16 +455,20 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm):
454455
widget=BulkEditNullBooleanSelect,
455456
label=_('VM role')
456457
)
458+
config_template = DynamicModelChoiceField(
459+
queryset=ConfigTemplate.objects.all(),
460+
required=False
461+
)
457462
description = forms.CharField(
458463
max_length=200,
459464
required=False
460465
)
461466

462467
model = DeviceRole
463468
fieldsets = (
464-
(None, ('color', 'vm_role', 'description')),
469+
(None, ('color', 'vm_role', 'config_template', 'description')),
465470
)
466-
nullable_fields = ('color', 'description')
471+
nullable_fields = ('color', 'config_template', 'description')
467472

468473

469474
class PlatformBulkEditForm(NetBoxModelBulkEditForm):
@@ -475,17 +480,20 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm):
475480
max_length=50,
476481
required=False
477482
)
478-
# TODO: Bulk edit support for napalm_args
483+
config_template = DynamicModelChoiceField(
484+
queryset=ConfigTemplate.objects.all(),
485+
required=False
486+
)
479487
description = forms.CharField(
480488
max_length=200,
481489
required=False
482490
)
483491

484492
model = Platform
485493
fieldsets = (
486-
(None, ('manufacturer', 'napalm_driver', 'description')),
494+
(None, ('manufacturer', 'config_template', 'napalm_driver', 'description')),
487495
)
488-
nullable_fields = ('manufacturer', 'napalm_driver', 'description')
496+
nullable_fields = ('manufacturer', 'config_template', 'napalm_driver', 'description')
489497

490498

491499
class DeviceBulkEditForm(NetBoxModelBulkEditForm):
@@ -540,6 +548,10 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
540548
max_length=200,
541549
required=False
542550
)
551+
config_template = DynamicModelChoiceField(
552+
queryset=ConfigTemplate.objects.all(),
553+
required=False
554+
)
543555
comments = CommentField(
544556
widget=forms.Textarea,
545557
label='Comments'
@@ -550,6 +562,7 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm):
550562
('Device', ('device_role', 'status', 'tenant', 'platform', 'description')),
551563
('Location', ('site', 'location')),
552564
('Hardware', ('manufacturer', 'device_type', 'airflow', 'serial')),
565+
('Configuration', ('config_template',)),
553566
)
554567
nullable_fields = (
555568
'location', 'tenant', 'platform', 'serial', 'airflow', 'description', 'comments',

netbox/dcim/forms/bulk_import.py

+24-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dcim.choices import *
99
from dcim.constants import *
1010
from dcim.models import *
11+
from extras.models import ConfigTemplate
1112
from ipam.models import VRF
1213
from netbox.forms import NetBoxModelImportForm
1314
from tenancy.models import Tenant
@@ -307,11 +308,17 @@ class Meta:
307308

308309

309310
class DeviceRoleImportForm(NetBoxModelImportForm):
311+
config_template = CSVModelChoiceField(
312+
queryset=ConfigTemplate.objects.all(),
313+
to_field_name='name',
314+
required=False,
315+
help_text=_('Config template')
316+
)
310317
slug = SlugField()
311318

312319
class Meta:
313320
model = DeviceRole
314-
fields = ('name', 'slug', 'color', 'vm_role', 'description', 'tags')
321+
fields = ('name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags')
315322
help_texts = {
316323
'color': mark_safe(_('RGB color in hexadecimal (e.g. <code>00ff00</code>)')),
317324
}
@@ -325,10 +332,18 @@ class PlatformImportForm(NetBoxModelImportForm):
325332
to_field_name='name',
326333
help_text=_('Limit platform assignments to this manufacturer')
327334
)
335+
config_template = CSVModelChoiceField(
336+
queryset=ConfigTemplate.objects.all(),
337+
to_field_name='name',
338+
required=False,
339+
help_text=_('Config template')
340+
)
328341

329342
class Meta:
330343
model = Platform
331-
fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description', 'tags')
344+
fields = (
345+
'name', 'slug', 'manufacturer', 'config_template', 'napalm_driver', 'napalm_args', 'description', 'tags',
346+
)
332347

333348

334349
class BaseDeviceImportForm(NetBoxModelImportForm):
@@ -434,12 +449,18 @@ class DeviceImportForm(BaseDeviceImportForm):
434449
required=False,
435450
help_text=_('Airflow direction')
436451
)
452+
config_template = CSVModelChoiceField(
453+
queryset=ConfigTemplate.objects.all(),
454+
to_field_name='name',
455+
required=False,
456+
help_text=_('Config template')
457+
)
437458

438459
class Meta(BaseDeviceImportForm.Meta):
439460
fields = [
440461
'name', 'device_role', 'tenant', 'manufacturer', 'device_type', 'platform', 'serial', 'asset_tag', 'status',
441462
'site', 'location', 'rack', 'position', 'face', 'parent', 'device_bay', 'airflow', 'virtual_chassis',
442-
'vc_position', 'vc_priority', 'cluster', 'description', 'comments', 'tags',
463+
'vc_position', 'vc_priority', 'cluster', 'description', 'config_template', 'comments', 'tags',
443464
]
444465

445466
def __init__(self, data=None, *args, **kwargs):

0 commit comments

Comments
 (0)