Skip to content

Commit

Permalink
Adds config template to vm model (#13450)
Browse files Browse the repository at this point in the history
* adds config template to vm model #12461

* Add translation tags; collapse config data

* i18n cleanup

* Establish parity with DeviceRenderConfigView

* Move config_template field to RenderConfigMixin

---------

Co-authored-by: Jeremy Stretch <jstretch@netboxlabs.com>
  • Loading branch information
abhi1693 and jeremystretch authored Aug 14, 2023
1 parent 8593715 commit 752e26c
Show file tree
Hide file tree
Showing 15 changed files with 232 additions and 28 deletions.
2 changes: 1 addition & 1 deletion netbox/dcim/migrations/0170_configtemplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='device',
name='config_template',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='extras.configtemplate'),
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss', to='extras.configtemplate'),
),
migrations.AddField(
model_name='devicerole',
Expand Down
29 changes: 9 additions & 20 deletions netbox/dcim/models/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
from utilities.tracking import TrackingModelMixin
from .device_components import *
from .mixins import WeightMixin
from .mixins import RenderConfigMixin, WeightMixin


__all__ = (
Expand Down Expand Up @@ -525,7 +525,14 @@ def update_interface_bridges(device, interface_templates, module=None):
interface.save()


class Device(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, ConfigContextModel, TrackingModelMixin):
class Device(
ContactsMixin,
ImageAttachmentsMixin,
RenderConfigMixin,
ConfigContextModel,
TrackingModelMixin,
PrimaryModel
):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
Expand Down Expand Up @@ -686,13 +693,6 @@ class Device(ContactsMixin, ImageAttachmentsMixin, PrimaryModel, ConfigContextMo
validators=[MaxValueValidator(255)],
help_text=_('Virtual chassis master election priority')
)
config_template = models.ForeignKey(
to='extras.ConfigTemplate',
on_delete=models.PROTECT,
related_name='devices',
blank=True,
null=True
)
latitude = models.DecimalField(
verbose_name=_('latitude'),
max_digits=8,
Expand Down Expand Up @@ -1070,17 +1070,6 @@ def primary_ip(self):
def interfaces_count(self):
return self.vc_interfaces().count()

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

def get_vc_master(self):
"""
If this Device is a VirtualChassis member, return the VC master. Otherwise, return None.
Expand Down
29 changes: 29 additions & 0 deletions netbox/dcim/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
from dcim.choices import *
from utilities.utils import to_grams

__all__ = (
'RenderConfigMixin',
'WeightMixin',
)


class WeightMixin(models.Model):
weight = models.DecimalField(
Expand Down Expand Up @@ -44,3 +49,27 @@ def clean(self):
# Validate weight and weight_unit
if self.weight and not self.weight_unit:
raise ValidationError(_("Must specify a unit when setting a weight"))


class RenderConfigMixin(models.Model):
config_template = models.ForeignKey(
to='extras.ConfigTemplate',
on_delete=models.PROTECT,
related_name='%(class)ss',
blank=True,
null=True
)

class Meta:
abstract = True

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
4 changes: 4 additions & 0 deletions netbox/templates/virtualization/virtualmachine.html
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ <h5 class="card-header">
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Config Template" %}</th>
<td>{{ object.config_template|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Primary IPv4" %}</th>
<td>
Expand Down
70 changes: 70 additions & 0 deletions netbox/templates/virtualization/virtualmachine/render_config.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{% extends 'virtualization/virtualmachine/base.html' %}
{% load static %}
{% load i18n %}

{% block title %}{{ object }} - {% trans "Config" %}{% endblock %}

{% block content %}
<div class="row mb-3">
<div class="col-5">
<div class="card">
<h5 class="card-header">{% trans "Config Template" %}</h5>
<div class="card-body">
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Config Template" %}</th>
<td>{{ config_template|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Data Source" %}</th>
<td>{{ config_template.data_file.source|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Data File" %}</th>
<td>{{ config_template.data_file|linkify|placeholder }}</td>
</tr>
</table>
</div>
</div>
</div>
<div class="col-7">
<div class="card">
<div class="accordion accordion-flush" id="renderConfig">
<div class="card-body">
<div class="accordion-item">
<h2 class="accordion-header" id="renderConfigHeading">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapsedRenderConfig" aria-expanded="false" aria-controls="collapsedRenderConfig">
{% trans "Context Data" %}
</button>
</h2>
<div id="collapsedRenderConfig" class="accordion-collapse collapse" aria-labelledby="renderConfigHeading" data-bs-parent="#renderConfig">
<div class="accordion-body">
<pre class="card-body">{{ context_data|pprint }}</pre>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="card">
<div class="card-header">
<div class="float-end">
<a href="?export=True" class="btn btn-sm btn-primary" role="button">
<i class="mdi mdi-download" aria-hidden="true"></i> {% trans "Download" %}
</a>
</div>
<h5>{% trans "Rendered Config" %}</h5>
</div>
{% if config_template %}
<pre class="card-body">{{ rendered_config }}</pre>
{% else %}
<div class="card-body text-muted">{% trans "No configuration template found" %}</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}
5 changes: 4 additions & 1 deletion netbox/virtualization/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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)
Expand All @@ -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 = []

Expand Down
5 changes: 5 additions & 0 deletions netbox/virtualization/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion netbox/virtualization/forms/bulk_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down
10 changes: 9 additions & 1 deletion netbox/virtualization/forms/bulk_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -123,12 +124,19 @@ class VirtualMachineImportForm(NetBoxModelImportForm):
to_field_name='name',
help_text=_('Assigned platform')
)
config_template = CSVModelChoiceField(
queryset=ConfigTemplate.objects.all(),
to_field_name='name',
required=False,
label=_('Config template'),
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',
)


Expand Down
8 changes: 7 additions & 1 deletion netbox/virtualization/forms/filtersets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')),
)
Expand Down Expand Up @@ -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)


Expand Down
9 changes: 8 additions & 1 deletion netbox/virtualization/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -205,13 +206,18 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm):
required=False,
label=''
)
config_template = DynamicModelChoiceField(
queryset=ConfigTemplate.objects.all(),
required=False,
label=_('Config template')
)
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',)),
)
Expand All @@ -221,6 +227,7 @@ class Meta:
fields = [
'name', 'status', 'site', 'cluster', 'device', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4',
'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', 'tags', 'local_context_data',
'config_template',
]

def __init__(self, *args, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
@@ -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='%(class)ss', to='extras.configtemplate'),
),
]
3 changes: 2 additions & 1 deletion netbox/virtualization/models/virtualmachines.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.utils.translation import gettext_lazy as _

from dcim.models import BaseInterface
from dcim.models.mixins import RenderConfigMixin
from extras.models import ConfigContextModel
from extras.querysets import ConfigContextModelQuerySet
from netbox.config import get_config
Expand All @@ -25,7 +26,7 @@
)


class VirtualMachine(ContactsMixin, PrimaryModel, ConfigContextModel):
class VirtualMachine(ContactsMixin, RenderConfigMixin, ConfigContextModel, PrimaryModel):
"""
A virtual machine which runs inside a Cluster.
"""
Expand Down
Loading

0 comments on commit 752e26c

Please sign in to comment.