From 0da113b72375dca3677d47ab8fc39730bb67a326 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Sep 2018 00:25:20 -0400 Subject: [PATCH 1/4] implemnted #2392 - local config context for devices and VMs --- docs/additional-features/context-data.md | 2 + netbox/dcim/api/serializers.py | 4 +- netbox/dcim/forms.py | 12 ++++- .../0063_device_local_config_context_data.py | 19 +++++++ netbox/dcim/models.py | 4 ++ netbox/dcim/urls.py | 2 + netbox/dcim/views.py | 15 +++++- netbox/extras/models.py | 4 ++ netbox/extras/views.py | 8 ++- .../device_edit_local_config_context.html | 11 ++++ .../extras/object_configcontext.html | 32 ++++++++++++ .../utilities/object_set_field_null.html | 9 ++++ ...tualmachine_edit_local_config_context.html | 11 ++++ netbox/utilities/views.py | 50 +++++++++++++++++++ netbox/virtualization/api/serializers.py | 2 + netbox/virtualization/forms.py | 13 ++++- ...irtualmachine_local_config_context_data.py | 19 +++++++ netbox/virtualization/models.py | 5 ++ netbox/virtualization/urls.py | 2 + netbox/virtualization/views.py | 15 +++++- 20 files changed, 232 insertions(+), 7 deletions(-) create mode 100644 netbox/dcim/migrations/0063_device_local_config_context_data.py create mode 100644 netbox/templates/dcim/device_edit_local_config_context.html create mode 100644 netbox/templates/utilities/object_set_field_null.html create mode 100644 netbox/templates/virtualization/virtualmachine_edit_local_config_context.html create mode 100644 netbox/virtualization/migrations/0008_virtualmachine_local_config_context_data.py diff --git a/docs/additional-features/context-data.md b/docs/additional-features/context-data.md index cd9f1ceaa..465b4d2dc 100644 --- a/docs/additional-features/context-data.md +++ b/docs/additional-features/context-data.md @@ -1,3 +1,5 @@ # Contextual Configuration Data Sometimes it is desirable to associate arbitrary data with a group of devices to aid in their configuration. For example, you might want to associate a set of syslog servers for all devices at a particular site. Context data enables the association of arbitrary data to devices and virtual machines grouped by region, site, role, platform, and/or tenant. Context data is arranged hierarchically, so that data with a higher weight can be entered to override more general lower-weight data. Multiple instances of data are automatically merged by NetBox to present a single dictionary for each object. + +Devices and Virtual Machines may also have a local config context defined. This local context will always overwrite the rendered config context objects for the Device/VM. This is useful in situations were the device requires a one-off value different from the rest of the environment. diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 0478932f7..3acafda8b 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -412,7 +412,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer): 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', + 'last_updated', 'local_config_context_data', ] validators = [] @@ -448,7 +448,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', - 'config_context', 'created', 'last_updated', + 'config_context', 'created', 'last_updated', 'local_config_context_data', ] def get_config_context(self, obj): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4e201639c..e5d64ff5d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -18,7 +18,7 @@ from utilities.forms import ( AnnotatedMultipleChoiceField, APISelect, add_blank_choice, ArrayFieldSelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, CommentField, ComponentForm, ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, FilterTreeNodeMultipleChoiceField, - FlexibleModelChoiceField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField, + FlexibleModelChoiceField, JSONField, Livesearch, SelectWithDisabled, SelectWithPK, SmallTextarea, SlugField, ) from virtualization.models import Cluster from .constants import ( @@ -920,6 +920,16 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.initial['rack'] = self.instance.parent_bay.device.rack_id +class DeviceLocalConfigContextForm(BootstrapMixin, forms.ModelForm): + local_config_context_data = JSONField() + + class Meta: + model = Device + fields = [ + 'local_config_context_data', + ] + + class BaseDeviceCSVForm(forms.ModelForm): device_role = forms.ModelChoiceField( queryset=DeviceRole.objects.all(), diff --git a/netbox/dcim/migrations/0063_device_local_config_context_data.py b/netbox/dcim/migrations/0063_device_local_config_context_data.py new file mode 100644 index 000000000..cbadde2ca --- /dev/null +++ b/netbox/dcim/migrations/0063_device_local_config_context_data.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.8 on 2018-09-16 02:01 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0062_interface_mtu'), + ] + + operations = [ + migrations.AddField( + model_name='device', + name='local_config_context_data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 19c75bdb9..bc3677b6e 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1287,6 +1287,10 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): images = GenericRelation( to='extras.ImageAttachment' ) + local_config_context_data = JSONField( + blank=True, + null=True, + ) objects = DeviceManager() tags = TaggableManager() diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 7345cdacd..51cedaa20 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -142,6 +142,8 @@ urlpatterns = [ url(r'^devices/(?P\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), url(r'^devices/(?P\d+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'), + url(r'^devices/(?P\d+)/config-context/edit-local/$', views.DeviceEditLocalConfigContextView.as_view(), name='device_edit_localconfigcontext'), + url(r'^devices/(?P\d+)/config-context/clear-local/$', views.DeviceClearLocalContextDataView.as_view(), name='device_delete_localconfigcontext'), url(r'^devices/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), url(r'^devices/(?P\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index eb7f71a25..42106b060 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -26,7 +26,7 @@ from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin, - ObjectDeleteView, ObjectEditView, ObjectListView, + ObjectDeleteView, ObjectEditView, ObjectListView, ObjectSetFieldNullView, ) from virtualization.models import VirtualMachine from . import filters, forms, tables @@ -983,6 +983,19 @@ class DeviceEditView(DeviceCreateView): permission_required = 'dcim.change_device' +class DeviceEditLocalConfigContextView(DeviceCreateView): + permission_required = 'dcim.change_device' + model_form = forms.DeviceLocalConfigContextForm + template_name = 'dcim/device_edit_local_config_context.html' + + +class DeviceClearLocalContextDataView(ObjectSetFieldNullView): + permission_required = 'dcim.change_device' + model = Device + field = 'local_config_context_data' + field_human_friendly_name = 'local config context' + + class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_device' model = Device diff --git a/netbox/extras/models.py b/netbox/extras/models.py index ad4fcdb18..467a96f64 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -716,6 +716,10 @@ class ConfigContextModel(models.Model): for context in ConfigContext.objects.get_for_object(self): data.update(context.data) + # If the object has local config context data defined, that data overwrites all rendered data + if self.local_config_context_data is not None: + data.update(self.local_config_context_data) + return data diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 90d0d698d..7be652ca0 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -106,9 +106,15 @@ class ObjectConfigContextView(View): obj = get_object_or_404(self.object_class, pk=pk) source_contexts = ConfigContext.objects.get_for_object(obj) + model_name = self.object_class._meta.model_name + app_label = self.object_class._meta.app_label return render(request, 'extras/object_configcontext.html', { - self.object_class._meta.model_name: obj, + model_name: obj, + 'obj': obj, + 'perm_string': '{}.change_{}'.format(app_label, model_name), + 'edit_url':'{}:{}_edit_localconfigcontext'.format(app_label, model_name), + 'delete_url':'{}:{}_delete_localconfigcontext'.format(app_label, model_name), 'rendered_context': obj.get_config_context(), 'source_contexts': source_contexts, 'base_template': self.base_template, diff --git a/netbox/templates/dcim/device_edit_local_config_context.html b/netbox/templates/dcim/device_edit_local_config_context.html new file mode 100644 index 000000000..de9c1c76f --- /dev/null +++ b/netbox/templates/dcim/device_edit_local_config_context.html @@ -0,0 +1,11 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
Local Config Context Data
+
+ {% render_field form.local_config_context_data %} +
+
+{% endblock %} diff --git a/netbox/templates/extras/object_configcontext.html b/netbox/templates/extras/object_configcontext.html index 81f8e1780..eab3df10b 100644 --- a/netbox/templates/extras/object_configcontext.html +++ b/netbox/templates/extras/object_configcontext.html @@ -16,6 +16,38 @@
+
+
+ Local Context +
+
+ {% if obj.local_config_context_data %} +
{{ obj.local_config_context_data|render_json }}
+ {% else %} + None + {% endif %} + + + The local config context overwrites all source contexts. + +
+ +
Source Contexts diff --git a/netbox/templates/utilities/object_set_field_null.html b/netbox/templates/utilities/object_set_field_null.html new file mode 100644 index 000000000..d1d58a9ed --- /dev/null +++ b/netbox/templates/utilities/object_set_field_null.html @@ -0,0 +1,9 @@ +{% extends 'utilities/confirmation_form.html' %} +{% load form_helpers %} + +{% block title %}Clear {{ field_human_friendly_name }}?{% endblock %} + +{% block message %} +

Are you sure you want to clear the {{ field_human_friendly_name }} on {{ obj_type }} {{ obj }}?

+ {% block message_extra %}{% endblock %} +{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine_edit_local_config_context.html b/netbox/templates/virtualization/virtualmachine_edit_local_config_context.html new file mode 100644 index 000000000..de9c1c76f --- /dev/null +++ b/netbox/templates/virtualization/virtualmachine_edit_local_config_context.html @@ -0,0 +1,11 @@ +{% extends 'utilities/obj_edit.html' %} +{% load form_helpers %} + +{% block form %} +
+
Local Config Context Data
+
+ {% render_field form.local_config_context_data %} +
+
+{% endblock %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index e11d681ef..40ef04dd4 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -844,6 +844,56 @@ class BulkComponentCreateView(GetReturnURLMixin, View): }) +class ObjectSetFieldNullView(ObjectDeleteView): + """ + Given a field name, set it to None (null) and save the object. + + field: The field to be nulled + field_friendly_name: Human friendly name for the field in the UI. + """ + template_name = 'utilities/object_set_field_null.html' + field_human_friendly_name = None + + def get(self, request, **kwargs): + + obj = self.get_object(kwargs) + form = ConfirmationForm(initial=request.GET) + + return render(request, self.template_name, { + 'obj': obj, + 'form': form, + 'obj_type': self.model._meta.verbose_name, + 'field_human_friendly_name': self.field_human_friendly_name, + 'return_url': self.get_return_url(request, obj), + }) + + def post(self, request, **kwargs): + + obj = self.get_object(kwargs) + form = ConfirmationForm(request.POST) + if form.is_valid(): + + setattr(obj, self.field, None) + obj.save() + + msg = 'Cleared {} on {} {}'.format(self.field_human_friendly_name, self.model._meta.verbose_name, obj) + messages.success(request, msg) + + return_url = form.cleaned_data.get('return_url') + if return_url is not None and is_safe_url(url=return_url, host=request.get_host()): + return redirect(return_url) + else: + return redirect(self.get_return_url(request, obj)) + + return render(request, self.template_name, { + 'obj': obj, + 'form': form, + 'obj_type': self.model._meta.verbose_name, + 'field_human_friendly_name': self.field_human_friendly_name, + 'return_url': self.get_return_url(request, obj), + }) + + @requires_csrf_token def server_error(request, template_name=ERROR_500_TEMPLATE_NAME): """ diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index 9dff223d3..faa2f3161 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -107,6 +107,7 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer): fields = [ 'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', + 'local_config_context_data', ] @@ -117,6 +118,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): fields = [ 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', + 'local_config_context_data', ] def get_config_context(self, obj): diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 10833234b..686864820 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -17,7 +17,8 @@ from tenancy.models import Tenant from utilities.forms import ( AnnotatedMultipleChoiceField, APISelect, APISelectMultiple, BootstrapMixin, BulkEditForm, BulkEditNullBooleanSelect, ChainedFieldsMixin, ChainedModelChoiceField, ChainedModelMultipleChoiceField, CommentField, ComponentForm, - ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, SlugField, SmallTextarea, add_blank_choice + ConfirmationForm, CSVChoiceField, ExpandableNameField, FilterChoiceField, JSONField, SlugField, SmallTextarea, + add_blank_choice ) from .constants import VM_STATUS_CHOICES from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -302,6 +303,16 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['primary_ip6'].widget.attrs['readonly'] = True +class VirtualMachineLocalConfigContextForm(BootstrapMixin, forms.ModelForm): + local_config_context_data = JSONField() + + class Meta: + model = VirtualMachine + fields = [ + 'local_config_context_data', + ] + + class VirtualMachineCSVForm(forms.ModelForm): status = CSVChoiceField( choices=VM_STATUS_CHOICES, diff --git a/netbox/virtualization/migrations/0008_virtualmachine_local_config_context_data.py b/netbox/virtualization/migrations/0008_virtualmachine_local_config_context_data.py new file mode 100644 index 000000000..e6f4b2bbf --- /dev/null +++ b/netbox/virtualization/migrations/0008_virtualmachine_local_config_context_data.py @@ -0,0 +1,19 @@ +# Generated by Django 2.0.8 on 2018-09-16 02:01 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('virtualization', '0007_change_logging'), + ] + + operations = [ + migrations.AddField( + model_name='virtualmachine', + name='local_config_context_data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), + ), + ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 119c9ee4f..9a55c10fd 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation +from django.contrib.postgres.fields import JSONField from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse @@ -244,6 +245,10 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): content_type_field='obj_type', object_id_field='obj_id' ) + local_config_context_data = JSONField( + blank=True, + null=True, + ) tags = TaggableManager() diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index b03b3bc0a..0af76bba2 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -49,6 +49,8 @@ urlpatterns = [ url(r'^virtual-machines/(?P\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), url(r'^virtual-machines/(?P\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), url(r'^virtual-machines/(?P\d+)/config-context/$', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), + url(r'^virtual-machines/(?P\d+)/config-context/edit-local/$', views.VirtualMachineEditLocalConfigContextView.as_view(), name='virtualmachine_edit_localconfigcontext'), + url(r'^virtual-machines/(?P\d+)/config-context/clear-local/$', views.VirtualMachineClearLocalContextDataView.as_view(), name='virtualmachine_delete_localconfigcontext'), url(r'^virtual-machines/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), url(r'^virtual-machines/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index d4728da45..34423f012 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -14,7 +14,7 @@ from extras.views import ObjectConfigContextView from ipam.models import Service from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectDeleteView, - ObjectEditView, ObjectListView, + ObjectEditView, ObjectListView, ObjectSetFieldNullView, ) from . import filters, forms, tables from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -285,6 +285,19 @@ class VirtualMachineCreateView(PermissionRequiredMixin, ObjectEditView): default_return_url = 'virtualization:virtualmachine_list' +class VirtualMachineEditLocalConfigContextView(VirtualMachineCreateView): + permission_required = 'virtualization.change_device' + model_form = forms.VirtualMachineLocalConfigContextForm + template_name = 'virtualization/virtualmachine_edit_local_config_context.html' + + +class VirtualMachineClearLocalContextDataView(ObjectSetFieldNullView): + permission_required = 'virtualization.change_virtualmachine' + model = VirtualMachine + field = 'local_config_context_data' + field_human_friendly_name = 'local config context' + + class VirtualMachineEditView(VirtualMachineCreateView): permission_required = 'virtualization.change_virtualmachine' From e3e9211e8a31a3ed4db787980838aad1d4e58c38 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Sep 2018 00:30:51 -0400 Subject: [PATCH 2/4] PEP8 fix --- netbox/extras/views.py | 4 ++-- netbox/utilities/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/netbox/extras/views.py b/netbox/extras/views.py index 7be652ca0..c49242772 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -113,8 +113,8 @@ class ObjectConfigContextView(View): model_name: obj, 'obj': obj, 'perm_string': '{}.change_{}'.format(app_label, model_name), - 'edit_url':'{}:{}_edit_localconfigcontext'.format(app_label, model_name), - 'delete_url':'{}:{}_delete_localconfigcontext'.format(app_label, model_name), + 'edit_url': '{}:{}_edit_localconfigcontext'.format(app_label, model_name), + 'delete_url': '{}:{}_delete_localconfigcontext'.format(app_label, model_name), 'rendered_context': obj.get_config_context(), 'source_contexts': source_contexts, 'base_template': self.base_template, diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 40ef04dd4..5e1dd68df 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -855,7 +855,7 @@ class ObjectSetFieldNullView(ObjectDeleteView): field_human_friendly_name = None def get(self, request, **kwargs): - + obj = self.get_object(kwargs) form = ConfirmationForm(initial=request.GET) From 4039753b2f69602a0203d76f7ced3d2da5f0835c Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 18 Sep 2018 11:52:12 -0400 Subject: [PATCH 3/4] refactored UI for local config context --- netbox/dcim/api/serializers.py | 4 +- netbox/dcim/forms.py | 13 ++--- ...a.py => 0063_device_local_context_data.py} | 2 +- netbox/dcim/models.py | 4 -- netbox/dcim/urls.py | 2 - netbox/dcim/views.py | 15 +----- netbox/extras/models.py | 9 +++- netbox/extras/views.py | 4 -- netbox/templates/dcim/device_edit.html | 6 +++ .../extras/object_configcontext.html | 22 ++------ .../virtualization/virtualmachine_edit.html | 6 +++ netbox/utilities/views.py | 50 ------------------- netbox/virtualization/api/serializers.py | 4 +- netbox/virtualization/forms.py | 14 ++---- ...0008_virtualmachine_local_context_data.py} | 2 +- netbox/virtualization/models.py | 4 -- netbox/virtualization/urls.py | 2 - netbox/virtualization/views.py | 15 +----- 18 files changed, 38 insertions(+), 140 deletions(-) rename netbox/dcim/migrations/{0063_device_local_config_context_data.py => 0063_device_local_context_data.py} (90%) rename netbox/virtualization/migrations/{0008_virtualmachine_local_config_context_data.py => 0008_virtualmachine_local_context_data.py} (90%) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 3acafda8b..a95743fd5 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -412,7 +412,7 @@ class DeviceSerializer(TaggitSerializer, CustomFieldModelSerializer): 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', 'created', - 'last_updated', 'local_config_context_data', + 'last_updated', 'local_context_data', ] validators = [] @@ -448,7 +448,7 @@ class DeviceWithConfigContextSerializer(DeviceSerializer): 'id', 'name', 'display_name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'parent_device', 'status', 'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'comments', 'tags', 'custom_fields', - 'config_context', 'created', 'last_updated', 'local_config_context_data', + 'config_context', 'created', 'last_updated', 'local_context_data', ] def get_config_context(self, obj): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index e5d64ff5d..333e90548 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -823,16 +823,19 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) comments = CommentField() tags = TagField(required=False) + local_context_data = JSONField(required=False) class Meta: model = Device fields = [ 'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'position', 'face', 'status', 'platform', 'primary_ip4', 'primary_ip6', 'tenant_group', 'tenant', 'comments', 'tags', + 'local_context_data' ] help_texts = { 'device_role': "The function this device serves", 'serial': "Chassis serial number", + 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered config context" } widgets = { 'face': forms.Select(attrs={'filter-for': 'position'}), @@ -920,16 +923,6 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.initial['rack'] = self.instance.parent_bay.device.rack_id -class DeviceLocalConfigContextForm(BootstrapMixin, forms.ModelForm): - local_config_context_data = JSONField() - - class Meta: - model = Device - fields = [ - 'local_config_context_data', - ] - - class BaseDeviceCSVForm(forms.ModelForm): device_role = forms.ModelChoiceField( queryset=DeviceRole.objects.all(), diff --git a/netbox/dcim/migrations/0063_device_local_config_context_data.py b/netbox/dcim/migrations/0063_device_local_context_data.py similarity index 90% rename from netbox/dcim/migrations/0063_device_local_config_context_data.py rename to netbox/dcim/migrations/0063_device_local_context_data.py index cbadde2ca..73c568887 100644 --- a/netbox/dcim/migrations/0063_device_local_config_context_data.py +++ b/netbox/dcim/migrations/0063_device_local_context_data.py @@ -13,7 +13,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='device', - name='local_config_context_data', + name='local_context_data', field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), ), ] diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index bc3677b6e..19c75bdb9 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -1287,10 +1287,6 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): images = GenericRelation( to='extras.ImageAttachment' ) - local_config_context_data = JSONField( - blank=True, - null=True, - ) objects = DeviceManager() tags = TaggableManager() diff --git a/netbox/dcim/urls.py b/netbox/dcim/urls.py index 51cedaa20..7345cdacd 100644 --- a/netbox/dcim/urls.py +++ b/netbox/dcim/urls.py @@ -142,8 +142,6 @@ urlpatterns = [ url(r'^devices/(?P\d+)/edit/$', views.DeviceEditView.as_view(), name='device_edit'), url(r'^devices/(?P\d+)/delete/$', views.DeviceDeleteView.as_view(), name='device_delete'), url(r'^devices/(?P\d+)/config-context/$', views.DeviceConfigContextView.as_view(), name='device_configcontext'), - url(r'^devices/(?P\d+)/config-context/edit-local/$', views.DeviceEditLocalConfigContextView.as_view(), name='device_edit_localconfigcontext'), - url(r'^devices/(?P\d+)/config-context/clear-local/$', views.DeviceClearLocalContextDataView.as_view(), name='device_delete_localconfigcontext'), url(r'^devices/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='device_changelog', kwargs={'model': Device}), url(r'^devices/(?P\d+)/inventory/$', views.DeviceInventoryView.as_view(), name='device_inventory'), url(r'^devices/(?P\d+)/status/$', views.DeviceStatusView.as_view(), name='device_status'), diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 42106b060..eb7f71a25 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -26,7 +26,7 @@ from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, GetReturnURLMixin, - ObjectDeleteView, ObjectEditView, ObjectListView, ObjectSetFieldNullView, + ObjectDeleteView, ObjectEditView, ObjectListView, ) from virtualization.models import VirtualMachine from . import filters, forms, tables @@ -983,19 +983,6 @@ class DeviceEditView(DeviceCreateView): permission_required = 'dcim.change_device' -class DeviceEditLocalConfigContextView(DeviceCreateView): - permission_required = 'dcim.change_device' - model_form = forms.DeviceLocalConfigContextForm - template_name = 'dcim/device_edit_local_config_context.html' - - -class DeviceClearLocalContextDataView(ObjectSetFieldNullView): - permission_required = 'dcim.change_device' - model = Device - field = 'local_config_context_data' - field_human_friendly_name = 'local config context' - - class DeviceDeleteView(PermissionRequiredMixin, ObjectDeleteView): permission_required = 'dcim.delete_device' model = Device diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 467a96f64..2ccb7cdf1 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -703,6 +703,11 @@ class ConfigContext(models.Model): class ConfigContextModel(models.Model): + local_context_data = JSONField( + blank=True, + null=True, + ) + class Meta: abstract = True @@ -717,8 +722,8 @@ class ConfigContextModel(models.Model): data.update(context.data) # If the object has local config context data defined, that data overwrites all rendered data - if self.local_config_context_data is not None: - data.update(self.local_config_context_data) + if self.local_context_data is not None: + data.update(self.local_context_data) return data diff --git a/netbox/extras/views.py b/netbox/extras/views.py index c49242772..7626d4012 100644 --- a/netbox/extras/views.py +++ b/netbox/extras/views.py @@ -107,14 +107,10 @@ class ObjectConfigContextView(View): obj = get_object_or_404(self.object_class, pk=pk) source_contexts = ConfigContext.objects.get_for_object(obj) model_name = self.object_class._meta.model_name - app_label = self.object_class._meta.app_label return render(request, 'extras/object_configcontext.html', { model_name: obj, 'obj': obj, - 'perm_string': '{}.change_{}'.format(app_label, model_name), - 'edit_url': '{}:{}_edit_localconfigcontext'.format(app_label, model_name), - 'delete_url': '{}:{}_delete_localconfigcontext'.format(app_label, model_name), 'rendered_context': obj.get_config_context(), 'source_contexts': source_contexts, 'base_template': self.base_template, diff --git a/netbox/templates/dcim/device_edit.html b/netbox/templates/dcim/device_edit.html index 23e023c5c..23b2b404e 100644 --- a/netbox/templates/dcim/device_edit.html +++ b/netbox/templates/dcim/device_edit.html @@ -77,6 +77,12 @@
{% endif %} +
+
Local Config Context Data
+
+ {% render_field form.local_context_data %} +
+
Tags
diff --git a/netbox/templates/extras/object_configcontext.html b/netbox/templates/extras/object_configcontext.html index eab3df10b..d23455c19 100644 --- a/netbox/templates/extras/object_configcontext.html +++ b/netbox/templates/extras/object_configcontext.html @@ -21,32 +21,18 @@ Local Context
- {% if obj.local_config_context_data %} -
{{ obj.local_config_context_data|render_json }}
+ {% if obj.local_context_data %} +
{{ obj.local_context_data|render_json }}
{% else %} None {% endif %} +
+ -
diff --git a/netbox/templates/virtualization/virtualmachine_edit.html b/netbox/templates/virtualization/virtualmachine_edit.html index ad49f752d..3be462c4d 100644 --- a/netbox/templates/virtualization/virtualmachine_edit.html +++ b/netbox/templates/virtualization/virtualmachine_edit.html @@ -48,6 +48,12 @@
{% endif %} +
+
Local Config Context Data
+
+ {% render_field form.local_context_data %} +
+
Tags
diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index 5e1dd68df..e11d681ef 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -844,56 +844,6 @@ class BulkComponentCreateView(GetReturnURLMixin, View): }) -class ObjectSetFieldNullView(ObjectDeleteView): - """ - Given a field name, set it to None (null) and save the object. - - field: The field to be nulled - field_friendly_name: Human friendly name for the field in the UI. - """ - template_name = 'utilities/object_set_field_null.html' - field_human_friendly_name = None - - def get(self, request, **kwargs): - - obj = self.get_object(kwargs) - form = ConfirmationForm(initial=request.GET) - - return render(request, self.template_name, { - 'obj': obj, - 'form': form, - 'obj_type': self.model._meta.verbose_name, - 'field_human_friendly_name': self.field_human_friendly_name, - 'return_url': self.get_return_url(request, obj), - }) - - def post(self, request, **kwargs): - - obj = self.get_object(kwargs) - form = ConfirmationForm(request.POST) - if form.is_valid(): - - setattr(obj, self.field, None) - obj.save() - - msg = 'Cleared {} on {} {}'.format(self.field_human_friendly_name, self.model._meta.verbose_name, obj) - messages.success(request, msg) - - return_url = form.cleaned_data.get('return_url') - if return_url is not None and is_safe_url(url=return_url, host=request.get_host()): - return redirect(return_url) - else: - return redirect(self.get_return_url(request, obj)) - - return render(request, self.template_name, { - 'obj': obj, - 'form': form, - 'obj_type': self.model._meta.verbose_name, - 'field_human_friendly_name': self.field_human_friendly_name, - 'return_url': self.get_return_url(request, obj), - }) - - @requires_csrf_token def server_error(request, template_name=ERROR_500_TEMPLATE_NAME): """ diff --git a/netbox/virtualization/api/serializers.py b/netbox/virtualization/api/serializers.py index faa2f3161..80a2f756a 100644 --- a/netbox/virtualization/api/serializers.py +++ b/netbox/virtualization/api/serializers.py @@ -107,7 +107,7 @@ class VirtualMachineSerializer(TaggitSerializer, CustomFieldModelSerializer): fields = [ 'id', 'name', 'status', 'site', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', - 'local_config_context_data', + 'local_context_data', ] @@ -118,7 +118,7 @@ class VirtualMachineWithConfigContextSerializer(VirtualMachineSerializer): fields = [ 'id', 'name', 'status', 'cluster', 'role', 'tenant', 'platform', 'primary_ip', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'custom_fields', 'config_context', 'created', 'last_updated', - 'local_config_context_data', + 'local_context_data', ] def get_config_context(self, obj): diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 686864820..9853157d8 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -248,6 +248,7 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): ) ) tags = TagField(required=False) + local_context_data = JSONField(required=False) class Meta: model = VirtualMachine @@ -255,6 +256,9 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', ] + help_texts = { + 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered config context", + } def __init__(self, *args, **kwargs): @@ -303,16 +307,6 @@ class VirtualMachineForm(BootstrapMixin, TenancyForm, CustomFieldForm): self.fields['primary_ip6'].widget.attrs['readonly'] = True -class VirtualMachineLocalConfigContextForm(BootstrapMixin, forms.ModelForm): - local_config_context_data = JSONField() - - class Meta: - model = VirtualMachine - fields = [ - 'local_config_context_data', - ] - - class VirtualMachineCSVForm(forms.ModelForm): status = CSVChoiceField( choices=VM_STATUS_CHOICES, diff --git a/netbox/virtualization/migrations/0008_virtualmachine_local_config_context_data.py b/netbox/virtualization/migrations/0008_virtualmachine_local_context_data.py similarity index 90% rename from netbox/virtualization/migrations/0008_virtualmachine_local_config_context_data.py rename to netbox/virtualization/migrations/0008_virtualmachine_local_context_data.py index e6f4b2bbf..ce8105d95 100644 --- a/netbox/virtualization/migrations/0008_virtualmachine_local_config_context_data.py +++ b/netbox/virtualization/migrations/0008_virtualmachine_local_context_data.py @@ -13,7 +13,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='virtualmachine', - name='local_config_context_data', + name='local_context_data', field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, null=True), ), ] diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 9a55c10fd..16c90c1cd 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -245,10 +245,6 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): content_type_field='obj_type', object_id_field='obj_id' ) - local_config_context_data = JSONField( - blank=True, - null=True, - ) tags = TaggableManager() diff --git a/netbox/virtualization/urls.py b/netbox/virtualization/urls.py index 0af76bba2..b03b3bc0a 100644 --- a/netbox/virtualization/urls.py +++ b/netbox/virtualization/urls.py @@ -49,8 +49,6 @@ urlpatterns = [ url(r'^virtual-machines/(?P\d+)/edit/$', views.VirtualMachineEditView.as_view(), name='virtualmachine_edit'), url(r'^virtual-machines/(?P\d+)/delete/$', views.VirtualMachineDeleteView.as_view(), name='virtualmachine_delete'), url(r'^virtual-machines/(?P\d+)/config-context/$', views.VirtualMachineConfigContextView.as_view(), name='virtualmachine_configcontext'), - url(r'^virtual-machines/(?P\d+)/config-context/edit-local/$', views.VirtualMachineEditLocalConfigContextView.as_view(), name='virtualmachine_edit_localconfigcontext'), - url(r'^virtual-machines/(?P\d+)/config-context/clear-local/$', views.VirtualMachineClearLocalContextDataView.as_view(), name='virtualmachine_delete_localconfigcontext'), url(r'^virtual-machines/(?P\d+)/changelog/$', ObjectChangeLogView.as_view(), name='virtualmachine_changelog', kwargs={'model': VirtualMachine}), url(r'^virtual-machines/(?P\d+)/services/assign/$', ServiceCreateView.as_view(), name='virtualmachine_service_assign'), diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py index 34423f012..d4728da45 100644 --- a/netbox/virtualization/views.py +++ b/netbox/virtualization/views.py @@ -14,7 +14,7 @@ from extras.views import ObjectConfigContextView from ipam.models import Service from utilities.views import ( BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, ComponentCreateView, ObjectDeleteView, - ObjectEditView, ObjectListView, ObjectSetFieldNullView, + ObjectEditView, ObjectListView, ) from . import filters, forms, tables from .models import Cluster, ClusterGroup, ClusterType, VirtualMachine @@ -285,19 +285,6 @@ class VirtualMachineCreateView(PermissionRequiredMixin, ObjectEditView): default_return_url = 'virtualization:virtualmachine_list' -class VirtualMachineEditLocalConfigContextView(VirtualMachineCreateView): - permission_required = 'virtualization.change_device' - model_form = forms.VirtualMachineLocalConfigContextForm - template_name = 'virtualization/virtualmachine_edit_local_config_context.html' - - -class VirtualMachineClearLocalContextDataView(ObjectSetFieldNullView): - permission_required = 'virtualization.change_virtualmachine' - model = VirtualMachine - field = 'local_config_context_data' - field_human_friendly_name = 'local config context' - - class VirtualMachineEditView(VirtualMachineCreateView): permission_required = 'virtualization.change_virtualmachine' From f76ce980e36aaadfe0179755ef6990c18b5f71dc Mon Sep 17 00:00:00 2001 From: John Anderson Date: Wed, 26 Sep 2018 10:30:34 -0400 Subject: [PATCH 4/4] remove templates no longer needed for local config context --- .../dcim/device_edit_local_config_context.html | 11 ----------- netbox/templates/utilities/object_set_field_null.html | 9 --------- .../virtualmachine_edit_local_config_context.html | 11 ----------- 3 files changed, 31 deletions(-) delete mode 100644 netbox/templates/dcim/device_edit_local_config_context.html delete mode 100644 netbox/templates/utilities/object_set_field_null.html delete mode 100644 netbox/templates/virtualization/virtualmachine_edit_local_config_context.html diff --git a/netbox/templates/dcim/device_edit_local_config_context.html b/netbox/templates/dcim/device_edit_local_config_context.html deleted file mode 100644 index de9c1c76f..000000000 --- a/netbox/templates/dcim/device_edit_local_config_context.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'utilities/obj_edit.html' %} -{% load form_helpers %} - -{% block form %} -
-
Local Config Context Data
-
- {% render_field form.local_config_context_data %} -
-
-{% endblock %} diff --git a/netbox/templates/utilities/object_set_field_null.html b/netbox/templates/utilities/object_set_field_null.html deleted file mode 100644 index d1d58a9ed..000000000 --- a/netbox/templates/utilities/object_set_field_null.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'utilities/confirmation_form.html' %} -{% load form_helpers %} - -{% block title %}Clear {{ field_human_friendly_name }}?{% endblock %} - -{% block message %} -

Are you sure you want to clear the {{ field_human_friendly_name }} on {{ obj_type }} {{ obj }}?

- {% block message_extra %}{% endblock %} -{% endblock %} diff --git a/netbox/templates/virtualization/virtualmachine_edit_local_config_context.html b/netbox/templates/virtualization/virtualmachine_edit_local_config_context.html deleted file mode 100644 index de9c1c76f..000000000 --- a/netbox/templates/virtualization/virtualmachine_edit_local_config_context.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends 'utilities/obj_edit.html' %} -{% load form_helpers %} - -{% block form %} -
-
Local Config Context Data
-
- {% render_field form.local_config_context_data %} -
-
-{% endblock %}