diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html
index 04e038b92..5dfd33128 100644
--- a/netbox/templates/virtualization/virtualmachine.html
+++ b/netbox/templates/virtualization/virtualmachine.html
@@ -43,6 +43,10 @@
{{ object.tenant|linkify|placeholder }}
+
{% trans "Primary IPv4" %} |
diff --git a/netbox/templates/virtualization/virtualmachine/render_config.html b/netbox/templates/virtualization/virtualmachine/render_config.html
new file mode 100644
index 000000000..d83152113
--- /dev/null
+++ b/netbox/templates/virtualization/virtualmachine/render_config.html
@@ -0,0 +1,47 @@
+{% extends 'virtualization/virtualmachine/base.html' %}
+{% load static %}
+
+{% block title %}{{ object }} - Config{% endblock %}
+
+{% block content %}
+
+
+
+
+
+
+
+ Config Template |
+ {{ config_template|linkify|placeholder }} |
+
+
+ Data Source |
+ {{ config_template.data_file.source|linkify|placeholder }} |
+
+
+ Data File |
+ {{ config_template.data_file|linkify|placeholder }} |
+
+
+
+
+
+
+
+
+ {{ context_data|pprint }}
+
+
+
+
+
+
+ {% if config_template %}
+ {{ rendered_config }}
+ {% else %}
+ No configuration template found
+ {% endif %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py
index 693bb362f..c9fa559aa 100644
--- a/netbox/virtualization/api/serializers.py
+++ b/netbox/virtualization/api/serializers.py
@@ -5,6 +5,7 @@ from dcim.api.nested_serializers import (
NestedDeviceSerializer, NestedDeviceRoleSerializer, NestedPlatformSerializer, NestedSiteSerializer,
)
from dcim.choices import InterfaceModeChoices
+from extras.api.nested_serializers import NestedConfigTemplateSerializer
from ipam.api.nested_serializers import (
NestedIPAddressSerializer, NestedL2VPNTerminationSerializer, NestedVLANSerializer, NestedVRFSerializer,
)
@@ -79,6 +80,7 @@ class VirtualMachineSerializer(NetBoxModelSerializer):
primary_ip = NestedIPAddressSerializer(read_only=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
+ config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
# Counter fields
interface_count = serializers.IntegerField(read_only=True)
@@ -88,7 +90,8 @@ class VirtualMachineSerializer(NetBoxModelSerializer):
fields = [
'id', 'url', 'display', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'platform',
'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments',
- 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated', 'interface_count',
+ 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created', 'last_updated',
+ 'interface_count',
]
validators = []
diff --git a/netbox/virtualization/filtersets.py b/netbox/virtualization/filtersets.py
index cf716ca32..571dbe64b 100644
--- a/netbox/virtualization/filtersets.py
+++ b/netbox/virtualization/filtersets.py
@@ -5,6 +5,7 @@ from django.utils.translation import gettext as _
from dcim.filtersets import CommonInterfaceFilterSet
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
from extras.filtersets import LocalConfigContextFilterSet
+from extras.models import ConfigTemplate
from netbox.filtersets import OrganizationalModelFilterSet, NetBoxModelFilterSet
from tenancy.filtersets import TenancyFilterSet, ContactModelFilterSet
from utilities.filters import MultiValueCharFilter, MultiValueMACAddressFilter, TreeNodeMultipleChoiceFilter
@@ -228,6 +229,10 @@ class VirtualMachineFilterSet(
method='_has_primary_ip',
label=_('Has a primary IP'),
)
+ config_template_id = django_filters.ModelMultipleChoiceFilter(
+ queryset=ConfigTemplate.objects.all(),
+ label=_('Config template (ID)'),
+ )
class Meta:
model = VirtualMachine
diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py
index cc281a4f7..a33ffac53 100644
--- a/netbox/virtualization/forms/bulk_edit.py
+++ b/netbox/virtualization/forms/bulk_edit.py
@@ -4,6 +4,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfaceModeChoices
from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
+from extras.models import ConfigTemplate
from ipam.models import VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelBulkEditForm
from tenancy.models import Tenant
@@ -174,12 +175,17 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm):
max_length=200,
required=False
)
+ config_template = DynamicModelChoiceField(
+ queryset=ConfigTemplate.objects.all(),
+ required=False
+ )
comments = CommentField()
model = VirtualMachine
fieldsets = (
(None, ('site', 'cluster', 'device', 'status', 'role', 'tenant', 'platform', 'description')),
- (_('Resources'), ('vcpus', 'memory', 'disk'))
+ (_('Resources'), ('vcpus', 'memory', 'disk')),
+ ('Configuration', ('config_template',)),
)
nullable_fields = (
'site', 'cluster', 'device', 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'description', 'comments',
diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py
index 19f718f03..b0c7282c8 100644
--- a/netbox/virtualization/forms/bulk_import.py
+++ b/netbox/virtualization/forms/bulk_import.py
@@ -2,6 +2,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfaceModeChoices
from dcim.models import Device, DeviceRole, Platform, Site
+from extras.models import ConfigTemplate
from ipam.models import VRF
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
@@ -123,12 +124,18 @@ class VirtualMachineImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Assigned platform')
)
+ config_template = CSVModelChoiceField(
+ queryset=ConfigTemplate.objects.all(),
+ to_field_name='name',
+ required=False,
+ help_text=_('Config template')
+ )
class Meta:
model = VirtualMachine
fields = (
'name', 'status', 'role', 'site', 'cluster', 'device', 'tenant', 'platform', 'vcpus', 'memory', 'disk',
- 'description', 'comments', 'tags',
+ 'description', 'config_template', 'comments', 'tags',
)
diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py
index cd1269645..99ac0cb77 100644
--- a/netbox/virtualization/forms/filtersets.py
+++ b/netbox/virtualization/forms/filtersets.py
@@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.models import Device, DeviceRole, Platform, Region, Site, SiteGroup
from extras.forms import LocalConfigContextFilterForm
+from extras.models import ConfigTemplate
from ipam.models import L2VPN, VRF
from netbox.forms import NetBoxModelFilterSetForm
from tenancy.forms import ContactModelFilterForm, TenancyFilterForm
@@ -93,7 +94,7 @@ class VirtualMachineFilterForm(
(None, ('q', 'filter_id', 'tag')),
(_('Cluster'), ('cluster_group_id', 'cluster_type_id', 'cluster_id', 'device_id')),
(_('Location'), ('region_id', 'site_group_id', 'site_id')),
- (_('Attributes'), ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')),
+ (_('Attributes'), ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'config_template_id', 'local_context_data')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
@@ -170,6 +171,11 @@ class VirtualMachineFilterForm(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
+ config_template_id = DynamicModelMultipleChoiceField(
+ queryset=ConfigTemplate.objects.all(),
+ required=False,
+ label=_('Config template')
+ )
tag = TagFilterField(model)
diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py
index 0c8c98f9f..51e53d31c 100644
--- a/netbox/virtualization/forms/model_forms.py
+++ b/netbox/virtualization/forms/model_forms.py
@@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.forms.common import InterfaceCommonForm
from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup
+from extras.models import ConfigTemplate
from ipam.models import IPAddress, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
@@ -205,13 +206,17 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
required=False,
label=''
)
+ config_template = DynamicModelChoiceField(
+ queryset=ConfigTemplate.objects.all(),
+ required=False
+ )
comments = CommentField()
fieldsets = (
(_('Virtual Machine'), ('name', 'role', 'status', 'description', 'tags')),
(_('Site/Cluster'), ('site', 'cluster', 'device')),
(_('Tenancy'), ('tenant_group', 'tenant')),
- (_('Management'), ('platform', 'primary_ip4', 'primary_ip6')),
+ (_('Management'), ('platform', 'primary_ip4', 'primary_ip6', 'config_template')),
(_('Resources'), ('vcpus', 'memory', 'disk')),
(_('Config Context'), ('local_context_data',)),
)
@@ -220,7 +225,7 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
model = VirtualMachine
fields = [
'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
- 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', 'tags', 'local_context_data',
+ 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', 'tags', 'local_context_data', 'config_template',
]
def __init__(self, *args, **kwargs):
diff --git a/netbox/virtualization/migrations/0036_virtualmachine_config_template.py b/netbox/virtualization/migrations/0036_virtualmachine_config_template.py
new file mode 100644
index 000000000..f3f03ce33
--- /dev/null
+++ b/netbox/virtualization/migrations/0036_virtualmachine_config_template.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.1.10 on 2023-08-11 17:16
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('extras', '0098_webhook_custom_field_data_webhook_tags'),
+ ('virtualization', '0035_virtualmachine_interface_count'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='virtualmachine',
+ name='config_template',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='virtual_machines', to='extras.configtemplate'),
+ ),
+ ]
diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py
index 1cacd8adc..441bb182d 100644
--- a/netbox/virtualization/models/virtualmachines.py
+++ b/netbox/virtualization/models/virtualmachines.py
@@ -123,6 +123,13 @@ class VirtualMachine(ContactsMixin, PrimaryModel, ConfigContextModel):
null=True,
verbose_name=_('disk (GB)')
)
+ config_template = models.ForeignKey(
+ to='extras.ConfigTemplate',
+ on_delete=models.PROTECT,
+ related_name='virtual_machines',
+ blank=True,
+ null=True
+ )
# Counter fields
interface_count = CounterCacheField(
@@ -234,6 +241,17 @@ class VirtualMachine(ContactsMixin, PrimaryModel, ConfigContextModel):
else:
return None
+ def get_config_template(self):
+ """
+ Return the appropriate ConfigTemplate (if any) for this Device.
+ """
+ if self.config_template:
+ return self.config_template
+ if self.role.config_template:
+ return self.role.config_template
+ if self.platform and self.platform.config_template:
+ return self.platform.config_template
+
class VMInterface(NetBoxModel, BaseInterface, TrackingModelMixin):
virtual_machine = models.ForeignKey(
diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py
index cece6f092..73f2c414d 100644
--- a/netbox/virtualization/tables/virtualmachines.py
+++ b/netbox/virtualization/tables/virtualmachines.py
@@ -84,13 +84,16 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable)
interface_count = tables.Column(
verbose_name=_('Interfaces')
)
+ config_template = tables.Column(
+ linkify=True
+ )
class Meta(NetBoxTable.Meta):
model = VirtualMachine
fields = (
'pk', 'id', 'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant', 'tenant_group', 'platform',
'vcpus', 'memory', 'disk', 'primary_ip4', 'primary_ip6', 'primary_ip', 'description', 'comments',
- 'contacts', 'tags', 'created', 'last_updated',
+ 'contacts', 'config_template', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip',
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index a4474610a..e85971076 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -1,3 +1,4 @@
+import traceback
from collections import defaultdict
from django.contrib import messages
@@ -6,6 +7,7 @@ from django.db.models import Prefetch, Sum
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext as _
+from jinja2.exceptions import TemplateError
from dcim.filtersets import DeviceFilterSet
from dcim.models import Device
@@ -378,6 +380,39 @@ class VirtualMachineInterfacesView(generic.ObjectChildrenView):
)
+@register_model_view(VirtualMachine, 'render-config')
+class VirtualMachineRenderConfigView(generic.ObjectView):
+ queryset = VirtualMachine.objects.all()
+ template_name = 'virtualization/virtualmachine/render_config.html'
+ tab = ViewTab(
+ label=_('Render Config'),
+ permission='extras.view_configtemplate',
+ weight=2000
+ )
+
+ def get_extra_context(self, request, instance):
+ # Compile context data
+ context_data = {
+ 'virtualmachine': instance,
+ }
+ context_data.update(**instance.get_config_context())
+
+ # Render the config template
+ rendered_config = None
+ if config_template := instance.get_config_template():
+ try:
+ rendered_config = config_template.render(context=context_data)
+ except TemplateError as e:
+ messages.error(request, f"An error occurred while rendering the template: {e}")
+ rendered_config = traceback.format_exc()
+
+ return {
+ 'config_template': config_template,
+ 'context_data': context_data,
+ 'rendered_config': rendered_config,
+ }
+
+
@register_model_view(VirtualMachine, 'configcontext', path='config-context')
class VirtualMachineConfigContextView(ObjectConfigContextView):
queryset = VirtualMachine.objects.annotate_config_context_data()
@@ -385,7 +420,7 @@ class VirtualMachineConfigContextView(ObjectConfigContextView):
tab = ViewTab(
label=_('Config Context'),
permission='extras.view_configcontext',
- weight=2000
+ weight=2100
)
|