From 1471aff91f2ec9fad94b8b4fe5d126dde2ce797b Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 11 Aug 2023 23:09:18 +0530 Subject: [PATCH] adds config template to vm model #12461 --- .../virtualization/virtualmachine.html | 4 ++ .../virtualmachine/render_config.html | 47 +++++++++++++++++++ netbox/virtualization/api/serializers.py | 5 +- netbox/virtualization/filtersets.py | 5 ++ netbox/virtualization/forms/bulk_edit.py | 8 +++- netbox/virtualization/forms/bulk_import.py | 9 +++- netbox/virtualization/forms/filtersets.py | 8 +++- netbox/virtualization/forms/model_forms.py | 9 +++- .../0036_virtualmachine_config_template.py | 20 ++++++++ .../virtualization/models/virtualmachines.py | 18 +++++++ .../virtualization/tables/virtualmachines.py | 5 +- netbox/virtualization/views.py | 37 ++++++++++++++- 12 files changed, 167 insertions(+), 8 deletions(-) create mode 100644 netbox/templates/virtualization/virtualmachine/render_config.html create mode 100644 netbox/virtualization/migrations/0036_virtualmachine_config_template.py diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 04e038b92d..5dfd33128c 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -43,6 +43,10 @@
{{ object.tenant|linkify|placeholder }} + + Config Template + {{ object.config_template|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 0000000000..d83152113c --- /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{{ config_template|linkify|placeholder }}
Data Source{{ config_template.data_file.source|linkify|placeholder }}
Data File{{ config_template.data_file|linkify|placeholder }}
+
+
+
+
+
+
Context Data
+
{{ 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 693bb362fd..c9fa559aa7 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -5,6 +5,7 @@ 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 Meta: 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 cf716ca32c..571dbe64b2 100644 --- a/netbox/virtualization/filtersets.py +++ b/netbox/virtualization/filtersets.py @@ -5,6 +5,7 @@ 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 cc281a4f7e..a33ffac537 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -4,6 +4,7 @@ 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 19f718f035..b0c7282c83 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -2,6 +2,7 @@ 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 cd12696458..99ac0cb774 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -3,6 +3,7 @@ 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 0c8c98f9f2..51e53d31c2 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -5,6 +5,7 @@ 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 Meta: 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 0000000000..f3f03ce332 --- /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 1cacd8adc3..441bb182d1 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 @@ def primary_ip(self): 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 cece6f0921..73f2c414df 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 a4474610a2..e859710762 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.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 @@ def get_children(self, request, parent): ) +@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 )