adds config template to vm model #12461

This commit is contained in:
Abhimanyu Saharan 2023-08-11 23:09:18 +05:30
parent ff59845821
commit 1471aff91f
12 changed files with 167 additions and 8 deletions

View File

@ -43,6 +43,10 @@
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
<tr>
<th scope="row">Config Template</th>
<td>{{ object.config_template|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Primary IPv4" %}</th>
<td>

View File

@ -0,0 +1,47 @@
{% extends 'virtualization/virtualmachine/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>{{ config_template|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Data Source</th>
<td>{{ config_template.data_file.source|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">Data File</th>
<td>{{ 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 config_template %}
<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

@ -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 = []

View File

@ -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

View File

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

View File

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

View File

@ -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)

View File

@ -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):

View File

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

View File

@ -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(

View File

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

View File

@ -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
)