From 010765e1319001899c9d26d9c8cfe9fd52273634 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 22 Aug 2018 11:55:51 -0400 Subject: [PATCH 01/21] Post-release version bump --- netbox/netbox/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 5d2426841df..cdcc7106999 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.4.4' +VERSION = '2.4.5-dev' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) From 3a88e431039a678aa0885dd3a3d0a825fe5dd1e6 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 13 Sep 2018 11:21:40 -0400 Subject: [PATCH 02/21] Fixes #2406: Remove hard-coded limit of 1000 objects from API-populated form fields --- CHANGELOG.md | 8 ++++++++ netbox/project-static/js/forms.js | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e50be204f63..de80e2ad973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +v2.4.5 (FUTURE) + +## Bug Fixes + +* [#2406](https://github.com/digitalocean/netbox/issues/2406) - Remove hard-coded limit of 1000 objects from API-populated form fields + +--- + v2.4.4 (2018-08-22) ## Enhancements diff --git a/netbox/project-static/js/forms.js b/netbox/project-static/js/forms.js index 91b83bf2ae4..193e95ae802 100644 --- a/netbox/project-static/js/forms.js +++ b/netbox/project-static/js/forms.js @@ -82,7 +82,7 @@ $(document).ready(function() { } if ($(parent).val() || $(parent).attr('nullable') == 'true') { - var api_url = child_field.attr('api-url') + '&limit=1000'; + var api_url = child_field.attr('api-url'); var disabled_indicator = child_field.attr('disabled-indicator'); var initial_value = child_field.attr('initial'); var display_field = child_field.attr('display-field') || 'name'; From 292647da14d566fe0f2867c31c68c471e304a39e Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 13 Sep 2018 11:31:34 -0400 Subject: [PATCH 03/21] Closes #2402: Order and format JSON data in form fields --- CHANGELOG.md | 4 ++++ netbox/utilities/forms.py | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de80e2ad973..4e146f72c52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ v2.4.5 (FUTURE) +## Enhancements + +* [#2402](https://github.com/digitalocean/netbox/issues/2402) - Order and format JSON data in form fields + ## Bug Fixes * [#2406](https://github.com/digitalocean/netbox/issues/2406) - Remove hard-coded limit of 1000 objects from API-populated form fields diff --git a/netbox/utilities/forms.py b/netbox/utilities/forms.py index 1e6e3c0c415..47ba3744f28 100644 --- a/netbox/utilities/forms.py +++ b/netbox/utilities/forms.py @@ -2,11 +2,12 @@ from __future__ import unicode_literals import csv from io import StringIO +import json import re from django import forms from django.conf import settings -from django.contrib.postgres.forms import JSONField as _JSONField +from django.contrib.postgres.forms.jsonb import JSONField as _JSONField, InvalidJSONInput from django.db.models import Count from django.urls import reverse_lazy from mptt.forms import TreeNodeMultipleChoiceField @@ -556,9 +557,11 @@ class JSONField(_JSONField): self.widget.attrs['placeholder'] = '' def prepare_value(self, value): + if isinstance(value, InvalidJSONInput): + return value if value is None: return '' - return super(JSONField, self).prepare_value(value) + return json.dumps(value, sort_keys=True, indent=4) # From 162828da9046fb86055564156a6ab6f413ad26d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20L=C3=A9one?= Date: Mon, 10 Sep 2018 13:30:22 +0200 Subject: [PATCH 04/21] Add a page related to community related projects --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 28673bf365f..5b090048d82 100644 --- a/README.md +++ b/README.md @@ -42,3 +42,18 @@ and run `upgrade.sh`. * [Docker container](https://github.com/ninech/netbox-docker) (via [@cimnine](https://github.com/cimnine)) * [Vagrant deployment](https://github.com/ryanmerolle/netbox-vagrant) (via [@ryanmerolle](https://github.com/ryanmerolle)) * [Ansible deployment](https://github.com/lae/ansible-role-netbox) (via [@lae](https://github.com/lae)) + +# Related projects + +## Supported SDK + +- [pynetbox](https://github.com/digitalocean/pynetbox) Python API client library for Netbox. + +## Community SDK + +- [netbox-client-ruby](https://github.com/ninech/netbox-client-ruby) A ruby client library for Netbox v2. + +## Ansible Inventory + +- [netbox-as-ansible-inventory](https://github.com/AAbouZaid/netbox-as-ansible-inventory) Ansible dynamic inventory script for Netbox. + From 57b225b6807e49ecef6014fae20c36c0c2443eb6 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 15 Sep 2018 17:23:58 -0400 Subject: [PATCH 05/21] fixes #2423 - interface connection links --- netbox/dcim/tables.py | 6 ++++-- netbox/templates/dcim/inc/interface.html | 2 +- netbox/templates/dcim/interface.html | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index fc91057745f..2630a9ba250 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -614,10 +614,12 @@ class PowerConnectionTable(BaseTable): class InterfaceConnectionTable(BaseTable): device_a = tables.LinkColumn('dcim:device', accessor=Accessor('interface_a.device'), args=[Accessor('interface_a.device.pk')], verbose_name='Device A') - interface_a = tables.Column(verbose_name='Interface A') + interface_a = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_a'), + args=[Accessor('interface_a.pk')], verbose_name='Interface A') device_b = tables.LinkColumn('dcim:device', accessor=Accessor('interface_b.device'), args=[Accessor('interface_b.device.pk')], verbose_name='Device B') - interface_b = tables.Column(verbose_name='Interface B') + interface_b = tables.LinkColumn('dcim:interface', accessor=Accessor('interface_b'), + args=[Accessor('interface_b.pk')], verbose_name='Interface B') class Meta(BaseTable.Meta): model = InterfaceConnection diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 44da7cbc212..229f6f2eb81 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -44,7 +44,7 @@ {{ connected_iface.device }} - {{ connected_iface }} + {{ connected_iface }} {% endwith %} {% elif iface.circuit_termination %} diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index f82f81baf4c..47278d468d3 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -134,7 +134,9 @@ Name - {{ connected_interface.name }} + + {{ connected_interface.name }} + Type From e965adad7c6b0749a2d15a374dfda3932f1e97b3 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sat, 15 Sep 2018 17:25:50 -0400 Subject: [PATCH 06/21] changelog for #2432 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e146f72c52..db5120ead31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ v2.4.5 (FUTURE) ## Enhancements * [#2402](https://github.com/digitalocean/netbox/issues/2402) - Order and format JSON data in form fields +* [#2432](https://github.com/digitalocean/netbox/issues/2432) - Link remote interface connections to the Interface view ## Bug Fixes From 0da113b72375dca3677d47ab8fc39730bb67a326 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Sun, 16 Sep 2018 00:25:20 -0400 Subject: [PATCH 07/21] 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 cd9f1ceaa5b..465b4d2dcf0 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 0478932f71c..3acafda8b48 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 4e201639c5f..e5d64ff5d0e 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 00000000000..cbadde2cac0 --- /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 19c75bdb904..bc3677b6e7f 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 7345cdacd3a..51cedaa20c2 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 eb7f71a25a5..42106b0608d 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 ad4fcdb183d..467a96f6484 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 90d0d698d28..7be652ca06c 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 00000000000..de9c1c76ffc --- /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 81f8e1780f3..eab3df10b01 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 00000000000..d1d58a9edd7 --- /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 00000000000..de9c1c76ffc --- /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 e11d681efe1..40ef04dd40e 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 9dff223d3f1..faa2f316152 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 10833234b82..6868648201b 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 00000000000..e6f4b2bbf97 --- /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 119c9ee4ffa..9a55c10fd6a 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 b03b3bc0a40..0af76bba259 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 d4728da4548..34423f01235 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 08/21] 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 7be652ca06c..c49242772d8 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 40ef04dd40e..5e1dd68dff1 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 9df33cef8b2d4a5672e0917a66f58a4af9615ed9 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 18 Sep 2018 11:46:22 -0400 Subject: [PATCH 09/21] Fixes #2443: Enforce JSON object format when creating config contexts --- CHANGELOG.md | 1 + netbox/extras/models.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db5120ead31..8a665e8939b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ v2.4.5 (FUTURE) ## Bug Fixes * [#2406](https://github.com/digitalocean/netbox/issues/2406) - Remove hard-coded limit of 1000 objects from API-populated form fields +* [#2443](https://github.com/digitalocean/netbox/issues/2443) - Enforce JSON object format when creating config contexts --- diff --git a/netbox/extras/models.py b/netbox/extras/models.py index ad4fcdb183d..cf4d646f393 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -700,6 +700,14 @@ class ConfigContext(models.Model): def get_absolute_url(self): return reverse('extras:configcontext', kwargs={'pk': self.pk}) + def clean(self): + + # Verify that JSON data is provided as an object + if type(self.data) is not dict: + raise ValidationError( + {'data': 'JSON data must be in object form. Example: {"foo": 123}'} + ) + class ConfigContextModel(models.Model): From 4039753b2f69602a0203d76f7ced3d2da5f0835c Mon Sep 17 00:00:00 2001 From: John Anderson Date: Tue, 18 Sep 2018 11:52:12 -0400 Subject: [PATCH 10/21] 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 3acafda8b48..a95743fd559 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 e5d64ff5d0e..333e90548c2 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 cbadde2cac0..73c5688872b 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 bc3677b6e7f..19c75bdb904 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 51cedaa20c2..7345cdacd3a 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 42106b0608d..eb7f71a25a5 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 467a96f6484..2ccb7cdf1ff 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 c49242772d8..7626d401253 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 23e023c5cd7..23b2b404eaf 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 eab3df10b01..d23455c19ad 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 ad49f752d08..3be462c4d6d 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 5e1dd68dff1..e11d681efe1 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 faa2f316152..80a2f756a29 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 6868648201b..9853157d8e6 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 e6f4b2bbf97..ce8105d957c 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 9a55c10fd6a..16c90c1cd34 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 0af76bba259..b03b3bc0a40 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 34423f01235..d4728da4548 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 6cdff955dc516084c6514df1cf53ef58564dd83a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 18 Sep 2018 12:02:59 -0400 Subject: [PATCH 11/21] Fixes #2444: Improve validation of interface MAC addresses --- CHANGELOG.md | 1 + netbox/dcim/fields.py | 14 ++------------ netbox/dcim/formfields.py | 27 --------------------------- netbox/dcim/forms.py | 3 +-- netbox/virtualization/forms.py | 3 +-- 5 files changed, 5 insertions(+), 43 deletions(-) delete mode 100644 netbox/dcim/formfields.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a665e8939b..8105e000638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ v2.4.5 (FUTURE) * [#2406](https://github.com/digitalocean/netbox/issues/2406) - Remove hard-coded limit of 1000 objects from API-populated form fields * [#2443](https://github.com/digitalocean/netbox/issues/2443) - Enforce JSON object format when creating config contexts +* [#2444](https://github.com/digitalocean/netbox/issues/2444) - Improve validation of interface MAC addresses --- diff --git a/netbox/dcim/fields.py b/netbox/dcim/fields.py index 22e0be5812f..4f38ec24e45 100644 --- a/netbox/dcim/fields.py +++ b/netbox/dcim/fields.py @@ -1,13 +1,11 @@ from __future__ import unicode_literals -from netaddr import EUI, mac_unix_expanded +from netaddr import AddrFormatError, EUI, mac_unix_expanded from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models -from .formfields import MACAddressFormField - class ASNField(models.BigIntegerField): description = "32-bit ASN field" @@ -35,7 +33,7 @@ class MACAddressField(models.Field): return value try: return EUI(value, version=48, dialect=mac_unix_expanded_uppercase) - except ValueError as e: + except AddrFormatError as e: raise ValidationError(e) def db_type(self, connection): @@ -45,11 +43,3 @@ class MACAddressField(models.Field): if not value: return None return str(self.to_python(value)) - - def form_class(self): - return MACAddressFormField - - def formfield(self, **kwargs): - defaults = {'form_class': self.form_class()} - defaults.update(kwargs) - return super(MACAddressField, self).formfield(**defaults) diff --git a/netbox/dcim/formfields.py b/netbox/dcim/formfields.py deleted file mode 100644 index 804c2c95601..00000000000 --- a/netbox/dcim/formfields.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import unicode_literals - -from django import forms -from django.core.exceptions import ValidationError -from netaddr import EUI, AddrFormatError - - -# -# Form fields -# - -class MACAddressFormField(forms.Field): - default_error_messages = { - 'invalid': "Enter a valid MAC address.", - } - - def to_python(self, value): - if not value: - return None - - if isinstance(value, EUI): - return value - - try: - return EUI(value, version=48) - except AddrFormatError: - raise ValidationError("Please specify a valid MAC address.") diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 4e201639c5f..74af0bcbd62 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -27,7 +27,6 @@ from .constants import ( RACK_TYPE_CHOICES, RACK_WIDTH_CHOICES, RACK_WIDTH_19IN, RACK_WIDTH_23IN, SITE_STATUS_CHOICES, SUBDEVICE_ROLE_CHILD, SUBDEVICE_ROLE_PARENT, SUBDEVICE_ROLE_CHOICES, ) -from .formfields import MACAddressFormField from .models import ( DeviceBay, DeviceBayTemplate, ConsolePort, ConsolePortTemplate, ConsoleServerPort, ConsoleServerPortTemplate, Device, DeviceRole, DeviceType, Interface, InterfaceConnection, InterfaceTemplate, Manufacturer, InventoryItem, @@ -1854,7 +1853,7 @@ class InterfaceCreateForm(ComponentForm, forms.Form): enabled = forms.BooleanField(required=False) lag = forms.ModelChoiceField(queryset=Interface.objects.all(), required=False, label='Parent LAG') mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') - mac_address = MACAddressFormField(required=False, label='MAC Address') + mac_address = forms.CharField(required=False, label='MAC Address') mgmt_only = forms.BooleanField( required=False, label='OOB Management', diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index 10833234b82..478ac9503fc 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -8,7 +8,6 @@ from taggit.forms import TagField from dcim.constants import IFACE_FF_VIRTUAL, IFACE_MODE_ACCESS, IFACE_MODE_TAGGED_ALL from dcim.forms import INTERFACE_MODE_HELP_TEXT -from dcim.formfields import MACAddressFormField from dcim.models import Device, DeviceRole, Interface, Platform, Rack, Region, Site from extras.forms import AddRemoveTagsForm, CustomFieldBulkEditForm, CustomFieldForm, CustomFieldFilterForm from ipam.models import IPAddress @@ -456,7 +455,7 @@ class InterfaceCreateForm(ComponentForm): form_factor = forms.ChoiceField(choices=VIFACE_FF_CHOICES, initial=IFACE_FF_VIRTUAL, widget=forms.HiddenInput()) enabled = forms.BooleanField(required=False) mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') - mac_address = MACAddressFormField(required=False, label='MAC Address') + mac_address = forms.CharField(required=False, label='MAC Address') description = forms.CharField(max_length=100, required=False) def __init__(self, *args, **kwargs): From b4445dfdf8051f156f33abe0c332a118ebc14bfd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 18 Sep 2018 13:59:50 -0400 Subject: [PATCH 12/21] Fixes #2442: Nullify "next" link in API when limit=0 is passed --- CHANGELOG.md | 1 + netbox/netbox/api.py | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8105e000638..1e3fa157c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ v2.4.5 (FUTURE) ## Bug Fixes * [#2406](https://github.com/digitalocean/netbox/issues/2406) - Remove hard-coded limit of 1000 objects from API-populated form fields +* [#2442](https://github.com/digitalocean/netbox/issues/2442) - Nullify "next" link in API when limit=0 is passed * [#2443](https://github.com/digitalocean/netbox/issues/2443) - Enforce JSON object format when creating config contexts * [#2444](https://github.com/digitalocean/netbox/issues/2444) - Improve validation of interface MAC addresses diff --git a/netbox/netbox/api.py b/netbox/netbox/api.py index 28a0d7685dc..a0a9e91465e 100644 --- a/netbox/netbox/api.py +++ b/netbox/netbox/api.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +from django.conf import settings from rest_framework import authentication, exceptions from rest_framework.pagination import LimitOffsetPagination from rest_framework.permissions import DjangoModelPermissions, SAFE_METHODS @@ -104,8 +105,6 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination): def get_limit(self, request): - from django.conf import settings - if self.limit_query_param: try: limit = int(request.query_params[self.limit_query_param]) @@ -123,6 +122,22 @@ class OptionalLimitOffsetPagination(LimitOffsetPagination): return self.default_limit + def get_next_link(self): + + # Pagination has been disabled + if not self.limit: + return None + + return super(OptionalLimitOffsetPagination, self).get_next_link() + + def get_previous_link(self): + + # Pagination has been disabled + if not self.limit: + return None + + return super(OptionalLimitOffsetPagination, self).get_previous_link() + # # Miscellaneous From 95464772accd66382c3d99eb3e73dc08686c9a97 Mon Sep 17 00:00:00 2001 From: Veit Heller Date: Wed, 19 Sep 2018 10:57:09 +0200 Subject: [PATCH 13/21] docs: typo fix in devices --- docs/core-functionality/devices.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core-functionality/devices.md b/docs/core-functionality/devices.md index 0a03efb4c54..0d3df016cdf 100644 --- a/docs/core-functionality/devices.md +++ b/docs/core-functionality/devices.md @@ -99,7 +99,7 @@ Device bays represent the ability of a device to house child devices. For exampl # Platforms -A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15. +A platform defines the type of software running on a device or virtual machine. This can be helpful when it is necessary to distinguish between, for instance, different feature sets. Note that two devices of the same type may be assigned different platforms: for example, one Juniper MX240 running Junos 14 and another running Junos 15. The platform model is also used to indicate which [NAPALM](https://napalm-automation.net/) driver NetBox should use when connecting to a remote device. The name of the driver along with optional parameters are stored with the platform. See the [API documentation](api/napalm-integration.md) for more information on NAPALM integration. From 9440ac76409f70925a8630370864a98263a7e5b7 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 24 Sep 2018 16:59:33 -0400 Subject: [PATCH 14/21] Fixes #2455: Ignore unique address enforcement for IPs with a shared/virtual role --- CHANGELOG.md | 1 + netbox/ipam/constants.py | 10 ++++++++++ netbox/ipam/models.py | 6 +++++- netbox/ipam/tests/test_models.py | 6 ++++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3fa157c76..092fc7ab50e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ v2.4.5 (FUTURE) * [#2442](https://github.com/digitalocean/netbox/issues/2442) - Nullify "next" link in API when limit=0 is passed * [#2443](https://github.com/digitalocean/netbox/issues/2443) - Enforce JSON object format when creating config contexts * [#2444](https://github.com/digitalocean/netbox/issues/2444) - Improve validation of interface MAC addresses +* [#2455](https://github.com/digitalocean/netbox/issues/2455) - Ignore unique address enforcement for IPs with a shared/virtual role --- diff --git a/netbox/ipam/constants.py b/netbox/ipam/constants.py index e2b98a1ef8a..a675d3ca9be 100644 --- a/netbox/ipam/constants.py +++ b/netbox/ipam/constants.py @@ -51,6 +51,16 @@ IPADDRESS_ROLE_CHOICES = ( (IPADDRESS_ROLE_CARP, 'CARP'), ) +IPADDRESS_ROLES_NONUNIQUE = ( + # IPAddress roles which are exempt from unique address enforcement + IPADDRESS_ROLE_ANYCAST, + IPADDRESS_ROLE_VIP, + IPADDRESS_ROLE_VRRP, + IPADDRESS_ROLE_HSRP, + IPADDRESS_ROLE_GLBP, + IPADDRESS_ROLE_CARP, +) + # VLAN statuses VLAN_STATUS_ACTIVE = 1 VLAN_STATUS_RESERVED = 2 diff --git a/netbox/ipam/models.py b/netbox/ipam/models.py index 1b109f939c4..f9170cd58a4 100644 --- a/netbox/ipam/models.py +++ b/netbox/ipam/models.py @@ -596,7 +596,11 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel): if self.address: # Enforce unique IP space (if applicable) - if (self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE) or (self.vrf and self.vrf.enforce_unique): + if self.role not in IPADDRESS_ROLES_NONUNIQUE and ( + self.vrf is None and settings.ENFORCE_GLOBAL_UNIQUE + ) or ( + self.vrf and self.vrf.enforce_unique + ): duplicate_ips = self.get_duplicates() if duplicate_ips: raise ValidationError({ diff --git a/netbox/ipam/tests/test_models.py b/netbox/ipam/tests/test_models.py index 790b665cda7..d17e8f5ef09 100644 --- a/netbox/ipam/tests/test_models.py +++ b/netbox/ipam/tests/test_models.py @@ -4,6 +4,7 @@ import netaddr from django.core.exceptions import ValidationError from django.test import TestCase, override_settings +from ipam.constants import IPADDRESS_ROLE_VIP from ipam.models import IPAddress, Prefix, VRF @@ -59,3 +60,8 @@ class TestIPAddress(TestCase): IPAddress.objects.create(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24')) duplicate_ip = IPAddress(vrf=vrf, address=netaddr.IPNetwork('192.0.2.1/24')) self.assertRaises(ValidationError, duplicate_ip.clean) + + @override_settings(ENFORCE_GLOBAL_UNIQUE=True) + def test_duplicate_nonunique_role(self): + IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPADDRESS_ROLE_VIP) + IPAddress.objects.create(address=netaddr.IPNetwork('192.0.2.1/24'), role=IPADDRESS_ROLE_VIP) From f76ce980e36aaadfe0179755ef6990c18b5f71dc Mon Sep 17 00:00:00 2001 From: John Anderson Date: Wed, 26 Sep 2018 10:30:34 -0400 Subject: [PATCH 15/21] 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 de9c1c76ffc..00000000000 --- 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 d1d58a9edd7..00000000000 --- 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 de9c1c76ffc..00000000000 --- 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 %} From 2ee5b2344ebf8c34fd6461712f26785f6c571cbd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Sep 2018 14:21:49 -0400 Subject: [PATCH 16/21] Changelog and misc cleanup --- CHANGELOG.md | 1 + netbox/dcim/models.py | 2 +- netbox/virtualization/models.py | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 092fc7ab50e..fdee746d6ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ v2.4.5 (FUTURE) ## Enhancements +* [#2392](https://github.com/digitalocean/netbox/issues/2392) - Implemented local context data for devices and virtual machines * [#2402](https://github.com/digitalocean/netbox/issues/2402) - Order and format JSON data in form fields * [#2432](https://github.com/digitalocean/netbox/issues/2432) - Link remote interface connections to the Interface view diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 19c75bdb904..700741a1585 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -10,7 +10,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models -from django.db.models import Count, Q, ObjectDoesNotExist +from django.db.models import Count, Q from django.urls import reverse from django.utils.encoding import python_2_unicode_compatible from mptt.models import MPTTModel, TreeForeignKey diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py index 16c90c1cd34..119c9ee4ffa 100644 --- a/netbox/virtualization/models.py +++ b/netbox/virtualization/models.py @@ -2,7 +2,6 @@ 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 From 020b5ea870007922ef14a8d9df262b2c26c0302a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Sep 2018 16:04:51 -0400 Subject: [PATCH 17/21] Fixes #2470: Log the creation of device/VM components as object changes --- CHANGELOG.md | 1 + netbox/utilities/views.py | 29 +++++++++-------------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdee746d6ef..4d88b2daab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ v2.4.5 (FUTURE) * [#2443](https://github.com/digitalocean/netbox/issues/2443) - Enforce JSON object format when creating config contexts * [#2444](https://github.com/digitalocean/netbox/issues/2444) - Improve validation of interface MAC addresses * [#2455](https://github.com/digitalocean/netbox/issues/2455) - Ignore unique address enforcement for IPs with a shared/virtual role +* [#2470](https://github.com/digitalocean/netbox/issues/2470) - Log the creation of device/VM components as object changes --- diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index e11d681efe1..abfaeb5daa1 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -713,19 +713,24 @@ class ComponentCreateView(View): data = deepcopy(form.cleaned_data) for name in form.cleaned_data['name_pattern']: + + # Initialize data for the individual component form component_data = { self.parent_field: parent.pk, 'name': name, } + # Replace objects with their primary key to keep component_form.clean() happy for k, v in data.items(): if hasattr(v, 'pk'): component_data[k] = v.pk else: component_data[k] = v + component_form = self.model_form(component_data) + if component_form.is_valid(): - new_components.append(component_form.save(commit=False)) + new_components.append(component_form) else: for field, errors in component_form.errors.as_data().items(): # Assign errors on the child form's name field to name_pattern on the parent form @@ -735,26 +740,10 @@ class ComponentCreateView(View): form.add_error(field, '{}: {}'.format(name, ', '.join(e))) if not form.errors: - self.model.objects.bulk_create(new_components) - # ManyToMany relations are bulk created via the through model - m2m_fields = [field for field in component_form.fields if type(component_form.fields[field]) in M2M_FIELD_TYPES] - if m2m_fields: - for field in m2m_fields: - field_links = [] - for new_component in new_components: - for related_obj in component_form.cleaned_data[field]: - # The through model columns are the id's of our M2M relation objects - through_kwargs = {} - new_component_column = new_component.__class__.__name__ + '_id' - related_obj_column = related_obj.__class__.__name__ + '_id' - through_kwargs.update({ - new_component_column.lower(): new_component.id, - related_obj_column.lower(): related_obj.id - }) - field_link = getattr(self.model, field).through(**through_kwargs) - field_links.append(field_link) - getattr(self.model, field).through.objects.bulk_create(field_links) + # Create the new components + for component_form in new_components: + component_form.save() messages.success(request, "Added {} {} to {}.".format( len(new_components), self.model._meta.verbose_name_plural, parent From 15babeb5840ab36d47dcf1da4dd7a849db90ae07 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Sep 2018 16:26:08 -0400 Subject: [PATCH 18/21] Fixes #2414: Tags field missing from device/VM component creation forms --- CHANGELOG.md | 1 + netbox/dcim/forms.py | 6 ++++++ netbox/templates/dcim/interface_edit.html | 5 ----- .../virtualization/interface_edit.html | 1 + netbox/utilities/views.py | 20 +++++-------------- netbox/virtualization/forms.py | 4 +++- 6 files changed, 16 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d88b2daab8..853a0df4724 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ v2.4.5 (FUTURE) ## Bug Fixes * [#2406](https://github.com/digitalocean/netbox/issues/2406) - Remove hard-coded limit of 1000 objects from API-populated form fields +* [#2414](https://github.com/digitalocean/netbox/issues/2414) - Tags field missing from device/VM component creation forms * [#2442](https://github.com/digitalocean/netbox/issues/2442) - Nullify "next" link in API when limit=0 is passed * [#2443](https://github.com/digitalocean/netbox/issues/2443) - Enforce JSON object format when creating config contexts * [#2444](https://github.com/digitalocean/netbox/issues/2444) - Improve validation of interface MAC addresses diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index cec2fafad1c..e7fa15f7b71 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1194,6 +1194,7 @@ class ConsolePortForm(BootstrapMixin, forms.ModelForm): class ConsolePortCreateForm(ComponentForm): name_pattern = ExpandableNameField(label='Name') + tags = TagField(required=False) class ConsoleConnectionCSVForm(forms.ModelForm): @@ -1364,6 +1365,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): class ConsoleServerPortCreateForm(ComponentForm): name_pattern = ExpandableNameField(label='Name') + tags = TagField(required=False) class ConsoleServerPortConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): @@ -1461,6 +1463,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm): class PowerPortCreateForm(ComponentForm): name_pattern = ExpandableNameField(label='Name') + tags = TagField(required=False) class PowerConnectionCSVForm(forms.ModelForm): @@ -1631,6 +1634,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm): class PowerOutletCreateForm(ComponentForm): name_pattern = ExpandableNameField(label='Name') + tags = TagField(required=False) class PowerOutletConnectionForm(BootstrapMixin, ChainedFieldsMixin, forms.Form): @@ -1864,6 +1868,7 @@ class InterfaceCreateForm(ComponentForm, forms.Form): ) description = forms.CharField(max_length=100, required=False) mode = forms.ChoiceField(choices=add_blank_choice(IFACE_MODE_CHOICES), required=False) + tags = TagField(required=False) def __init__(self, *args, **kwargs): @@ -2101,6 +2106,7 @@ class DeviceBayForm(BootstrapMixin, forms.ModelForm): class DeviceBayCreateForm(ComponentForm): name_pattern = ExpandableNameField(label='Name') + tags = TagField(required=False) class PopulateDeviceBayForm(BootstrapMixin, forms.Form): diff --git a/netbox/templates/dcim/interface_edit.html b/netbox/templates/dcim/interface_edit.html index 6423c61c271..60d233b0396 100644 --- a/netbox/templates/dcim/interface_edit.html +++ b/netbox/templates/dcim/interface_edit.html @@ -14,11 +14,6 @@ {% render_field form.mgmt_only %} {% render_field form.description %} {% render_field form.mode %} -
-
-
-
Tags
-
{% render_field form.tags %}
diff --git a/netbox/templates/virtualization/interface_edit.html b/netbox/templates/virtualization/interface_edit.html index b3aa38fd3b4..7977f9fed91 100644 --- a/netbox/templates/virtualization/interface_edit.html +++ b/netbox/templates/virtualization/interface_edit.html @@ -11,6 +11,7 @@ {% render_field form.mtu %} {% render_field form.description %} {% render_field form.mode %} + {% render_field form.tags %}
{% if obj.mode %} diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index abfaeb5daa1..ef042176e5b 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -710,24 +710,14 @@ class ComponentCreateView(View): if form.is_valid(): new_components = [] - data = deepcopy(form.cleaned_data) + data = deepcopy(request.POST) + data[self.parent_field] = parent.pk for name in form.cleaned_data['name_pattern']: - # Initialize data for the individual component form - component_data = { - self.parent_field: parent.pk, - 'name': name, - } - - # Replace objects with their primary key to keep component_form.clean() happy - for k, v in data.items(): - if hasattr(v, 'pk'): - component_data[k] = v.pk - else: - component_data[k] = v - - component_form = self.model_form(component_data) + # Initialize the individual component form + data['name'] = name + component_form = self.model_form(data) if component_form.is_valid(): new_components.append(component_form) diff --git a/netbox/virtualization/forms.py b/netbox/virtualization/forms.py index ee1007be38f..e94c2cdaa63 100644 --- a/netbox/virtualization/forms.py +++ b/netbox/virtualization/forms.py @@ -419,11 +419,12 @@ class VirtualMachineFilterForm(BootstrapMixin, CustomFieldFilterForm): # class InterfaceForm(BootstrapMixin, forms.ModelForm): + tags = TagField(required=False) class Meta: model = Interface fields = [ - 'virtual_machine', 'name', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description', 'mode', + 'virtual_machine', 'name', 'form_factor', 'enabled', 'mac_address', 'mtu', 'description', 'mode', 'tags', 'untagged_vlan', 'tagged_vlans', ] widgets = { @@ -462,6 +463,7 @@ class InterfaceCreateForm(ComponentForm): mtu = forms.IntegerField(required=False, min_value=1, max_value=32767, label='MTU') mac_address = forms.CharField(required=False, label='MAC Address') description = forms.CharField(max_length=100, required=False) + tags = TagField(required=False) def __init__(self, *args, **kwargs): From aed2a3cd1b28bfecffc2870a8d61c74d9696b6bf Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 28 Sep 2018 16:44:05 -0400 Subject: [PATCH 19/21] Closes #2438: API optimizations for tagged objects --- CHANGELOG.md | 1 + netbox/circuits/api/views.py | 4 ++-- netbox/dcim/api/views.py | 26 +++++++++++++------------- netbox/ipam/api/views.py | 12 ++++++------ netbox/secrets/api/views.py | 2 +- netbox/tenancy/api/views.py | 2 +- netbox/virtualization/api/views.py | 8 +++++--- 7 files changed, 29 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 853a0df4724..033e767b136 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ v2.4.5 (FUTURE) * [#2392](https://github.com/digitalocean/netbox/issues/2392) - Implemented local context data for devices and virtual machines * [#2402](https://github.com/digitalocean/netbox/issues/2402) - Order and format JSON data in form fields * [#2432](https://github.com/digitalocean/netbox/issues/2432) - Link remote interface connections to the Interface view +* [#2438](https://github.com/digitalocean/netbox/issues/2438) - API optimizations for tagged objects ## Bug Fixes diff --git a/netbox/circuits/api/views.py b/netbox/circuits/api/views.py index 3b1623da41b..eccc1edfc65 100644 --- a/netbox/circuits/api/views.py +++ b/netbox/circuits/api/views.py @@ -29,7 +29,7 @@ class CircuitsFieldChoicesViewSet(FieldChoicesViewSet): # class ProviderViewSet(CustomFieldModelViewSet): - queryset = Provider.objects.all() + queryset = Provider.objects.prefetch_related('tags') serializer_class = serializers.ProviderSerializer filter_class = filters.ProviderFilter @@ -59,7 +59,7 @@ class CircuitTypeViewSet(ModelViewSet): # class CircuitViewSet(CustomFieldModelViewSet): - queryset = Circuit.objects.select_related('type', 'tenant', 'provider') + queryset = Circuit.objects.select_related('type', 'tenant', 'provider').prefetch_related('tags') serializer_class = serializers.CircuitSerializer filter_class = filters.CircuitFilter diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 901d9d2a53a..9edfe7bb9ca 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from collections import OrderedDict from django.conf import settings -from django.http import HttpResponseBadRequest, HttpResponseForbidden +from django.http import HttpResponseForbidden from django.shortcuts import get_object_or_404 from drf_yasg import openapi from drf_yasg.openapi import Parameter @@ -60,7 +60,7 @@ class RegionViewSet(ModelViewSet): # class SiteViewSet(CustomFieldModelViewSet): - queryset = Site.objects.select_related('region', 'tenant') + queryset = Site.objects.select_related('region', 'tenant').prefetch_related('tags') serializer_class = serializers.SiteSerializer filter_class = filters.SiteFilter @@ -100,7 +100,7 @@ class RackRoleViewSet(ModelViewSet): # class RackViewSet(CustomFieldModelViewSet): - queryset = Rack.objects.select_related('site', 'group__site', 'tenant') + queryset = Rack.objects.select_related('site', 'group__site', 'tenant').prefetch_related('tags') serializer_class = serializers.RackSerializer filter_class = filters.RackFilter @@ -154,7 +154,7 @@ class ManufacturerViewSet(ModelViewSet): # class DeviceTypeViewSet(CustomFieldModelViewSet): - queryset = DeviceType.objects.select_related('manufacturer') + queryset = DeviceType.objects.select_related('manufacturer').prefetch_related('tags') serializer_class = serializers.DeviceTypeSerializer filter_class = filters.DeviceTypeFilter @@ -228,7 +228,7 @@ class DeviceViewSet(CustomFieldModelViewSet): 'device_type__manufacturer', 'device_role', 'tenant', 'platform', 'site', 'rack', 'parent_bay', 'virtual_chassis__master', ).prefetch_related( - 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', + 'primary_ip4__nat_outside', 'primary_ip6__nat_outside', 'tags', ) filter_class = filters.DeviceFilter @@ -315,31 +315,31 @@ class DeviceViewSet(CustomFieldModelViewSet): # class ConsolePortViewSet(ModelViewSet): - queryset = ConsolePort.objects.select_related('device', 'cs_port__device') + queryset = ConsolePort.objects.select_related('device', 'cs_port__device').prefetch_related('tags') serializer_class = serializers.ConsolePortSerializer filter_class = filters.ConsolePortFilter class ConsoleServerPortViewSet(ModelViewSet): - queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device') + queryset = ConsoleServerPort.objects.select_related('device', 'connected_console__device').prefetch_related('tags') serializer_class = serializers.ConsoleServerPortSerializer filter_class = filters.ConsoleServerPortFilter class PowerPortViewSet(ModelViewSet): - queryset = PowerPort.objects.select_related('device', 'power_outlet__device') + queryset = PowerPort.objects.select_related('device', 'power_outlet__device').prefetch_related('tags') serializer_class = serializers.PowerPortSerializer filter_class = filters.PowerPortFilter class PowerOutletViewSet(ModelViewSet): - queryset = PowerOutlet.objects.select_related('device', 'connected_port__device') + queryset = PowerOutlet.objects.select_related('device', 'connected_port__device').prefetch_related('tags') serializer_class = serializers.PowerOutletSerializer filter_class = filters.PowerOutletFilter class InterfaceViewSet(ModelViewSet): - queryset = Interface.objects.select_related('device') + queryset = Interface.objects.select_related('device').prefetch_related('tags') serializer_class = serializers.InterfaceSerializer filter_class = filters.InterfaceFilter @@ -355,13 +355,13 @@ class InterfaceViewSet(ModelViewSet): class DeviceBayViewSet(ModelViewSet): - queryset = DeviceBay.objects.select_related('installed_device') + queryset = DeviceBay.objects.select_related('installed_device').prefetch_related('tags') serializer_class = serializers.DeviceBaySerializer filter_class = filters.DeviceBayFilter class InventoryItemViewSet(ModelViewSet): - queryset = InventoryItem.objects.select_related('device', 'manufacturer') + queryset = InventoryItem.objects.select_related('device', 'manufacturer').prefetch_related('tags') serializer_class = serializers.InventoryItemSerializer filter_class = filters.InventoryItemFilter @@ -393,7 +393,7 @@ class InterfaceConnectionViewSet(ModelViewSet): # class VirtualChassisViewSet(ModelViewSet): - queryset = VirtualChassis.objects.all() + queryset = VirtualChassis.objects.prefetch_related('tags') serializer_class = serializers.VirtualChassisSerializer diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index e3268834328..41cea7eaabb 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -33,7 +33,7 @@ class IPAMFieldChoicesViewSet(FieldChoicesViewSet): # class VRFViewSet(CustomFieldModelViewSet): - queryset = VRF.objects.select_related('tenant') + queryset = VRF.objects.select_related('tenant').prefetch_related('tags') serializer_class = serializers.VRFSerializer filter_class = filters.VRFFilter @@ -53,7 +53,7 @@ class RIRViewSet(ModelViewSet): # class AggregateViewSet(CustomFieldModelViewSet): - queryset = Aggregate.objects.select_related('rir') + queryset = Aggregate.objects.select_related('rir').prefetch_related('tags') serializer_class = serializers.AggregateSerializer filter_class = filters.AggregateFilter @@ -73,7 +73,7 @@ class RoleViewSet(ModelViewSet): # class PrefixViewSet(CustomFieldModelViewSet): - queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role') + queryset = Prefix.objects.select_related('site', 'vrf__tenant', 'tenant', 'vlan', 'role').prefetch_related('tags') serializer_class = serializers.PrefixSerializer filter_class = filters.PrefixFilter @@ -245,7 +245,7 @@ class IPAddressViewSet(CustomFieldModelViewSet): queryset = IPAddress.objects.select_related( 'vrf__tenant', 'tenant', 'nat_inside', 'interface__device__device_type', 'interface__virtual_machine' ).prefetch_related( - 'nat_outside' + 'nat_outside', 'tags', ) serializer_class = serializers.IPAddressSerializer filter_class = filters.IPAddressFilter @@ -266,7 +266,7 @@ class VLANGroupViewSet(ModelViewSet): # class VLANViewSet(CustomFieldModelViewSet): - queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role') + queryset = VLAN.objects.select_related('site', 'group', 'tenant', 'role').prefetch_related('tags') serializer_class = serializers.VLANSerializer filter_class = filters.VLANFilter @@ -276,6 +276,6 @@ class VLANViewSet(CustomFieldModelViewSet): # class ServiceViewSet(ModelViewSet): - queryset = Service.objects.select_related('device') + queryset = Service.objects.select_related('device').prefetch_related('tags') serializer_class = serializers.ServiceSerializer filter_class = filters.ServiceFilter diff --git a/netbox/secrets/api/views.py b/netbox/secrets/api/views.py index 9bc52f9f0e1..01567be8b59 100644 --- a/netbox/secrets/api/views.py +++ b/netbox/secrets/api/views.py @@ -48,7 +48,7 @@ class SecretViewSet(ModelViewSet): queryset = Secret.objects.select_related( 'device__primary_ip4', 'device__primary_ip6', 'role', ).prefetch_related( - 'role__users', 'role__groups', + 'role__users', 'role__groups', 'tags', ) serializer_class = serializers.SecretSerializer filter_class = filters.SecretFilter diff --git a/netbox/tenancy/api/views.py b/netbox/tenancy/api/views.py index 1ebd955003e..48cd76163da 100644 --- a/netbox/tenancy/api/views.py +++ b/netbox/tenancy/api/views.py @@ -30,6 +30,6 @@ class TenantGroupViewSet(ModelViewSet): # class TenantViewSet(CustomFieldModelViewSet): - queryset = Tenant.objects.select_related('group') + queryset = Tenant.objects.select_related('group').prefetch_related('tags') serializer_class = serializers.TenantSerializer filter_class = filters.TenantFilter diff --git a/netbox/virtualization/api/views.py b/netbox/virtualization/api/views.py index c20b8091e14..ab8ce7fbcf0 100644 --- a/netbox/virtualization/api/views.py +++ b/netbox/virtualization/api/views.py @@ -35,7 +35,7 @@ class ClusterGroupViewSet(ModelViewSet): class ClusterViewSet(CustomFieldModelViewSet): - queryset = Cluster.objects.select_related('type', 'group') + queryset = Cluster.objects.select_related('type', 'group').prefetch_related('tags') serializer_class = serializers.ClusterSerializer filter_class = filters.ClusterFilter @@ -47,7 +47,7 @@ class ClusterViewSet(CustomFieldModelViewSet): class VirtualMachineViewSet(CustomFieldModelViewSet): queryset = VirtualMachine.objects.select_related( 'cluster__site', 'role', 'tenant', 'platform', 'primary_ip4', 'primary_ip6' - ) + ).prefetch_related('tags') filter_class = filters.VirtualMachineFilter def get_serializer_class(self): @@ -60,6 +60,8 @@ class VirtualMachineViewSet(CustomFieldModelViewSet): class InterfaceViewSet(ModelViewSet): - queryset = Interface.objects.filter(virtual_machine__isnull=False).select_related('virtual_machine') + queryset = Interface.objects.filter( + virtual_machine__isnull=False + ).select_related('virtual_machine').prefetch_related('tags') serializer_class = serializers.InterfaceSerializer filter_class = filters.InterfaceFilter From fc1b3d6927fecae410896f2b7c28a2aed1cffc3a Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 2 Oct 2018 11:51:53 -0400 Subject: [PATCH 20/21] Fixes #2471: Fix ReadTheDocs theme --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 532c60a70df..87b7da25450 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,5 @@ site_name: NetBox +theme: readthedocs repo_url: https://github.com/digitalocean/netbox pages: From 20fed375d1ba19093764d192f8a9fae61bd569a2 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 2 Oct 2018 15:24:42 -0400 Subject: [PATCH 21/21] Release v2.4.5 --- CHANGELOG.md | 2 +- netbox/netbox/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 033e767b136..36cb0d4f340 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -v2.4.5 (FUTURE) +v2.4.5 (2018-10-02) ## Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index cdcc7106999..c1440b85aff 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -22,7 +22,7 @@ if sys.version_info[0] < 3: DeprecationWarning ) -VERSION = '2.4.5-dev' +VERSION = '2.4.5' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))