From 2dfbd72f10f8bfddbee3c54a275d73c953d0af4b Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Sep 2023 10:33:54 -0400 Subject: [PATCH 01/25] Fixes #13767: Fix support for comments when creating a new service via web UI --- netbox/ipam/forms/model_forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index cc147cec4..e965bf7b1 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -731,7 +731,7 @@ class ServiceCreateForm(ServiceForm): class Meta(ServiceForm.Meta): fields = [ 'device', 'virtual_machine', 'service_template', 'name', 'protocol', 'ports', 'ipaddresses', 'description', - 'tags', + 'comments', 'tags', ] def __init__(self, *args, **kwargs): From 79bf12a8fe927c4107d6cef5202063c809613a22 Mon Sep 17 00:00:00 2001 From: Per von Zweigbergk Date: Mon, 18 Sep 2023 14:33:29 +0200 Subject: [PATCH 02/25] 13791 rename whitespace fix (#13793) * Add test for bug #13791 https://github.com/netbox-community/netbox/issues/13791 * Fix #13791 by disabling striping on find and replace fields of BulkRenameForm --- netbox/utilities/forms/forms.py | 5 ++++- netbox/utilities/tests/test_forms.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 9f84e100f..04d956a49 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -40,8 +40,11 @@ class BulkRenameForm(BootstrapMixin, forms.Form): """ An extendable form to be used for renaming objects in bulk. """ - find = forms.CharField() + find = forms.CharField( + strip=False + ) replace = forms.CharField( + strip=False, required=False ) use_regex = forms.BooleanField( diff --git a/netbox/utilities/tests/test_forms.py b/netbox/utilities/tests/test_forms.py index 8fd001ba8..d014d4bbd 100644 --- a/netbox/utilities/tests/test_forms.py +++ b/netbox/utilities/tests/test_forms.py @@ -3,6 +3,7 @@ from django.test import TestCase from utilities.choices import ImportFormatChoices from utilities.forms.bulk_import import BulkImportForm +from utilities.forms.forms import BulkRenameForm from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipaddress_pattern @@ -364,3 +365,16 @@ class ImportFormTest(TestCase): {'a': '1', 'b': '2', 'c': '3'}, {'a': '4', 'b': '5', 'c': '6'}, ]) + + +class BulkRenameFormTest(TestCase): + def test_no_strip_whitespace(self): + # Tests to make sure Bulk Rename Form isn't stripping whitespaces + # See: https://github.com/netbox-community/netbox/issues/13791 + form = BulkRenameForm(data={ + "find": " hello ", + "replace": " world " + }) + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data["find"], " hello ") + self.assertEqual(form.cleaned_data["replace"], " world ") From c7dd4206c80815fef827bf08fedddc6017c14d55 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Sep 2023 08:44:42 -0400 Subject: [PATCH 03/25] Fixes #13727: Fix exception when viewing rendered config for VM without a role assigned --- netbox/dcim/models/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/models/mixins.py b/netbox/dcim/models/mixins.py index 95f6d41fe..9be8dc0a3 100644 --- a/netbox/dcim/models/mixins.py +++ b/netbox/dcim/models/mixins.py @@ -69,7 +69,7 @@ class RenderConfigMixin(models.Model): """ if self.config_template: return self.config_template - if self.role.config_template: + if self.role and self.role.config_template: return self.role.config_template if self.platform and self.platform.config_template: return self.platform.config_template From 940c947d3fcedbba6c1f4c1f4ae531624c8f9dad Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Sep 2023 08:49:08 -0400 Subject: [PATCH 04/25] Changelog for #11209, #12219, #13727, #13563, #13767, #13791 --- docs/release-notes/version-3.6.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index a019cbe13..af0ee1c06 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -2,11 +2,20 @@ ## v3.6.2 (FUTURE) +### Enhancements + +* [#13563](https://github.com/netbox-community/netbox/issues/13563) - Add support for other delimiting characters when using CSV import + ### Bug Fixes +* [#11209](https://github.com/netbox-community/netbox/issues/11209) - Hide available IP/VLAN listing when sorting under a parent prefix or VLAN range +* [#12219](https://github.com/netbox-community/netbox/issues/12219) - Fix dashboard widget heading contrast under dark mode * [#13701](https://github.com/netbox-community/netbox/issues/13701) - Correct display of power feed legs under device view * [#13706](https://github.com/netbox-community/netbox/issues/13706) - Restore extra filters dropdown on device interfaces list * [#13721](https://github.com/netbox-community/netbox/issues/13721) - Filter VLAN choices by selected site (if any) when creating a prefix +* [#13727](https://github.com/netbox-community/netbox/issues/13727) - Fix exception when viewing rendered config for VM without a role assigned +* [#13767](https://github.com/netbox-community/netbox/issues/13767) - Fix support for comments when creating a new service via web UI +* [#13791](https://github.com/netbox-community/netbox/issues/13791) - Preserve whitespace in values when performing bulk rename of objects via web UI --- @@ -33,7 +42,7 @@ * [#13657](https://github.com/netbox-community/netbox/issues/13657) - Fix decoding of data file content * [#13674](https://github.com/netbox-community/netbox/issues/13674) - Fix retrieving individual report via REST API * [#13682](https://github.com/netbox-community/netbox/issues/13682) - Fix error message returned when validation of custom field default value fails -* [#13684](https://github.com/netbox-community/netbox/issues/13684) - Enable modying the configuration when maintenance mode is enabled +* [#13684](https://github.com/netbox-community/netbox/issues/13684) - Enable modifying the configuration when maintenance mode is enabled --- From 3d1f66823587cf3cd420d06054b1afedada8abb0 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Mon, 18 Sep 2023 18:39:29 +0530 Subject: [PATCH 05/25] Disables module_status ordering (#13761) * disables module_status ordering #13756 * Set accessor for module status value --------- Co-authored-by: Jeremy Stretch --- netbox/dcim/tables/devices.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 68c24ca14..34dbcbf30 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -871,8 +871,9 @@ class ModuleBayTable(DeviceComponentTable): url_name='dcim:modulebay_list' ) module_status = columns.TemplateColumn( - verbose_name=_('Module Status'), - template_code=MODULEBAY_STATUS + accessor=tables.A('installed_module__status'), + template_code=MODULEBAY_STATUS, + verbose_name=_('Module Status') ) class Meta(DeviceComponentTable.Meta): From b0541be107728329e5830d1d2e7797478524983d Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Mon, 18 Sep 2023 06:59:26 -0700 Subject: [PATCH 06/25] 13745 device type migration (#13747) * 13745 update migrations to use batch_size * 13745 update migrations to use subquery update * 13745 refactor and update other counter migrations --- .../0176_device_component_counters.py | 47 +++++-------------- .../0177_devicetype_component_counters.py | 47 +++++-------------- .../0178_virtual_chassis_member_counter.py | 8 +--- netbox/utilities/counters.py | 20 +++++++- .../commands/calculate_cached_counts.py | 21 +-------- .../0035_virtualmachine_interface_count.py | 8 +--- 6 files changed, 47 insertions(+), 104 deletions(-) diff --git a/netbox/dcim/migrations/0176_device_component_counters.py b/netbox/dcim/migrations/0176_device_component_counters.py index a911d7fd7..60857ecb9 100644 --- a/netbox/dcim/migrations/0176_device_component_counters.py +++ b/netbox/dcim/migrations/0176_device_component_counters.py @@ -2,47 +2,22 @@ from django.db import migrations from django.db.models import Count import utilities.fields +from utilities.counters import update_counts def recalculate_device_counts(apps, schema_editor): Device = apps.get_model("dcim", "Device") - devices = Device.objects.annotate( - _console_port_count=Count('consoleports', distinct=True), - _console_server_port_count=Count('consoleserverports', distinct=True), - _power_port_count=Count('powerports', distinct=True), - _power_outlet_count=Count('poweroutlets', distinct=True), - _interface_count=Count('interfaces', distinct=True), - _front_port_count=Count('frontports', distinct=True), - _rear_port_count=Count('rearports', distinct=True), - _device_bay_count=Count('devicebays', distinct=True), - _module_bay_count=Count('modulebays', distinct=True), - _inventory_item_count=Count('inventoryitems', distinct=True), - ) - for device in devices: - device.console_port_count = device._console_port_count - device.console_server_port_count = device._console_server_port_count - device.power_port_count = device._power_port_count - device.power_outlet_count = device._power_outlet_count - device.interface_count = device._interface_count - device.front_port_count = device._front_port_count - device.rear_port_count = device._rear_port_count - device.device_bay_count = device._device_bay_count - device.module_bay_count = device._module_bay_count - device.inventory_item_count = device._inventory_item_count - - Device.objects.bulk_update(devices, [ - 'console_port_count', - 'console_server_port_count', - 'power_port_count', - 'power_outlet_count', - 'interface_count', - 'front_port_count', - 'rear_port_count', - 'device_bay_count', - 'module_bay_count', - 'inventory_item_count', - ], batch_size=100) + update_counts(Device, 'console_port_count', 'consoleports') + update_counts(Device, 'console_server_port_count', 'consoleserverports') + update_counts(Device, 'power_port_count', 'powerports') + update_counts(Device, 'power_outlet_count', 'poweroutlets') + update_counts(Device, 'interface_count', 'interfaces') + update_counts(Device, 'front_port_count', 'frontports') + update_counts(Device, 'rear_port_count', 'rearports') + update_counts(Device, 'device_bay_count', 'devicebays') + update_counts(Device, 'module_bay_count', 'modulebays') + update_counts(Device, 'inventory_item_count', 'inventoryitems') class Migration(migrations.Migration): diff --git a/netbox/dcim/migrations/0177_devicetype_component_counters.py b/netbox/dcim/migrations/0177_devicetype_component_counters.py index 66d1460d9..b452ce2d9 100644 --- a/netbox/dcim/migrations/0177_devicetype_component_counters.py +++ b/netbox/dcim/migrations/0177_devicetype_component_counters.py @@ -2,47 +2,22 @@ from django.db import migrations from django.db.models import Count import utilities.fields +from utilities.counters import update_counts def recalculate_devicetype_template_counts(apps, schema_editor): DeviceType = apps.get_model("dcim", "DeviceType") - device_types = list(DeviceType.objects.all().annotate( - _console_port_template_count=Count('consoleporttemplates', distinct=True), - _console_server_port_template_count=Count('consoleserverporttemplates', distinct=True), - _power_port_template_count=Count('powerporttemplates', distinct=True), - _power_outlet_template_count=Count('poweroutlettemplates', distinct=True), - _interface_template_count=Count('interfacetemplates', distinct=True), - _front_port_template_count=Count('frontporttemplates', distinct=True), - _rear_port_template_count=Count('rearporttemplates', distinct=True), - _device_bay_template_count=Count('devicebaytemplates', distinct=True), - _module_bay_template_count=Count('modulebaytemplates', distinct=True), - _inventory_item_template_count=Count('inventoryitemtemplates', distinct=True), - )) - for devicetype in device_types: - devicetype.console_port_template_count = devicetype._console_port_template_count - devicetype.console_server_port_template_count = devicetype._console_server_port_template_count - devicetype.power_port_template_count = devicetype._power_port_template_count - devicetype.power_outlet_template_count = devicetype._power_outlet_template_count - devicetype.interface_template_count = devicetype._interface_template_count - devicetype.front_port_template_count = devicetype._front_port_template_count - devicetype.rear_port_template_count = devicetype._rear_port_template_count - devicetype.device_bay_template_count = devicetype._device_bay_template_count - devicetype.module_bay_template_count = devicetype._module_bay_template_count - devicetype.inventory_item_template_count = devicetype._inventory_item_template_count - - DeviceType.objects.bulk_update(device_types, [ - 'console_port_template_count', - 'console_server_port_template_count', - 'power_port_template_count', - 'power_outlet_template_count', - 'interface_template_count', - 'front_port_template_count', - 'rear_port_template_count', - 'device_bay_template_count', - 'module_bay_template_count', - 'inventory_item_template_count', - ]) + update_counts(DeviceType, 'console_port_template_count', 'consoleporttemplates') + update_counts(DeviceType, 'console_server_port_template_count', 'consoleserverporttemplates') + update_counts(DeviceType, 'power_port_template_count', 'powerporttemplates') + update_counts(DeviceType, 'power_outlet_template_count', 'poweroutlettemplates') + update_counts(DeviceType, 'interface_template_count', 'interfacetemplates') + update_counts(DeviceType, 'front_port_template_count', 'frontporttemplates') + update_counts(DeviceType, 'rear_port_template_count', 'rearporttemplates') + update_counts(DeviceType, 'device_bay_template_count', 'devicebaytemplates') + update_counts(DeviceType, 'module_bay_template_count', 'modulebaytemplates') + update_counts(DeviceType, 'inventory_item_template_count', 'inventoryitemtemplates') class Migration(migrations.Migration): diff --git a/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py b/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py index 7d07a4d9d..99b304b66 100644 --- a/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py +++ b/netbox/dcim/migrations/0178_virtual_chassis_member_counter.py @@ -2,17 +2,13 @@ from django.db import migrations from django.db.models import Count import utilities.fields +from utilities.counters import update_counts def populate_virtualchassis_members(apps, schema_editor): VirtualChassis = apps.get_model('dcim', 'VirtualChassis') - vcs = VirtualChassis.objects.annotate(_member_count=Count('members', distinct=True)) - - for vc in vcs: - vc.member_count = vc._member_count - - VirtualChassis.objects.bulk_update(vcs, ['member_count'], batch_size=100) + update_counts(VirtualChassis, 'member_count', 'members') class Migration(migrations.Migration): diff --git a/netbox/utilities/counters.py b/netbox/utilities/counters.py index 6c1418dff..b0bd2560b 100644 --- a/netbox/utilities/counters.py +++ b/netbox/utilities/counters.py @@ -1,5 +1,5 @@ from django.apps import apps -from django.db.models import F +from django.db.models import F, Count, OuterRef, Subquery from django.db.models.signals import post_delete, post_save from netbox.registry import registry @@ -23,6 +23,24 @@ def update_counter(model, pk, counter_name, value): ) +def update_counts(model, field_name, related_query): + """ + Perform a bulk update for the given model and counter field. For example, + + update_counts(Device, '_interface_count', 'interfaces') + + will effectively set + + Device.objects.update(_interface_count=Count('interfaces')) + """ + subquery = Subquery( + model.objects.filter(pk=OuterRef('pk')).annotate(_count=Count(related_query)).values('_count') + ) + return model.objects.update(**{ + field_name: subquery + }) + + # # Signal handlers # diff --git a/netbox/utilities/management/commands/calculate_cached_counts.py b/netbox/utilities/management/commands/calculate_cached_counts.py index 62354797c..f7810604f 100644 --- a/netbox/utilities/management/commands/calculate_cached_counts.py +++ b/netbox/utilities/management/commands/calculate_cached_counts.py @@ -4,6 +4,7 @@ from django.core.management.base import BaseCommand from django.db.models import Count, OuterRef, Subquery from netbox.registry import registry +from utilities.counters import update_counts class Command(BaseCommand): @@ -26,27 +27,9 @@ class Command(BaseCommand): return models - def update_counts(self, model, field_name, related_query): - """ - Perform a bulk update for the given model and counter field. For example, - - update_counts(Device, '_interface_count', 'interfaces') - - will effectively set - - Device.objects.update(_interface_count=Count('interfaces')) - """ - self.stdout.write(f'Updating {model.__name__} {field_name}...') - subquery = Subquery( - model.objects.filter(pk=OuterRef('pk')).annotate(_count=Count(related_query)).values('_count') - ) - return model.objects.update(**{ - field_name: subquery - }) - def handle(self, *model_names, **options): for model, mappings in self.collect_models().items(): for field_name, related_query in mappings.items(): - self.update_counts(model, field_name, related_query) + update_counts(model, field_name, related_query) self.stdout.write(self.style.SUCCESS('Finished.')) diff --git a/netbox/virtualization/migrations/0035_virtualmachine_interface_count.py b/netbox/virtualization/migrations/0035_virtualmachine_interface_count.py index 725b73573..abed09d7e 100644 --- a/netbox/virtualization/migrations/0035_virtualmachine_interface_count.py +++ b/netbox/virtualization/migrations/0035_virtualmachine_interface_count.py @@ -2,17 +2,13 @@ from django.db import migrations from django.db.models import Count import utilities.fields +from utilities.counters import update_counts def populate_virtualmachine_counts(apps, schema_editor): VirtualMachine = apps.get_model('virtualization', 'VirtualMachine') - vms = VirtualMachine.objects.annotate(_interface_count=Count('interfaces', distinct=True)) - - for vm in vms: - vm.interface_count = vm._interface_count - - VirtualMachine.objects.bulk_update(vms, ['interface_count'], batch_size=100) + update_counts(VirtualMachine, 'interface_count', 'interfaces') class Migration(migrations.Migration): From 9aa7444bf9158c8ffa825e1d96144f9c55bfd260 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Sep 2023 10:55:00 -0400 Subject: [PATCH 07/25] Fixes #13782: Fix tag exclusion support for contact assignments --- netbox/extras/forms/mixins.py | 17 +++++++++++++++++ netbox/netbox/forms/base.py | 19 ++++--------------- netbox/tenancy/forms/model_forms.py | 10 +++------- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/netbox/extras/forms/mixins.py b/netbox/extras/forms/mixins.py index be45f5211..5366dcc28 100644 --- a/netbox/extras/forms/mixins.py +++ b/netbox/extras/forms/mixins.py @@ -9,6 +9,7 @@ from utilities.forms.fields import DynamicModelMultipleChoiceField __all__ = ( 'CustomFieldsMixin', 'SavedFiltersMixin', + 'TagsMixin', ) @@ -72,3 +73,19 @@ class SavedFiltersMixin(forms.Form): 'usable': True, } ) + + +class TagsMixin(forms.Form): + tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False, + label=_('Tags'), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Limit tags to those applicable to the object type + content_type = ContentType.objects.get_for_model(self._meta.model) + if content_type and hasattr(self.fields['tags'].widget, 'add_query_param'): + self.fields['tags'].widget.add_query_param('for_object_type_id', content_type.pk) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index c5dac90f7..43d0850f0 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -4,10 +4,11 @@ from django.db.models import Q from django.utils.translation import gettext_lazy as _ from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices, CustomFieldVisibilityChoices -from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin +from extras.forms.mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin from extras.models import CustomField, Tag -from utilities.forms import BootstrapMixin, CSVModelForm, CheckLastUpdatedMixin +from utilities.forms import CSVModelForm from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.mixins import BootstrapMixin, CheckLastUpdatedMixin __all__ = ( 'NetBoxModelForm', @@ -17,7 +18,7 @@ __all__ = ( ) -class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, forms.ModelForm): +class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm): """ Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. @@ -26,18 +27,6 @@ class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, the rendered form (optional). If not defined, the all fields will be rendered as a single section. """ fieldsets = () - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False, - label=_('Tags'), - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Limit tags to those applicable to the object type - if (ct := self._get_content_type()) and hasattr(self.fields['tags'].widget, 'add_query_param'): - self.fields['tags'].widget.add_query_param('for_object_type_id', ct.pk) def _get_content_type(self): return ContentType.objects.get_for_model(self._meta.model) diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index 72c030d84..5b1051c68 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -1,10 +1,11 @@ from django import forms from django.utils.translation import gettext_lazy as _ +from extras.forms.mixins import TagsMixin from extras.models import Tag from netbox.forms import NetBoxModelForm from tenancy.models import * -from utilities.forms import BootstrapMixin +from utilities.forms.mixins import BootstrapMixin from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField __all__ = ( @@ -121,7 +122,7 @@ class ContactForm(NetBoxModelForm): } -class ContactAssignmentForm(BootstrapMixin, forms.ModelForm): +class ContactAssignmentForm(BootstrapMixin, TagsMixin, forms.ModelForm): group = DynamicModelChoiceField( label=_('Group'), queryset=ContactGroup.objects.all(), @@ -141,11 +142,6 @@ class ContactAssignmentForm(BootstrapMixin, forms.ModelForm): label=_('Role'), queryset=ContactRole.objects.all() ) - tags = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False, - label=_('Tags') - ) class Meta: model = ContactAssignment From 68966db23d22edcd81423e717a4157428ebfb896 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 15 Sep 2023 10:22:46 -0400 Subject: [PATCH 08/25] Fixes #13741: Enforce unique names for inventory items with no parent item --- .../0182_inventoryitem_uniqueness_fix.py | 17 +++++++++++++++++ netbox/dcim/models/device_components.py | 5 +++++ 2 files changed, 22 insertions(+) create mode 100644 netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py diff --git a/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py b/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py new file mode 100644 index 000000000..85cd2196b --- /dev/null +++ b/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-09-15 14:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dcim', '0181_rename_device_role_device_role'), + ] + + operations = [ + migrations.AddConstraint( + model_name='inventoryitem', + constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('device', 'name'), name='dcim_inventoryitem_unique_device_name'), + ), + ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index f42ae5895..76c378cc0 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1246,6 +1246,11 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin): fields=('device', 'parent', 'name'), name='%(app_label)s_%(class)s_unique_device_parent_name' ), + models.UniqueConstraint( + fields=('device', 'name'), + name='%(app_label)s_%(class)s_unique_device_name', + condition=Q(parent__isnull=True) + ), ) verbose_name = _('inventory item') verbose_name_plural = _('inventory items') From b9b9bb134fa766c795bdfaa3f3d41a5601f5f614 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Sep 2023 11:12:27 -0400 Subject: [PATCH 09/25] Changelog for #13741, #13745, #13756, #13782 --- docs/release-notes/version-3.6.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index af0ee1c06..9b00ac2e5 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -14,7 +14,11 @@ * [#13706](https://github.com/netbox-community/netbox/issues/13706) - Restore extra filters dropdown on device interfaces list * [#13721](https://github.com/netbox-community/netbox/issues/13721) - Filter VLAN choices by selected site (if any) when creating a prefix * [#13727](https://github.com/netbox-community/netbox/issues/13727) - Fix exception when viewing rendered config for VM without a role assigned +* [#13741](https://github.com/netbox-community/netbox/issues/13741) - Enforce unique names for inventory items with no parent item +* [#13745](https://github.com/netbox-community/netbox/issues/13745) - Optimize counter field migrations for large databases +* [#13756](https://github.com/netbox-community/netbox/issues/13756) - Fix exception when sorting module bay list by installed module status * [#13767](https://github.com/netbox-community/netbox/issues/13767) - Fix support for comments when creating a new service via web UI +* [#13782](https://github.com/netbox-community/netbox/issues/13782) - Fix tag exclusion support for contact assignments * [#13791](https://github.com/netbox-community/netbox/issues/13791) - Preserve whitespace in values when performing bulk rename of objects via web UI --- From 252bf035250d8d8a336183578081e7f5b27c285c Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 18 Sep 2023 13:35:54 -0400 Subject: [PATCH 10/25] Fixes #13802: Restore 'description' header text for custom fields --- netbox/templates/extras/customfield.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/templates/extras/customfield.html b/netbox/templates/extras/customfield.html index 85824e61b..dd5cce7bd 100644 --- a/netbox/templates/extras/customfield.html +++ b/netbox/templates/extras/customfield.html @@ -32,7 +32,7 @@ {{ object.group_name|placeholder }} - + {% trans "Description" %} {{ object.description|markdown|placeholder }} From a08b5793f62b57f11bb184acf3bc427a0fa39e5f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Tue, 19 Sep 2023 14:40:52 -0400 Subject: [PATCH 11/25] Correct example default dashboard config --- docs/configuration/default-values.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/configuration/default-values.md b/docs/configuration/default-values.md index e76930208..d90e6eafc 100644 --- a/docs/configuration/default-values.md +++ b/docs/configuration/default-values.md @@ -20,7 +20,7 @@ DEFAULT_DASHBOARD = [ { 'widget': 'extras.ObjectCountsWidget', 'width': 4, - 'height': 2, + 'height': 3, 'title': 'Organization', 'config': { 'models': [ @@ -32,6 +32,8 @@ DEFAULT_DASHBOARD = [ }, { 'widget': 'extras.ObjectCountsWidget', + 'width': 4, + 'height': 3, 'title': 'IPAM', 'color': 'blue', 'config': { From 69215c411b76082412607a0674132ebe4961a95a Mon Sep 17 00:00:00 2001 From: Arthur Date: Tue, 19 Sep 2023 11:04:59 -0700 Subject: [PATCH 12/25] 13818 add tags to l2vpntermination edit form --- netbox/templates/ipam/l2vpntermination_edit.html | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/templates/ipam/l2vpntermination_edit.html b/netbox/templates/ipam/l2vpntermination_edit.html index 3db0d0102..0df2c883e 100644 --- a/netbox/templates/ipam/l2vpntermination_edit.html +++ b/netbox/templates/ipam/l2vpntermination_edit.html @@ -41,6 +41,7 @@
{% render_field form.vminterface %}
+ {% render_field form.tags %} From 35bcc2ce9d3717ff5ec50718693bff818ec93e85 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2023 08:44:25 -0400 Subject: [PATCH 13/25] Revert "Fixes #13741: Enforce unique names for inventory items with no parent item" This reverts commit 68966db23d22edcd81423e717a4157428ebfb896. --- .../0182_inventoryitem_uniqueness_fix.py | 17 ----------------- netbox/dcim/models/device_components.py | 5 ----- 2 files changed, 22 deletions(-) delete mode 100644 netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py diff --git a/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py b/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py deleted file mode 100644 index 85cd2196b..000000000 --- a/netbox/dcim/migrations/0182_inventoryitem_uniqueness_fix.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.5 on 2023-09-15 14:20 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('dcim', '0181_rename_device_role_device_role'), - ] - - operations = [ - migrations.AddConstraint( - model_name='inventoryitem', - constraint=models.UniqueConstraint(condition=models.Q(('parent__isnull', True)), fields=('device', 'name'), name='dcim_inventoryitem_unique_device_name'), - ), - ] diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 76c378cc0..f42ae5895 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -1246,11 +1246,6 @@ class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin): fields=('device', 'parent', 'name'), name='%(app_label)s_%(class)s_unique_device_parent_name' ), - models.UniqueConstraint( - fields=('device', 'name'), - name='%(app_label)s_%(class)s_unique_device_name', - condition=Q(parent__isnull=True) - ), ) verbose_name = _('inventory item') verbose_name_plural = _('inventory items') From 8e251ac33c13efacdbda0e2d89620191bac7d040 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 20 Sep 2023 06:56:52 -0700 Subject: [PATCH 14/25] 13757 Fix ConfigContext reference to DeviceType (#13804) * 13757 do prefetch to work around Django issue with vars in init (DeviceType) * 13757 use self.__dict to access vars in init * 13757 change test --- netbox/dcim/models/cables.py | 4 ++-- netbox/dcim/models/device_component_templates.py | 2 +- netbox/dcim/models/device_components.py | 2 +- netbox/dcim/models/devices.py | 6 +++--- netbox/extras/models/customfields.py | 2 +- netbox/extras/tests/test_views.py | 10 ++++++---- netbox/ipam/models/ip.py | 4 ++-- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index de7ba0eb6..ba9e11d46 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -98,10 +98,10 @@ class Cable(PrimaryModel): super().__init__(*args, **kwargs) # A copy of the PK to be used by __str__ in case the object is deleted - self._pk = self.pk + self._pk = self.__dict__.get('id') # Cache the original status so we can check later if it's been changed - self._orig_status = self.status + self._orig_status = self.__dict__.get('status') self._terminations_modified = False diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index f58d2bbca..86b6d85fe 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -89,7 +89,7 @@ class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin): super().__init__(*args, **kwargs) # Cache the original DeviceType ID for reference under clean() - self._original_device_type = self.device_type_id + self._original_device_type = self.__dict__.get('device_type_id') def to_objectchange(self, action): objectchange = super().to_objectchange(action) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index f42ae5895..639f8aadb 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -86,7 +86,7 @@ class ComponentModel(NetBoxModel): super().__init__(*args, **kwargs) # Cache the original Device ID for reference under clean() - self._original_device = self.device_id + self._original_device = self.__dict__.get('device_id') def __str__(self): if self.label: diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 857251caf..9cca724ce 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -205,11 +205,11 @@ class DeviceType(ImageAttachmentsMixin, PrimaryModel, WeightMixin): super().__init__(*args, **kwargs) # Save a copy of u_height for validation in clean() - self._original_u_height = self.u_height + self._original_u_height = self.__dict__.get('u_height') # Save references to the original front/rear images - self._original_front_image = self.front_image - self._original_rear_image = self.rear_image + self._original_front_image = self.__dict__.get('front_image') + self._original_rear_image = self.__dict__.get('rear_image') def get_absolute_url(self): return reverse('dcim:devicetype', args=[self.pk]) diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index 0c4a0c615..abea48032 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -219,7 +219,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): super().__init__(*args, **kwargs) # Cache instance's original name so we can check later whether it has changed - self._name = self.name + self._name = self.__dict__.get('name') @property def search_type(self): diff --git a/netbox/extras/tests/test_views.py b/netbox/extras/tests/test_views.py index 01ef9a2a6..296ed9f4d 100644 --- a/netbox/extras/tests/test_views.py +++ b/netbox/extras/tests/test_views.py @@ -6,7 +6,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.urls import reverse -from dcim.models import Site +from dcim.models import DeviceType, Manufacturer, Site from extras.choices import * from extras.models import * from utilities.testing import ViewTestCases, TestCase @@ -434,7 +434,8 @@ class ConfigContextTestCase( @classmethod def setUpTestData(cls): - site = Site.objects.create(name='Site 1', slug='site-1') + manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1') + devicetype = DeviceType.objects.create(manufacturer=manufacturer, model='Device Type 1', slug='device-type-1') # Create three ConfigContexts for i in range(1, 4): @@ -443,7 +444,7 @@ class ConfigContextTestCase( data={'foo': i} ) configcontext.save() - configcontext.sites.add(site) + configcontext.device_types.add(devicetype) cls.form_data = { 'name': 'Config Context X', @@ -451,11 +452,12 @@ class ConfigContextTestCase( 'description': 'A new config context', 'is_active': True, 'regions': [], - 'sites': [site.pk], + 'sites': [], 'roles': [], 'platforms': [], 'tenant_groups': [], 'tenants': [], + 'device_types': [devicetype.id,], 'tags': [], 'data': '{"foo": 123}', } diff --git a/netbox/ipam/models/ip.py b/netbox/ipam/models/ip.py index 2456fa021..00c08b3bc 100644 --- a/netbox/ipam/models/ip.py +++ b/netbox/ipam/models/ip.py @@ -290,8 +290,8 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel): super().__init__(*args, **kwargs) # Cache the original prefix and VRF so we can check if they have changed on post_save - self._prefix = self.prefix - self._vrf_id = self.vrf_id + self._prefix = self.__dict__.get('prefix') + self._vrf_id = self.__dict__.get('vrf_id') def __str__(self): return str(self.prefix) From 6fb980349f67776613d08d402535c1264a76b283 Mon Sep 17 00:00:00 2001 From: bluikko <14869000+bluikko@users.noreply.github.com> Date: Wed, 20 Sep 2023 12:35:55 +0700 Subject: [PATCH 15/25] 13245 add QSFP112 and OSFP-RHS interface choices --- netbox/dcim/choices.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netbox/dcim/choices.py b/netbox/dcim/choices.py index 1bcf61b20..e1d4a330a 100644 --- a/netbox/dcim/choices.py +++ b/netbox/dcim/choices.py @@ -837,8 +837,10 @@ class InterfaceTypeChoices(ChoiceSet): TYPE_200GE_QSFP56 = '200gbase-x-qsfp56' TYPE_200GE_QSFP_DD = '200gbase-x-qsfpdd' TYPE_400GE_CFP2 = '400gbase-x-cfp2' + TYPE_400GE_QSFP112 = '400gbase-x-qsfp112' TYPE_400GE_QSFP_DD = '400gbase-x-qsfpdd' TYPE_400GE_OSFP = '400gbase-x-osfp' + TYPE_400GE_OSFP_RHS = '400gbase-x-osfp-rhs' TYPE_400GE_CDFP = '400gbase-x-cdfp' TYPE_400GE_CFP8 = '400gbase-x-cfp8' TYPE_800GE_QSFP_DD = '800gbase-x-qsfpdd' @@ -989,8 +991,10 @@ class InterfaceTypeChoices(ChoiceSet): (TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'), (TYPE_200GE_QSFP56, 'QSFP56 (200GE)'), (TYPE_200GE_QSFP_DD, 'QSFP-DD (200GE)'), + (TYPE_400GE_QSFP112, 'QSFP112 (400GE)'), (TYPE_400GE_QSFP_DD, 'QSFP-DD (400GE)'), (TYPE_400GE_OSFP, 'OSFP (400GE)'), + (TYPE_400GE_OSFP_RHS, 'OSFP-RHS (400GE)'), (TYPE_400GE_CDFP, 'CDFP (400GE)'), (TYPE_400GE_CFP8, 'CPF8 (400GE)'), (TYPE_800GE_QSFP_DD, 'QSFP-DD (800GE)'), From 7a410dfd00e0074cb6d5445c5c9f0a90a9222283 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 20 Sep 2023 10:57:35 -0700 Subject: [PATCH 16/25] 13813 fix virtual chassis member count (#13823) * 13813 fix virtual chassis member count * 13813 add test --- netbox/utilities/counters.py | 5 +++-- netbox/utilities/tests/test_counters.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/netbox/utilities/counters.py b/netbox/utilities/counters.py index b0bd2560b..6c597b943 100644 --- a/netbox/utilities/counters.py +++ b/netbox/utilities/counters.py @@ -52,12 +52,13 @@ def post_save_receiver(sender, instance, created, **kwargs): for field_name, counter_name in get_counters_for_model(sender): parent_model = sender._meta.get_field(field_name).related_model new_pk = getattr(instance, field_name, None) - old_pk = instance.tracker.get(field_name) if field_name in instance.tracker else None + has_old_field = field_name in instance.tracker + old_pk = instance.tracker.get(field_name) if has_old_field else None # Update the counters on the old and/or new parents as needed if old_pk is not None: update_counter(parent_model, old_pk, counter_name, -1) - if new_pk is not None and (old_pk or created): + if new_pk is not None and (has_old_field or created): update_counter(parent_model, new_pk, counter_name, 1) diff --git a/netbox/utilities/tests/test_counters.py b/netbox/utilities/tests/test_counters.py index 0c61c0890..cf8850c52 100644 --- a/netbox/utilities/tests/test_counters.py +++ b/netbox/utilities/tests/test_counters.py @@ -36,10 +36,18 @@ class CountersTest(TestCase): self.assertEqual(device1.interface_count, 3) self.assertEqual(device2.interface_count, 3) + # test saving an existing object - counter should not change interface1.save() device1.refresh_from_db() self.assertEqual(device1.interface_count, 3) + # test save where tracked object FK back pointer is None + vc = VirtualChassis.objects.create(name='Virtual Chassis 1') + device1.virtual_chassis = vc + device1.save() + vc.refresh_from_db() + self.assertEqual(vc.member_count, 1) + def test_interface_count_deletion(self): """ When a tracked object (Interface) is deleted the tracking counter should be updated. From 6e4b4a553b02616973df9631799e23779b56ab2e Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 20 Sep 2023 11:06:04 -0700 Subject: [PATCH 17/25] 12685 use markdown for custom fields added to form (#13828) * 12685 use markdown for custom fields added to form * 13809 change markdown to use utilities * Add help_text for CustomField description indicating Markdown support --------- Co-authored-by: Jeremy Stretch --- netbox/extras/forms/model_forms.py | 3 ++- netbox/extras/models/customfields.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index c8afeb090..7bc83b715 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -76,7 +76,8 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): 'type': _( "The type of data stored in this field. For object/multi-object fields, select the related object " "type below." - ) + ), + 'description': _("This will be displayed as help text for the form field. Markdown is supported.") } def __init__(self, *args, **kwargs): diff --git a/netbox/extras/models/customfields.py b/netbox/extras/models/customfields.py index abea48032..e6f339e5a 100644 --- a/netbox/extras/models/customfields.py +++ b/netbox/extras/models/customfields.py @@ -28,6 +28,7 @@ from utilities.forms.fields import ( from utilities.forms.utils import add_blank_choice from utilities.forms.widgets import APISelect, APISelectMultiple, DatePicker, DateTimePicker from utilities.querysets import RestrictedQuerySet +from utilities.templatetags.builtins.filters import render_markdown from utilities.validators import validate_regex __all__ = ( @@ -498,7 +499,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel): field.model = self field.label = str(self) if self.description: - field.help_text = escape(self.description) + field.help_text = render_markdown(self.description) # Annotate read-only fields if enforce_visibility and self.ui_visibility == CustomFieldVisibilityChoices.VISIBILITY_READ_ONLY: From a1e42dad1016aba97c2ba70467ca2e5a20fe3e69 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 20 Sep 2023 11:08:12 -0700 Subject: [PATCH 18/25] 13653 darken code color to work in light and dark modes (#13827) * 13653 darken code color to work in light and dark modes * 13809 changed to use mx-1 on code block --- netbox/project-static/dist/netbox-dark.css | Bin 375591 -> 375591 bytes netbox/project-static/styles/theme-dark.scss | 2 +- netbox/templates/extras/script_list.html | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/netbox/project-static/dist/netbox-dark.css b/netbox/project-static/dist/netbox-dark.css index 2aa24b72c1b05dc2fdcadd498f5c2a8817b85ee2..2d7142bc6b6da7f32935de02c63becb00dc33c22 100644 GIT binary patch delta 43 vcmZ4fO>Fr$u?>^NCMOuMO&7SyB-*@ItbMH*BM>tIF*6XeY+ozJTD1fKh+q(u delta 46 ycmZ4fO>Fr$u?>^N*itQ1lT*{C3*2N9ZC)$ZzE+G8h?#(x8Hic7uN7mhS^@yN0TH4A diff --git a/netbox/project-static/styles/theme-dark.scss b/netbox/project-static/styles/theme-dark.scss index 4bbe5cea5..2d04b44e3 100644 --- a/netbox/project-static/styles/theme-dark.scss +++ b/netbox/project-static/styles/theme-dark.scss @@ -282,7 +282,7 @@ $btn-close-white-filter: invert(1) grayscale(100%) brightness(200%); $btn-close-bg: url("data:image/svg+xml,"); // Code -$code-color: $gray-200; +$code-color: $gray-600; $kbd-color: $white; $kbd-bg: $gray-300; $pre-color: null; diff --git a/netbox/templates/extras/script_list.html b/netbox/templates/extras/script_list.html index fc45bebc7..167e3073c 100644 --- a/netbox/templates/extras/script_list.html +++ b/netbox/templates/extras/script_list.html @@ -42,7 +42,7 @@ {% else %} From f5dd7d853ab557ce1b74207fb256d0dcc4030283 Mon Sep 17 00:00:00 2001 From: Arthur Hanson Date: Wed, 20 Sep 2023 11:11:25 -0700 Subject: [PATCH 19/25] 13809 fix ConfigRevision edit if custom validators (#13825) * 13809 fix ConfigRevision edit, check if custom validator JSON serializable * 13809 check json rendering for all fields * Refactor field initialization logic to more cleanly handle statically configured values --------- Co-authored-by: Jeremy Stretch --- netbox/extras/forms/model_forms.py | 40 +++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index 7bc83b715..83a346420 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -518,22 +518,34 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe config = get_config() for param in PARAMS: value = getattr(config, param.name) - is_static = hasattr(settings, param.name) - if value: - help_text = self.fields[param.name].help_text - if help_text: - help_text += '
' # Line break - help_text += _('Current value: {value}').format(value=value) - if is_static: - help_text += _(' (defined statically)') - elif value == param.default: - help_text += _(' (default)') - self.fields[param.name].help_text = help_text + + # Set the field's initial value, if it can be serialized. (This may not be the case e.g. for + # CUSTOM_VALIDATORS, which may reference Python objects.) + try: + json.dumps(value) if type(value) in (tuple, list): - value = ', '.join(value) - self.fields[param.name].initial = value - if is_static: + self.fields[param.name].initial = ', '.join(value) + else: + self.fields[param.name].initial = value + except TypeError: + pass + + # Check whether this parameter is statically configured (e.g. in configuration.py) + if hasattr(settings, param.name): self.fields[param.name].disabled = True + self.fields[param.name].help_text = _( + 'This parameter has been defined statically and cannot be modified.' + ) + continue + + # Set the field's help text + help_text = self.fields[param.name].help_text + if help_text: + help_text += '
' # Line break + help_text += _('Current value: {value}').format(value=value or '—') + if value == param.default: + help_text += _(' (default)') + self.fields[param.name].help_text = help_text def save(self, commit=True): instance = super().save(commit=False) From ae4ea3443e91c027144610e6cd7b1e018f9535ec Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2023 14:40:27 -0400 Subject: [PATCH 20/25] Fixes #11617: Check for invalid CSV headers during bulk import (#13826) * Fixes #11617: Check for invalid CSV headers during bulk import * Add test for CSV import header validation --- netbox/netbox/tests/test_import.py | 30 +++++++++++++++++++++++++++ netbox/utilities/forms/bulk_import.py | 1 + netbox/utilities/forms/forms.py | 22 +++++++++++--------- 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/netbox/netbox/tests/test_import.py b/netbox/netbox/tests/test_import.py index 6594409f2..bd07886e8 100644 --- a/netbox/netbox/tests/test_import.py +++ b/netbox/netbox/tests/test_import.py @@ -17,6 +17,36 @@ class CSVImportTestCase(ModelViewTestCase): def _get_csv_data(self, csv_data): return '\n'.join(csv_data) + def test_invalid_headers(self): + """ + Test that import form validation fails when an unknown CSV header is present. + """ + self.add_permissions('dcim.add_region') + + csv_data = [ + 'name,slug,INVALIDHEADER', + 'Region 1,region-1,abc', + 'Region 2,region-2,def', + 'Region 3,region-3,ghi', + ] + data = { + 'format': ImportFormatChoices.CSV, + 'data': self._get_csv_data(csv_data), + 'csv_delimiter': CSVDelimiterChoices.AUTO, + } + + # Form validation should fail with invalid header present + self.assertHttpStatus(self.client.post(self._get_url('import'), data), 200) + self.assertEqual(Region.objects.count(), 0) + + # Correct the CSV header name + csv_data[0] = 'name,slug,description' + data['data'] = self._get_csv_data(csv_data) + + # Validation should succeed + self.assertHttpStatus(self.client.post(self._get_url('import'), data), 302) + self.assertEqual(Region.objects.count(), 3) + @override_settings(EXEMPT_VIEW_PERMISSIONS=['*']) def test_valid_tags(self): csv_data = ( diff --git a/netbox/utilities/forms/bulk_import.py b/netbox/utilities/forms/bulk_import.py index 63cec2ba2..57362d3dd 100644 --- a/netbox/utilities/forms/bulk_import.py +++ b/netbox/utilities/forms/bulk_import.py @@ -129,6 +129,7 @@ class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form): headers, records = parse_csv(reader) # Set CSV headers for reference by the model form + headers.pop('id', None) self._csv_headers = headers return records diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 04d956a49..54c9e41cb 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -70,22 +70,24 @@ class CSVModelForm(forms.ModelForm): """ ModelForm used for the import of objects in CSV format. """ - def __init__(self, *args, headers=None, fields=None, **kwargs): - headers = headers or {} - fields = fields or [] + def __init__(self, *args, headers=None, **kwargs): + self.headers = headers or {} super().__init__(*args, **kwargs) # Modify the model form to accommodate any customized to_field_name properties - for field, to_field in headers.items(): + for field, to_field in self.headers.items(): if to_field is not None: self.fields[field].to_field_name = to_field - # Omit any fields not specified (e.g. because the form is being used to - # updated rather than create objects) - if fields: - for field in list(self.fields.keys()): - if field not in fields: - del self.fields[field] + def clean(self): + # Flag any invalid CSV headers + for header in self.headers: + if header not in self.fields: + raise forms.ValidationError( + _("Unrecognized header: {name}").format(name=header) + ) + + return super().clean() class FilterForm(BootstrapMixin, forms.Form): From 7a21541ed6368d99a902ef1e8046fda4abbc01ee Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2023 14:43:12 -0400 Subject: [PATCH 21/25] Plug NetBox Cloud in installation docs --- docs/installation/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/installation/index.md b/docs/installation/index.md index da50fa5fa..5affdf247 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -1,5 +1,8 @@ # Installation +!!! info "NetBox Cloud" + The instructions below are for installing NetBox as a standalone, self-hosted application. For a Cloud-delivered solution, check out [NetBox Cloud](https://netboxlabs.com/netbox-cloud/) by NetBox Labs. + The installation instructions provided here have been tested to work on Ubuntu 22.04 and CentOS 8.3. The particular commands needed to install dependencies on other distributions may vary significantly. Unfortunately, this is outside the control of the NetBox maintainers. Please consult your distribution's documentation for assistance with any errors. From 5dcf8502afe14b3110b88613cbbb219ac85e06ea Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2023 14:44:04 -0400 Subject: [PATCH 22/25] Grammar fix --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 54b3e727e..6e50e5687 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
NetBox logo -

The premiere source of truth powering network automation

+

The premier source of truth powering network automation

CI status

diff --git a/docs/index.md b/docs/index.md index 6a53403d6..05cd79f23 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ ![NetBox](netbox_logo.svg "NetBox logo"){style="height: 100px; margin-bottom: 3em"} -# The Premiere Network Source of Truth +# The Premier Network Source of Truth NetBox is the leading solution for modeling and documenting modern networks. By combining the traditional disciplines of IP address management (IPAM) and datacenter infrastructure management (DCIM) with powerful APIs and extensions, NetBox provides the ideal "source of truth" to power network automation. Read on to discover why thousands of organizations worldwide put NetBox at the heart of their infrastructure. From 4f05cf55a5bef36a3ea8b66ae4b78ae6daae7e11 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2023 14:47:47 -0400 Subject: [PATCH 23/25] Changelog for #11617, #12685, #13245, #13653, #13757, #13809, #13813, #13818 --- docs/release-notes/version-3.6.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 9b00ac2e5..094a859d9 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -4,22 +4,29 @@ ### Enhancements +* [#13245](https://github.com/netbox-community/netbox/issues/13245) - Add interface types for QSFP112 and OSFP-RHS * [#13563](https://github.com/netbox-community/netbox/issues/13563) - Add support for other delimiting characters when using CSV import ### Bug Fixes * [#11209](https://github.com/netbox-community/netbox/issues/11209) - Hide available IP/VLAN listing when sorting under a parent prefix or VLAN range +* [#11617](https://github.com/netbox-community/netbox/issues/11617) - Raise validation error on the presence of an unknown CSV header during bulk import * [#12219](https://github.com/netbox-community/netbox/issues/12219) - Fix dashboard widget heading contrast under dark mode +* [#12685](https://github.com/netbox-community/netbox/issues/12685) - Render Markdown in custom field help text on object edit forms +* [#13653](https://github.com/netbox-community/netbox/issues/13653) - Tweak color of error text to improve legibility * [#13701](https://github.com/netbox-community/netbox/issues/13701) - Correct display of power feed legs under device view * [#13706](https://github.com/netbox-community/netbox/issues/13706) - Restore extra filters dropdown on device interfaces list * [#13721](https://github.com/netbox-community/netbox/issues/13721) - Filter VLAN choices by selected site (if any) when creating a prefix * [#13727](https://github.com/netbox-community/netbox/issues/13727) - Fix exception when viewing rendered config for VM without a role assigned -* [#13741](https://github.com/netbox-community/netbox/issues/13741) - Enforce unique names for inventory items with no parent item * [#13745](https://github.com/netbox-community/netbox/issues/13745) - Optimize counter field migrations for large databases * [#13756](https://github.com/netbox-community/netbox/issues/13756) - Fix exception when sorting module bay list by installed module status +* [#13757](https://github.com/netbox-community/netbox/issues/13757) - Fix RecursionError exception when assigning config context to a device type * [#13767](https://github.com/netbox-community/netbox/issues/13767) - Fix support for comments when creating a new service via web UI * [#13782](https://github.com/netbox-community/netbox/issues/13782) - Fix tag exclusion support for contact assignments * [#13791](https://github.com/netbox-community/netbox/issues/13791) - Preserve whitespace in values when performing bulk rename of objects via web UI +* [#13809](https://github.com/netbox-community/netbox/issues/13809) - Avoid TypeError exception when editing active configuration with statically defined `CUSTOM_VALIDATORS` +* [#13813](https://github.com/netbox-community/netbox/issues/13813) - Fix member count for newly created virtual chassis +* [#13818](https://github.com/netbox-community/netbox/issues/13818) - Restore missing tags field on L2VPN termination edit form --- From b57a47475d9deecaab2424d995c2775f43fe4356 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2023 15:05:29 -0400 Subject: [PATCH 24/25] Release v3.6.2 --- .github/ISSUE_TEMPLATE/bug_report.yaml | 2 +- .github/ISSUE_TEMPLATE/feature_request.yaml | 2 +- contrib/generated_schema.json | 2 ++ docs/release-notes/version-3.6.md | 2 +- netbox/netbox/settings.py | 2 +- requirements.txt | 12 ++++++------ 6 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index ec7d667e6..8664768ee 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.6.1 + placeholder: v3.6.2 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index dc27ebd26..8e3af527a 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v3.6.1 + placeholder: v3.6.2 validations: required: true - type: dropdown diff --git a/contrib/generated_schema.json b/contrib/generated_schema.json index 9a6e2417a..5e8507798 100644 --- a/contrib/generated_schema.json +++ b/contrib/generated_schema.json @@ -342,8 +342,10 @@ "100gbase-x-qsfpdd", "200gbase-x-qsfp56", "200gbase-x-qsfpdd", + "400gbase-x-qsfp112", "400gbase-x-qsfpdd", "400gbase-x-osfp", + "400gbase-x-osfp-rhs", "400gbase-x-cdfp", "400gbase-x-cfp8", "800gbase-x-qsfpdd", diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index 094a859d9..db19b6c11 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -1,6 +1,6 @@ # NetBox v3.6 -## v3.6.2 (FUTURE) +## v3.6.2 (2023-09-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index c22281e9e..3977201e9 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.6.2-dev' +VERSION = '3.6.2' # Hostname HOSTNAME = platform.node() diff --git a/requirements.txt b/requirements.txt index 54f1334ed..931fc2c24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ bleach==6.0.0 Django==4.2.5 django-cors-headers==4.2.0 django-debug-toolbar==4.2.0 -django-filter==23.2 +django-filter==23.3 django-graphiql-debug-toolbar==0.2.0 django-mptt==0.14 django-pglocks==1.0.4 @@ -12,7 +12,7 @@ django-rich==1.7.0 django-rq==2.8.1 django-tables2==2.6.0 django-taggit==4.0.0 -django-timezone-field==6.0 +django-timezone-field==6.0.1 djangorestframework==3.14.0 drf-spectacular==0.26.4 drf-spectacular-sidecar==2023.9.1 @@ -21,13 +21,13 @@ graphene-django==3.0.0 gunicorn==21.2.0 Jinja2==3.1.2 Markdown==3.3.7 -mkdocs-material==9.2.7 +mkdocs-material==9.3.2 mkdocstrings[python-legacy]==0.23.0 -netaddr==0.8.0 -Pillow==10.0.0 +netaddr==0.9.0 +Pillow==10.0.1 psycopg[binary,pool]==3.1.10 PyYAML==6.0.1 -sentry-sdk==1.30.0 +sentry-sdk==1.31.0 social-auth-app-django==5.3.0 social-auth-core[openidconnect]==4.4.2 svgwrite==1.4.3 From 9b325f4b8611160f62294cfdb6bafebae2757bdd Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 20 Sep 2023 15:32:41 -0400 Subject: [PATCH 25/25] PRVB --- docs/release-notes/version-3.6.md | 4 ++++ netbox/netbox/settings.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/release-notes/version-3.6.md b/docs/release-notes/version-3.6.md index db19b6c11..4339682c2 100644 --- a/docs/release-notes/version-3.6.md +++ b/docs/release-notes/version-3.6.md @@ -1,5 +1,9 @@ # NetBox v3.6 +## v3.6.3 (FUTURE) + +--- + ## v3.6.2 (2023-09-20) ### Enhancements diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 3977201e9..53700073e 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from netbox.constants import RQ_QUEUE_DEFAULT, RQ_QUEUE_HIGH, RQ_QUEUE_LOW # Environment setup # -VERSION = '3.6.2' +VERSION = '3.6.3-dev' # Hostname HOSTNAME = platform.node()