From 1471aff91f2ec9fad94b8b4fe5d126dde2ce797b Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Fri, 11 Aug 2023 23:09:18 +0530 Subject: [PATCH 1/5] 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 04e038b92d3..5dfd33128c5 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 00000000000..d83152113c4 --- /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 693bb362fd4..c9fa559aa70 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 cf716ca32c8..571dbe64b2d 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 cc281a4f7e5..a33ffac5376 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 19f718f035a..b0c7282c83f 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 cd126964588..99ac0cb774d 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 0c8c98f9f29..51e53d31c2f 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 00000000000..f3f03ce3324 --- /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 1cacd8adc37..441bb182d1c 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 cece6f09215..73f2c414dfb 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 a4474610a24..e8597107623 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 ) From adbc4b7e0205b458adc024300971888e7d373ba6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 14 Aug 2023 14:52:27 -0400 Subject: [PATCH 2/5] Add translation tags; collapse config data --- .../virtualization/virtualmachine.html | 2 +- .../virtualmachine/render_config.html | 41 +++++++++++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html index 5dfd33128c5..27f5ea11498 100644 --- a/netbox/templates/virtualization/virtualmachine.html +++ b/netbox/templates/virtualization/virtualmachine.html @@ -44,7 +44,7 @@
- Config Template + {% trans "Config Template" %} {{ object.config_template|linkify|placeholder }} diff --git a/netbox/templates/virtualization/virtualmachine/render_config.html b/netbox/templates/virtualization/virtualmachine/render_config.html index d83152113c4..7b638199b95 100644 --- a/netbox/templates/virtualization/virtualmachine/render_config.html +++ b/netbox/templates/virtualization/virtualmachine/render_config.html @@ -1,25 +1,26 @@ {% extends 'virtualization/virtualmachine/base.html' %} {% load static %} +{% load i18n %} -{% block title %}{{ object }} - Config{% endblock %} +{% block title %}{{ object }} - {% trans "Config" %}{% endblock %} {% block content %}
-
Config Template
+
{% trans "Config Template" %}
- + - + - +
Config Template{% trans "Config Template" %} {{ config_template|linkify|placeholder }}
Data Source{% trans "Data Source" %} {{ config_template.data_file.source|linkify|placeholder }}
Data File{% trans "Data File" %} {{ config_template.data_file|linkify|placeholder }}
@@ -28,20 +29,42 @@
Config Template
-
Context Data
-
{{ context_data|pprint }}
+
+
+
+

+ +

+
+
+
{{ context_data|pprint }}
+
+
+
+
+
+
+ +
{% trans "Rendered Config" %}
+
{% if config_template %}
{{ rendered_config }}
{% else %} -
No configuration template found
+
{% trans "No configuration template found" %}
{% endif %}
-{% endblock %} \ No newline at end of file +{% endblock %} From 73935be7640c86f01c807d9d48fd9dda7538219d Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 14 Aug 2023 15:03:13 -0400 Subject: [PATCH 3/5] i18n cleanup --- netbox/virtualization/forms/bulk_import.py | 1 + netbox/virtualization/forms/model_forms.py | 6 ++++-- netbox/virtualization/tables/virtualmachines.py | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index b0c7282c83f..04fe2d7ae3f 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -128,6 +128,7 @@ class VirtualMachineImportForm(NetBoxModelImportForm): queryset=ConfigTemplate.objects.all(), to_field_name='name', required=False, + label=_('Config template'), help_text=_('Config template') ) diff --git a/netbox/virtualization/forms/model_forms.py b/netbox/virtualization/forms/model_forms.py index 51e53d31c2f..21dbc895a76 100644 --- a/netbox/virtualization/forms/model_forms.py +++ b/netbox/virtualization/forms/model_forms.py @@ -208,7 +208,8 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): ) config_template = DynamicModelChoiceField( queryset=ConfigTemplate.objects.all(), - required=False + required=False, + label=_('Config template') ) comments = CommentField() @@ -225,7 +226,8 @@ 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', 'config_template', + 'primary_ip6', 'vcpus', 'memory', 'disk', 'description', 'comments', 'tags', 'local_context_data', + 'config_template', ] def __init__(self, *args, **kwargs): diff --git a/netbox/virtualization/tables/virtualmachines.py b/netbox/virtualization/tables/virtualmachines.py index 73f2c414dfb..f8473df1e8a 100644 --- a/netbox/virtualization/tables/virtualmachines.py +++ b/netbox/virtualization/tables/virtualmachines.py @@ -85,6 +85,7 @@ class VirtualMachineTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable) verbose_name=_('Interfaces') ) config_template = tables.Column( + verbose_name=_('Config Template'), linkify=True ) @@ -93,7 +94,7 @@ class Meta(NetBoxTable.Meta): 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', 'config_template', 'tags', 'created', 'last_updated', + 'config_template', 'contacts', 'tags', 'created', 'last_updated', ) default_columns = ( 'pk', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'vcpus', 'memory', 'disk', 'primary_ip', From c0fae306847a4606e93c8d17c2c6385e4e380be5 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 14 Aug 2023 15:08:17 -0400 Subject: [PATCH 4/5] Establish parity with DeviceRenderConfigView --- netbox/virtualization/views.py | 49 +++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index e8597107623..9c7748cbd17 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -4,6 +4,7 @@ from django.contrib import messages from django.db import transaction from django.db.models import Prefetch, Sum +from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext as _ @@ -380,6 +381,17 @@ def get_children(self, request, parent): ) +@register_model_view(VirtualMachine, 'configcontext', path='config-context') +class VirtualMachineConfigContextView(ObjectConfigContextView): + queryset = VirtualMachine.objects.annotate_config_context_data() + base_template = 'virtualization/virtualmachine.html' + tab = ViewTab( + label=_('Config Context'), + permission='extras.view_configcontext', + weight=2000 + ) + + @register_model_view(VirtualMachine, 'render-config') class VirtualMachineRenderConfigView(generic.ObjectView): queryset = VirtualMachine.objects.all() @@ -387,15 +399,31 @@ class VirtualMachineRenderConfigView(generic.ObjectView): tab = ViewTab( label=_('Render Config'), permission='extras.view_configtemplate', - weight=2000 + weight=2100 ) + def get(self, request, **kwargs): + instance = self.get_object(**kwargs) + context = self.get_extra_context(request, instance) + + # If a direct export has been requested, return the rendered template content as a + # downloadable file. + if request.GET.get('export'): + response = HttpResponse(context['rendered_config'], content_type='text') + filename = f"{instance.name or 'config'}.txt" + response['Content-Disposition'] = f'attachment; filename="{filename}"' + return response + + return render(request, self.get_template_name(), { + 'object': instance, + 'tab': self.tab, + **context, + }) + def get_extra_context(self, request, instance): # Compile context data - context_data = { - 'virtualmachine': instance, - } - context_data.update(**instance.get_config_context()) + context_data = instance.get_config_context() + context_data.update({'virtualmachine': instance}) # Render the config template rendered_config = None @@ -413,17 +441,6 @@ def get_extra_context(self, request, instance): } -@register_model_view(VirtualMachine, 'configcontext', path='config-context') -class VirtualMachineConfigContextView(ObjectConfigContextView): - queryset = VirtualMachine.objects.annotate_config_context_data() - base_template = 'virtualization/virtualmachine.html' - tab = ViewTab( - label=_('Config Context'), - permission='extras.view_configcontext', - weight=2100 - ) - - @register_model_view(VirtualMachine, 'edit') class VirtualMachineEditView(generic.ObjectEditView): queryset = VirtualMachine.objects.all() From e53c4fac47725a049899c57e71841ef6abc236bc Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 14 Aug 2023 15:17:06 -0400 Subject: [PATCH 5/5] Move config_template field to RenderConfigMixin --- netbox/dcim/migrations/0170_configtemplate.py | 2 +- netbox/dcim/models/devices.py | 29 ++++++------------- netbox/dcim/models/mixins.py | 29 +++++++++++++++++++ .../0036_virtualmachine_config_template.py | 2 +- .../virtualization/models/virtualmachines.py | 21 ++------------ 5 files changed, 42 insertions(+), 41 deletions(-) diff --git a/netbox/dcim/migrations/0170_configtemplate.py b/netbox/dcim/migrations/0170_configtemplate.py index b1aac0ad202..f9508424d40 100644 --- a/netbox/dcim/migrations/0170_configtemplate.py +++ b/netbox/dcim/migrations/0170_configtemplate.py @@ -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', diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 6b8e9274360..857251caf41 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -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__ = ( @@ -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. @@ -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, @@ -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. diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py index f787c8e97b1..95f6d41fe95 100644 --- a/netbox/dcim/models/mixins.py +++ b/netbox/dcim/models/mixins.py @@ -4,6 +4,11 @@ from dcim.choices import * from utilities.utils import to_grams +__all__ = ( + 'RenderConfigMixin', + 'WeightMixin', +) + class WeightMixin(models.Model): weight = models.DecimalField( @@ -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 diff --git a/netbox/virtualization/migrations/0036_virtualmachine_config_template.py b/netbox/virtualization/migrations/0036_virtualmachine_config_template.py index f3f03ce3324..0456eea81d4 100644 --- a/netbox/virtualization/migrations/0036_virtualmachine_config_template.py +++ b/netbox/virtualization/migrations/0036_virtualmachine_config_template.py @@ -15,6 +15,6 @@ class Migration(migrations.Migration): 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'), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='%(class)ss', to='extras.configtemplate'), ), ] diff --git a/netbox/virtualization/models/virtualmachines.py b/netbox/virtualization/models/virtualmachines.py index 441bb182d1c..eb6c2a8b0dd 100644 --- a/netbox/virtualization/models/virtualmachines.py +++ b/netbox/virtualization/models/virtualmachines.py @@ -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 @@ -25,7 +26,7 @@ ) -class VirtualMachine(ContactsMixin, PrimaryModel, ConfigContextModel): +class VirtualMachine(ContactsMixin, RenderConfigMixin, ConfigContextModel, PrimaryModel): """ A virtual machine which runs inside a Cluster. """ @@ -123,13 +124,6 @@ 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( @@ -241,17 +235,6 @@ 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(